Added right and left shift operators
This commit is contained in:
parent
bc6fdaf8f2
commit
0357df9873
11 changed files with 2350 additions and 37 deletions
113
commands.md
113
commands.md
|
|
@ -225,33 +225,56 @@ NEXT
|
|||
|
||||
## FUNC
|
||||
|
||||
Defines a function with optional parameters.
|
||||
Defines a function with optional parameters. Functions must be terminated with `FEND`.
|
||||
|
||||
**Modern syntax (recommended):** Use implicit declarations with curly braces for self-contained functions with local parameters.
|
||||
|
||||
**Legacy syntax:** Can use pre-declared global variables for direct memory access.
|
||||
|
||||
Parameter passing modes: `in:` (default, read-only), `out:` (write-only), `io:` (read-write)
|
||||
|
||||
**Syntax:**
|
||||
```
|
||||
FUNC <n>
|
||||
FUNC <n>(<param1>[,<param2>,...])
|
||||
FUNC name # void function
|
||||
FUNC name ( {BYTE param} ) # single parameter (modern)
|
||||
FUNC name ( {BYTE a} {BYTE b} ) # multiple parameters (modern)
|
||||
FUNC name ( in:{BYTE x} out:{BYTE y} io:{BYTE z} ) # all direction modifiers
|
||||
FUNC name ( {BYTE param @ $fa} ) # parameter at absolute address
|
||||
|
||||
FUNC name ( param ) # single pre-declared variable (legacy)
|
||||
FUNC name ( a b ) # multiple pre-declared vars (legacy)
|
||||
FUNC name ( in:x out:y io:z ) # direction with pre-declared (legacy)
|
||||
```
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
FUNC initialize
|
||||
BYTE temp = 0
|
||||
screen = temp
|
||||
FEND
|
||||
|
||||
FUNC add(in:a,in:b,out:result)
|
||||
// Modern: self-contained function with local parameters
|
||||
FUNC add({BYTE a} {BYTE b} {BYTE result})
|
||||
result = a + b
|
||||
FEND
|
||||
|
||||
FUNC process(value,{BYTE temp})
|
||||
temp = value + 1
|
||||
// Modern: with direction modifiers
|
||||
FUNC process(in:{BYTE input} out:{BYTE output})
|
||||
output = input + 1
|
||||
FEND
|
||||
|
||||
FUNC swap(io:x,io:y)
|
||||
BYTE temp = x
|
||||
// Modern: io parameter (read-write)
|
||||
FUNC increment(io:{BYTE counter})
|
||||
counter = counter + 1
|
||||
FEND
|
||||
|
||||
// Modern: absolute address parameter
|
||||
FUNC read_byte({BYTE data @ $fa})
|
||||
BYTE temp
|
||||
temp = data // Reads from $fa
|
||||
FEND
|
||||
|
||||
// Legacy: uses global variables directly
|
||||
BYTE x
|
||||
BYTE y
|
||||
FUNC swap_legacy ( x y )
|
||||
BYTE temp
|
||||
temp = x
|
||||
x = y
|
||||
y = temp
|
||||
FEND
|
||||
|
|
@ -578,6 +601,70 @@ LABEL checkValue
|
|||
|
||||
---
|
||||
|
||||
## SHIFTL
|
||||
|
||||
Logical shift left operation.
|
||||
|
||||
**Syntax:**
|
||||
```
|
||||
SHIFTL <source> BY <amount> GIVING <dest>
|
||||
SHIFTL <source> << <amount> -> <dest>
|
||||
<dest> = <source> << <amount>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `source`: BYTE or WORD variable or constant expression
|
||||
- `amount`: BYTE variable or constant expression (0-255)
|
||||
- `dest`: BYTE or WORD variable (cannot be constant)
|
||||
|
||||
**Notes:**
|
||||
- Logical shift only (no sign extension)
|
||||
- Shift amount of 0 copies source to dest unchanged
|
||||
- Shift amount ≥ bit-width yields zero (≥8 for BYTE, ≥16 for WORD)
|
||||
- BYTE→WORD conversion zero-extends before shifting
|
||||
- WORD→BYTE conversion truncates low byte only
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
result = value << 3
|
||||
SHIFTL mask BY bits GIVING shifted
|
||||
SHIFTR flags >> 2 -> masked
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SHIFTR
|
||||
|
||||
Logical shift right operation.
|
||||
|
||||
**Syntax:**
|
||||
```
|
||||
SHIFTR <source> BY <amount> GIVING <dest>
|
||||
SHIFTR <source> >> <amount> -> <dest>
|
||||
<dest> = <source> >> <amount>
|
||||
```
|
||||
|
||||
**Parameters:**
|
||||
- `source`: BYTE or WORD variable or constant expression
|
||||
- `amount`: BYTE variable or constant expression (0-255)
|
||||
- `dest`: BYTE or WORD variable (cannot be constant)
|
||||
|
||||
**Notes:**
|
||||
- Logical shift only (no sign extension)
|
||||
- Shift amount of 0 copies source to dest unchanged
|
||||
- Shift amount ≥ bit-width yields zero (≥8 for BYTE, ≥16 for WORD)
|
||||
- BYTE→WORD conversion zero-extends before shifting
|
||||
- WORD→BYTE conversion truncates low byte only
|
||||
|
||||
**Examples:**
|
||||
```
|
||||
mask = flags >> 2
|
||||
SHIFTR value BY shift GIVING result
|
||||
SHIFTR data >> 4 -> nibble
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SUBTR
|
||||
|
||||
Subtracts second value from first.
|
||||
|
|
|
|||
20
examples/shift_demo/cm.sh
Executable file
20
examples/shift_demo/cm.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
# Define filename as variable
|
||||
PROGNAME="shift_demo"
|
||||
# Only set C65LIBPATH if not already defined
|
||||
if [ -z "$C65LIBPATH" ]; then
|
||||
export C65LIBPATH=$(readlink -f "../../lib")
|
||||
fi
|
||||
# Compile - use absolute path to c65gm
|
||||
c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Compilation terminated"
|
||||
exit 1
|
||||
fi
|
||||
echo assemble.
|
||||
acme ${PROGNAME}.s
|
||||
if [ -f ${PROGNAME}.prg ]; then
|
||||
rm ${PROGNAME}.prg
|
||||
fi
|
||||
# main.bin ${PROGNAME}.prg
|
||||
mv main.bin main.prg
|
||||
410
examples/shift_demo/shift_demo.c65
Normal file
410
examples/shift_demo/shift_demo.c65
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
//-----------------------------------------------------------
|
||||
// Shift Operations Demo
|
||||
// Practical examples of << and >> operators
|
||||
// Shows real-world uses for bit manipulation
|
||||
//-----------------------------------------------------------
|
||||
|
||||
#INCLUDE <c64start.c65>
|
||||
#INCLUDE <c64defs.c65>
|
||||
#INCLUDE <cbmiolib.c65>
|
||||
|
||||
#PRAGMA _P_USE_CBM_STRINGS 1
|
||||
|
||||
GOTO start
|
||||
|
||||
// Variables for demos
|
||||
BYTE demo_value
|
||||
BYTE demo_result
|
||||
BYTE demo_shift
|
||||
WORD demo_word
|
||||
WORD demo_word_result
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Wait for key press
|
||||
//-----------------------------------------------------------
|
||||
FUNC wait_key
|
||||
BYTE key
|
||||
|
||||
// Wait for no key
|
||||
WHILE 1
|
||||
key = PEEK $c5
|
||||
IF key = 64
|
||||
BREAK
|
||||
ENDIF
|
||||
WEND
|
||||
|
||||
// Wait for key press
|
||||
WHILE 1
|
||||
key = PEEK $c5
|
||||
IF key != 64
|
||||
BREAK
|
||||
ENDIF
|
||||
WEND
|
||||
|
||||
// Reset key buffer
|
||||
POKE $c6 WITH 0
|
||||
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Clear screen
|
||||
//-----------------------------------------------------------
|
||||
FUNC clear_screen
|
||||
lib_cbmio_cls()
|
||||
lib_cbmio_home()
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Print binary representation of byte
|
||||
//-----------------------------------------------------------
|
||||
FUNC print_binary({BYTE val})
|
||||
BYTE i = 7
|
||||
BYTE mask
|
||||
BYTE bit
|
||||
|
||||
lib_cbmio_print(" %")
|
||||
|
||||
WHILE i < 8 // Will break when i wraps around to 255
|
||||
mask = 1 << i
|
||||
bit = val & mask
|
||||
IF bit != 0
|
||||
lib_cbmio_print("1")
|
||||
ELSE
|
||||
lib_cbmio_print("0")
|
||||
ENDIF
|
||||
|
||||
// Add space every 4 bits for readability
|
||||
IF i = 4
|
||||
lib_cbmio_print(" ")
|
||||
ENDIF
|
||||
|
||||
// Decrement i, break if we go below 0
|
||||
IF i = 0
|
||||
BREAK
|
||||
ENDIF
|
||||
i = i - 1
|
||||
WEND
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Demo 1: Basic shift operations
|
||||
//-----------------------------------------------------------
|
||||
FUNC demo_basic_shifts
|
||||
lib_cbmio_printlf("basic shift operations")
|
||||
lib_cbmio_printlf("======================")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Constant left shift
|
||||
demo_value = $03 // Binary: 00000011
|
||||
demo_result = demo_value << 2
|
||||
lib_cbmio_print("$03 << 2 = $")
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
print_binary(demo_result)
|
||||
lib_cbmio_printlf(" (3 * 4 = 12)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Constant right shift
|
||||
demo_value = $30 // Binary: 00110000
|
||||
demo_result = demo_value >> 3
|
||||
lib_cbmio_print("$30 >> 3 = $")
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
print_binary(demo_result)
|
||||
lib_cbmio_printlf(" (48 / 8 = 6)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Variable shift amount
|
||||
demo_value = $80 // Binary: 10000000
|
||||
demo_shift = 3
|
||||
demo_result = demo_value >> demo_shift
|
||||
lib_cbmio_print("$80 >> ")
|
||||
lib_cbmio_hexoutb(demo_shift)
|
||||
lib_cbmio_print(" = $")
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
print_binary(demo_result)
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
wait_key()
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Demo 2: Bit manipulation
|
||||
//-----------------------------------------------------------
|
||||
FUNC demo_bit_manipulation
|
||||
lib_cbmio_printlf("bit manipulation")
|
||||
lib_cbmio_printlf("================")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Extract color components from C64 color byte
|
||||
// C64 color: bits 7-4 = background, bits 3-0 = foreground
|
||||
BYTE color = $3E // Background: 3, Foreground: E
|
||||
BYTE background
|
||||
BYTE foreground
|
||||
|
||||
background = color >> 4
|
||||
foreground = color & $0F
|
||||
|
||||
lib_cbmio_print("color byte: $")
|
||||
lib_cbmio_hexoutb(color)
|
||||
print_binary(color)
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_print("background: $")
|
||||
lib_cbmio_hexoutb(background)
|
||||
lib_cbmio_printlf(" (>> 4)")
|
||||
lib_cbmio_print("foreground: $")
|
||||
lib_cbmio_hexoutb(foreground)
|
||||
lib_cbmio_printlf(" (& $0f)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Create bit masks
|
||||
lib_cbmio_printlf("bit masks:")
|
||||
BYTE mask
|
||||
|
||||
mask = 1 << 0
|
||||
lib_cbmio_print("1 << 0 = $")
|
||||
lib_cbmio_hexoutb(mask)
|
||||
lib_cbmio_print(" (bit 0)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
mask = 1 << 7
|
||||
lib_cbmio_print("1 << 7 = $")
|
||||
lib_cbmio_hexoutb(mask)
|
||||
lib_cbmio_print(" (bit 7)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
mask = 1 | 2 // Bits 0 and 1
|
||||
lib_cbmio_print("bits 0+1 = $")
|
||||
lib_cbmio_hexoutb(mask)
|
||||
print_binary(mask)
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
wait_key()
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Demo 3: Multiplication and division
|
||||
//-----------------------------------------------------------
|
||||
FUNC demo_multiply_divide
|
||||
lib_cbmio_printlf("fast multiply/divide")
|
||||
lib_cbmio_printlf("====================")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Multiplication by powers of 2
|
||||
BYTE value = 7
|
||||
|
||||
lib_cbmio_print("7 * 2 = ")
|
||||
demo_result = value << 1
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
lib_cbmio_printlf(" (7 << 1)")
|
||||
|
||||
lib_cbmio_print("7 * 4 = ")
|
||||
demo_result = value << 2
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
lib_cbmio_printlf(" (7 << 2)")
|
||||
|
||||
lib_cbmio_print("7 * 8 = ")
|
||||
demo_result = value << 3
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
lib_cbmio_printlf(" (7 << 3)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Division by powers of 2
|
||||
value = 100
|
||||
|
||||
lib_cbmio_print("100 / 2 = ")
|
||||
demo_result = value >> 1
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
lib_cbmio_printlf(" (100 >> 1)")
|
||||
|
||||
lib_cbmio_print("100 / 4 = ")
|
||||
demo_result = value >> 2
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
lib_cbmio_printlf(" (100 >> 2)")
|
||||
|
||||
lib_cbmio_print("100 / 8 = ")
|
||||
demo_result = value >> 3
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
lib_cbmio_printlf(" (100 >> 3)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Variable shift for scaling
|
||||
value = 5
|
||||
demo_shift = 3 // Multiply by 8
|
||||
|
||||
lib_cbmio_print("5 * 8 = ")
|
||||
demo_result = value << demo_shift
|
||||
lib_cbmio_hexoutb(demo_result)
|
||||
lib_cbmio_print(" (5 << ")
|
||||
lib_cbmio_hexoutb(demo_shift)
|
||||
lib_cbmio_printlf(")")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
wait_key()
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Demo 4: Word operations
|
||||
//-----------------------------------------------------------
|
||||
FUNC demo_word_operations
|
||||
lib_cbmio_printlf("16-bit word operations")
|
||||
lib_cbmio_printlf("======================")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Word left shift (multiply by 2)
|
||||
demo_word = $1234
|
||||
demo_word_result = demo_word << 1
|
||||
|
||||
lib_cbmio_print("$1234 << 1 = $")
|
||||
lib_cbmio_hexoutw(demo_word_result)
|
||||
lib_cbmio_printlf(" ($1234 * 2 = $2468)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Word right shift (divide by 2)
|
||||
demo_word = $ABCD
|
||||
demo_word_result = demo_word >> 1
|
||||
|
||||
lib_cbmio_print("$abcd >> 1 = $")
|
||||
lib_cbmio_hexoutw(demo_word_result)
|
||||
lib_cbmio_printlf(" ($abcd / 2 = $55e6)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Shift between bytes
|
||||
demo_word = $00FF
|
||||
demo_word_result = demo_word << 8
|
||||
|
||||
lib_cbmio_print("$00ff << 8 = $")
|
||||
lib_cbmio_hexoutw(demo_word_result)
|
||||
lib_cbmio_printlf(" (low->high byte)")
|
||||
|
||||
demo_word = $FF00
|
||||
demo_word_result = demo_word >> 8
|
||||
|
||||
lib_cbmio_print("$ff00 >> 8 = $")
|
||||
lib_cbmio_hexoutw(demo_word_result)
|
||||
lib_cbmio_printlf(" (high->low byte)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Byte to word conversion with shift
|
||||
BYTE small = $81
|
||||
WORD large
|
||||
|
||||
large = small << 2 // Zero-extends byte to word, then shifts
|
||||
|
||||
lib_cbmio_print("byte $81 << 2 = word $")
|
||||
lib_cbmio_hexoutw(large)
|
||||
lib_cbmio_printlf(" (zero-extended)")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
wait_key()
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Demo 5: Practical C64 example
|
||||
//-----------------------------------------------------------
|
||||
FUNC demo_c64_example
|
||||
lib_cbmio_printlf("c64 practical example")
|
||||
lib_cbmio_printlf("=====================")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Simulate reading joystick port 2
|
||||
// Bits: 0=up, 1=down, 2=left, 3=right, 4=fire
|
||||
BYTE joystick = $17 // Binary: 00010111 (up, down, right, fire)
|
||||
|
||||
lib_cbmio_print("joystick port: $")
|
||||
lib_cbmio_hexoutb(joystick)
|
||||
print_binary(joystick)
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Check individual buttons using shifts
|
||||
lib_cbmio_printlf("checking buttons:")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Fire button (bit 4)
|
||||
BYTE fire_mask
|
||||
BYTE fire_check
|
||||
fire_mask = 1 << 4
|
||||
fire_check = joystick & fire_mask
|
||||
IF fire_check = 0
|
||||
lib_cbmio_printlf("fire: pressed")
|
||||
ELSE
|
||||
lib_cbmio_printlf("fire: not pressed")
|
||||
ENDIF
|
||||
|
||||
// Up button (bit 0)
|
||||
BYTE up_mask
|
||||
BYTE up_check
|
||||
up_mask = 1 << 0
|
||||
up_check = joystick & up_mask
|
||||
IF up_check = 0
|
||||
lib_cbmio_printlf("up: pressed")
|
||||
ELSE
|
||||
lib_cbmio_printlf("up: not pressed")
|
||||
ENDIF
|
||||
|
||||
// Right button (bit 3)
|
||||
BYTE right_mask
|
||||
BYTE right_check
|
||||
right_mask = 1 << 3
|
||||
right_check = joystick & right_mask
|
||||
IF right_check = 0
|
||||
lib_cbmio_printlf("right: pressed")
|
||||
ELSE
|
||||
lib_cbmio_printlf("right: not pressed")
|
||||
ENDIF
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Extract direction bits to nibble
|
||||
BYTE direction
|
||||
direction = joystick & $0F // Mask off fire button
|
||||
|
||||
lib_cbmio_print("direction bits: $")
|
||||
lib_cbmio_hexoutb(direction)
|
||||
print_binary(direction)
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
wait_key()
|
||||
FEND
|
||||
|
||||
//-----------------------------------------------------------
|
||||
// Main program
|
||||
//-----------------------------------------------------------
|
||||
LABEL start
|
||||
clear_screen()
|
||||
|
||||
lib_cbmio_printlf("shift operators demo")
|
||||
lib_cbmio_printlf("====================")
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_printlf("press any key...")
|
||||
lib_cbmio_printlf("")
|
||||
wait_key()
|
||||
|
||||
demo_basic_shifts()
|
||||
clear_screen()
|
||||
|
||||
demo_bit_manipulation()
|
||||
clear_screen()
|
||||
|
||||
demo_multiply_divide()
|
||||
clear_screen()
|
||||
|
||||
demo_word_operations()
|
||||
clear_screen()
|
||||
|
||||
demo_c64_example()
|
||||
clear_screen()
|
||||
|
||||
lib_cbmio_printlf("demo complete!")
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_printlf("shift operators:")
|
||||
lib_cbmio_printlf(" << left shift (multiply)")
|
||||
lib_cbmio_printlf(" >> right shift (divide)")
|
||||
lib_cbmio_printlf("")
|
||||
lib_cbmio_printlf("works with bytes and words")
|
||||
lib_cbmio_printlf("")
|
||||
|
||||
// Infinite loop
|
||||
LABEL loop
|
||||
GOTO loop
|
||||
1
examples/shift_demo/start_in_vice.sh
Normal file
1
examples/shift_demo/start_in_vice.sh
Normal file
|
|
@ -0,0 +1 @@
|
|||
x64 -autostartprgmode 1 main.prg
|
||||
348
internal/commands/shiftl.go
Normal file
348
internal/commands/shiftl.go
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"c65gm/internal/compiler"
|
||||
"c65gm/internal/preproc"
|
||||
"c65gm/internal/utils"
|
||||
)
|
||||
|
||||
// ShiftLCommand handles logical shift left operations
|
||||
// Syntax:
|
||||
//
|
||||
// SHIFTL <source> BY <amount> GIVING <dest> # old syntax with BY/GIVING
|
||||
// SHIFTL <source> << <amount> -> <dest> # old syntax with <</->
|
||||
// <dest> = <source> << <amount> # new syntax
|
||||
type ShiftLCommand struct {
|
||||
sourceVarName string
|
||||
sourceVarKind compiler.VarKind
|
||||
sourceValue uint16
|
||||
sourceIsVar bool
|
||||
|
||||
amountVarName string
|
||||
amountVarKind compiler.VarKind
|
||||
amountValue uint16
|
||||
amountIsVar bool
|
||||
|
||||
destVarName string
|
||||
destVarKind compiler.VarKind
|
||||
|
||||
line preproc.Line // Store line info for warnings
|
||||
}
|
||||
|
||||
func (c *ShiftLCommand) WillHandle(line preproc.Line) bool {
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
if err != nil || len(params) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Old syntax: SHIFTL ... (must have exactly 6 params)
|
||||
if strings.ToUpper(params[0]) == "SHIFTL" && len(params) == 6 {
|
||||
return true
|
||||
}
|
||||
|
||||
// New syntax: <dest> = <source> << <amount>
|
||||
if len(params) == 5 && params[1] == "=" && params[3] == "<<" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *ShiftLCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
||||
// Clear state
|
||||
c.sourceVarName = ""
|
||||
c.sourceIsVar = false
|
||||
c.sourceValue = 0
|
||||
c.amountVarName = ""
|
||||
c.amountIsVar = false
|
||||
c.amountValue = 0
|
||||
c.destVarName = ""
|
||||
c.line = line // Store line for warnings
|
||||
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
paramCount := len(params)
|
||||
scope := ctx.CurrentScope()
|
||||
|
||||
// Create constant lookup function
|
||||
constLookup := ctx.SymbolTable.ConstantLookupFunc(scope)
|
||||
|
||||
// Determine syntax and parse accordingly
|
||||
if strings.ToUpper(params[0]) == "SHIFTL" {
|
||||
// Old syntax: SHIFTL <source> BY/<< <amount> GIVING/-> <dest>
|
||||
if paramCount != 6 {
|
||||
return fmt.Errorf("SHIFTL: wrong number of parameters (%d), expected 6", paramCount)
|
||||
}
|
||||
|
||||
separator1 := strings.ToUpper(params[2])
|
||||
if separator1 != "BY" && separator1 != "<<" {
|
||||
return fmt.Errorf("SHIFTL: parameter #3 must be 'BY' or '<<', got %q", params[2])
|
||||
}
|
||||
|
||||
separator2 := strings.ToUpper(params[4])
|
||||
if separator2 != "GIVING" && separator2 != "->" {
|
||||
return fmt.Errorf("SHIFTL: parameter #5 must be 'GIVING' or '->', got %q", params[4])
|
||||
}
|
||||
|
||||
// Parse destination
|
||||
destName := params[5]
|
||||
destSym := ctx.SymbolTable.Lookup(destName, scope)
|
||||
if destSym == nil {
|
||||
return fmt.Errorf("SHIFTL: unknown variable %q", destName)
|
||||
}
|
||||
if destSym.IsConst() {
|
||||
return fmt.Errorf("SHIFTL: cannot assign to constant %q", destName)
|
||||
}
|
||||
c.destVarName = destSym.FullName()
|
||||
c.destVarKind = destSym.GetVarKind()
|
||||
|
||||
// Parse source
|
||||
var err error
|
||||
c.sourceVarName, c.sourceVarKind, c.sourceValue, c.sourceIsVar, err = compiler.ParseOperandParam(
|
||||
params[1], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTL: source: %w", err)
|
||||
}
|
||||
|
||||
// Parse amount
|
||||
c.amountVarName, c.amountVarKind, c.amountValue, c.amountIsVar, err = compiler.ParseOperandParam(
|
||||
params[3], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTL: amount: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// New syntax: <dest> = <source> << <amount>
|
||||
if paramCount != 5 {
|
||||
return fmt.Errorf("SHIFTL: wrong number of parameters (%d), expected 5", paramCount)
|
||||
}
|
||||
|
||||
if params[1] != "=" {
|
||||
return fmt.Errorf("SHIFTL: expected '=' at position 2, got %q", params[1])
|
||||
}
|
||||
|
||||
if params[3] != "<<" {
|
||||
return fmt.Errorf("SHIFTL: expected '<<' at position 4, got %q", params[3])
|
||||
}
|
||||
|
||||
// Parse destination
|
||||
destName := params[0]
|
||||
destSym := ctx.SymbolTable.Lookup(destName, scope)
|
||||
if destSym == nil {
|
||||
return fmt.Errorf("SHIFTL: unknown variable %q", destName)
|
||||
}
|
||||
if destSym.IsConst() {
|
||||
return fmt.Errorf("SHIFTL: cannot assign to constant %q", destName)
|
||||
}
|
||||
c.destVarName = destSym.FullName()
|
||||
c.destVarKind = destSym.GetVarKind()
|
||||
|
||||
// Parse source
|
||||
var err error
|
||||
c.sourceVarName, c.sourceVarKind, c.sourceValue, c.sourceIsVar, err = compiler.ParseOperandParam(
|
||||
params[2], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTL: source: %w", err)
|
||||
}
|
||||
|
||||
// Parse amount
|
||||
c.amountVarName, c.amountVarKind, c.amountValue, c.amountIsVar, err = compiler.ParseOperandParam(
|
||||
params[4], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTL: amount: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate amount
|
||||
if c.amountIsVar {
|
||||
if c.amountVarKind == compiler.KindWord {
|
||||
return fmt.Errorf("SHIFTL: amount must be BYTE variable, got WORD %q", c.amountVarName)
|
||||
}
|
||||
} else {
|
||||
if c.amountValue > 255 {
|
||||
return fmt.Errorf("SHIFTL: amount constant %d out of BYTE range (0-255)", c.amountValue)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ShiftLCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||
var asm []string
|
||||
|
||||
// Check if shift amount >= bit width (result will be zero)
|
||||
bitWidth := 8
|
||||
if c.destVarKind == compiler.KindWord {
|
||||
bitWidth = 16
|
||||
}
|
||||
|
||||
amountZero := false
|
||||
if !c.amountIsVar {
|
||||
// Constant amount
|
||||
if c.amountValue >= uint16(bitWidth) {
|
||||
amountZero = true
|
||||
}
|
||||
}
|
||||
|
||||
if amountZero {
|
||||
// Result is zero, just store zero
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// Step 1: Copy source to destination if needed
|
||||
if c.sourceIsVar && c.sourceVarName == c.destVarName {
|
||||
// Same variable, no copy needed
|
||||
} else {
|
||||
copyAsm := c.generateCopy()
|
||||
asm = append(asm, copyAsm...)
|
||||
}
|
||||
|
||||
// Step 2: Apply shift
|
||||
shiftAsm, err := c.generateShift(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
asm = append(asm, shiftAsm...)
|
||||
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// generateCopy generates assembly to copy source to destination
|
||||
func (c *ShiftLCommand) generateCopy() []string {
|
||||
var asm []string
|
||||
|
||||
// If source is literal, just load it
|
||||
if !c.sourceIsVar {
|
||||
lo := uint8(c.sourceValue & 0xFF)
|
||||
hi := uint8((c.sourceValue >> 8) & 0xFF)
|
||||
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
// Optimization: don't reload if lo == hi
|
||||
if lo != hi {
|
||||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
|
||||
}
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
return asm
|
||||
}
|
||||
|
||||
// Source is variable
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
// Destination is byte
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
// Destination is word
|
||||
if c.sourceVarKind == compiler.KindByte {
|
||||
// Byte -> Word (zero-extend)
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
} else {
|
||||
// Word -> Word
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
}
|
||||
|
||||
return asm
|
||||
}
|
||||
|
||||
// generateShift generates assembly to shift destination left by amount
|
||||
func (c *ShiftLCommand) generateShift(ctx *compiler.CompilerContext) ([]string, error) {
|
||||
var asm []string
|
||||
|
||||
// Constant amount
|
||||
if !c.amountIsVar {
|
||||
amount := c.amountValue
|
||||
if amount == 0 {
|
||||
return asm, nil // No shift needed
|
||||
}
|
||||
|
||||
// Determine bit width
|
||||
bitWidth := 8
|
||||
if c.destVarKind == compiler.KindWord {
|
||||
bitWidth = 16
|
||||
}
|
||||
|
||||
// Warn if shift amount >= bit width (but not for 0)
|
||||
if amount >= uint16(bitWidth) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s:%d: warning: shift amount %d >= %d bits, value will be zero\n",
|
||||
c.line.Filename, c.line.LineNo, amount, bitWidth)
|
||||
}
|
||||
|
||||
if amount >= uint16(bitWidth) {
|
||||
// Shift all bits out -> zero
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// Unroll shift loop
|
||||
for i := uint16(0); i < amount; i++ {
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, fmt.Sprintf("\tasl %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, fmt.Sprintf("\tasl %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\trol %s+1", c.destVarName))
|
||||
}
|
||||
}
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// Variable amount
|
||||
// Generate labels
|
||||
loopLabel := ctx.GeneralStack.Push()
|
||||
ctx.GeneralStack.Pop()
|
||||
doneLabel := ctx.GeneralStack.Push()
|
||||
ctx.GeneralStack.Pop()
|
||||
|
||||
// Load amount into X
|
||||
asm = append(asm, fmt.Sprintf("\tldx %s", c.amountVarName))
|
||||
|
||||
// Check for zero amount
|
||||
asm = append(asm, fmt.Sprintf("\tbeq %s", doneLabel))
|
||||
|
||||
// Shift loop
|
||||
asm = append(asm, loopLabel)
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, fmt.Sprintf("\tasl %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, fmt.Sprintf("\tasl %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\trol %s+1", c.destVarName))
|
||||
}
|
||||
asm = append(asm, "\tdex")
|
||||
asm = append(asm, fmt.Sprintf("\tbne %s", loopLabel))
|
||||
|
||||
asm = append(asm, doneLabel)
|
||||
return asm, nil
|
||||
}
|
||||
551
internal/commands/shiftl_test.go
Normal file
551
internal/commands/shiftl_test.go
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"c65gm/internal/compiler"
|
||||
"c65gm/internal/preproc"
|
||||
)
|
||||
|
||||
func TestShiftLCommand_WillHandle(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
want bool
|
||||
}{
|
||||
// Old syntax
|
||||
{"old syntax BY/GIVING", "SHIFTL a BY b GIVING c", true},
|
||||
{"old syntax <</->", "SHIFTL a << b -> c", true},
|
||||
{"old syntax mixed case", "shiftl x by y giving z", true},
|
||||
|
||||
// New syntax
|
||||
{"new syntax basic", "result = a << b", true},
|
||||
{"new syntax with literals", "x = 10 << 3", true},
|
||||
|
||||
// Should not handle
|
||||
{"not shiftl - shiftr", "SHIFTR a BY b GIVING c", false},
|
||||
{"not shiftl - add", "result = a + b", false},
|
||||
{"not shiftl - wrong params", "SHIFTL a b c", false},
|
||||
{"empty", "", false},
|
||||
{"just SHIFTL", "SHIFTL", false},
|
||||
{"assignment without shift", "x = y", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := &ShiftLCommand{}
|
||||
line := preproc.Line{Text: tt.text}
|
||||
if got := cmd.WillHandle(line); got != tt.want {
|
||||
t.Errorf("WillHandle() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftLCommand_Interpret_OldSyntax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*compiler.CompilerContext)
|
||||
text string
|
||||
wantErr bool
|
||||
check func(*testing.T, *ShiftLCommand)
|
||||
}{
|
||||
{
|
||||
name: "byte << byte -> byte",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 3, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a BY b GIVING c",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if !cmd.sourceIsVar || cmd.sourceVarName != "a" {
|
||||
t.Errorf("source should be var 'a'")
|
||||
}
|
||||
if !cmd.amountIsVar || cmd.amountVarName != "b" {
|
||||
t.Errorf("amount should be var 'b'")
|
||||
}
|
||||
if cmd.destVarName != "c" || cmd.destVarKind != compiler.KindByte {
|
||||
t.Errorf("dest should be byte 'c'")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "word << byte -> word",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("x", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("n", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL x BY n GIVING result",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if cmd.sourceVarKind != compiler.KindWord {
|
||||
t.Errorf("source should be word")
|
||||
}
|
||||
if cmd.destVarKind != compiler.KindWord {
|
||||
t.Errorf("dest should be word")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "literal << var -> byte",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL $FF BY shift GIVING result",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if cmd.sourceIsVar {
|
||||
t.Errorf("source should be literal")
|
||||
}
|
||||
if cmd.sourceValue != 0xFF {
|
||||
t.Errorf("source value = %d, want 255", cmd.sourceValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "var << literal -> byte",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 1, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL value BY 3 GIVING result",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if cmd.amountIsVar {
|
||||
t.Errorf("amount should be literal")
|
||||
}
|
||||
if cmd.amountValue != 3 {
|
||||
t.Errorf("amount value = %d, want 3", cmd.amountValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arrow syntax",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a << b -> c",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if cmd.destVarName != "c" {
|
||||
t.Errorf("dest should be c")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown variable",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a BY b GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "assign to constant",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 255, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a BY b GIVING MAX",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "word amount variable (error)",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindWord, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a BY b GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "amount constant > 255 (error)",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a BY 300 GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong separator #3",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a WITH b GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong separator #5",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTL a BY b INTO c",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := compiler.NewCompilerContext(preproc.NewPragma())
|
||||
if tt.setup != nil {
|
||||
tt.setup(ctx)
|
||||
}
|
||||
|
||||
cmd := &ShiftLCommand{}
|
||||
line := preproc.Line{Text: tt.text}
|
||||
|
||||
err := cmd.Interpret(line, ctx)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Interpret() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr && tt.check != nil {
|
||||
tt.check(t, cmd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftLCommand_Interpret_NewSyntax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*compiler.CompilerContext)
|
||||
text string
|
||||
wantErr bool
|
||||
check func(*testing.T, *ShiftLCommand)
|
||||
}{
|
||||
{
|
||||
name: "dest = var << var",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = a << b",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if cmd.destVarName != "result" {
|
||||
t.Errorf("dest = %q, want 'result'", cmd.destVarName)
|
||||
}
|
||||
if !cmd.sourceIsVar || cmd.sourceVarName != "a" {
|
||||
t.Errorf("source should be var 'a'")
|
||||
}
|
||||
if !cmd.amountIsVar || cmd.amountVarName != "b" {
|
||||
t.Errorf("amount should be var 'b'")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dest = literal << literal",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = $01 << 3",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if cmd.sourceIsVar || cmd.amountIsVar {
|
||||
t.Errorf("both params should be literals")
|
||||
}
|
||||
if cmd.sourceValue != 1 || cmd.amountValue != 3 {
|
||||
t.Errorf("source=%d, amount=%d, want 1,3", cmd.sourceValue, cmd.amountValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "word destination",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 1, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = value << shift",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftLCommand) {
|
||||
if cmd.destVarKind != compiler.KindWord {
|
||||
t.Errorf("dest should be word")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown dest variable",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = a << b",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong operator (not <<)",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = a >> b",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := compiler.NewCompilerContext(preproc.NewPragma())
|
||||
if tt.setup != nil {
|
||||
tt.setup(ctx)
|
||||
}
|
||||
|
||||
cmd := &ShiftLCommand{}
|
||||
line := preproc.Line{Text: tt.text}
|
||||
|
||||
err := cmd.Interpret(line, ctx)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Interpret() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr && tt.check != nil {
|
||||
tt.check(t, cmd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftLCommand_Generate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*compiler.CompilerContext) *ShiftLCommand
|
||||
wantLines []string
|
||||
}{
|
||||
{
|
||||
name: "constant folding - byte << 0",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0x55,
|
||||
amountIsVar: false,
|
||||
amountValue: 0,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #$55",
|
||||
"\tsta result",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - byte << 3",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0x01,
|
||||
amountIsVar: false,
|
||||
amountValue: 3,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #$01",
|
||||
"\tsta result",
|
||||
"\tasl result",
|
||||
"\tasl result",
|
||||
"\tasl result",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - byte << 8 (zero)",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0xFF,
|
||||
amountIsVar: false,
|
||||
amountValue: 8,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #0",
|
||||
"\tsta result",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - word << 1",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0x1234,
|
||||
amountIsVar: false,
|
||||
amountValue: 1,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindWord,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #$34",
|
||||
"\tsta result",
|
||||
"\tlda #$12",
|
||||
"\tsta result+1",
|
||||
"\tasl result",
|
||||
"\trol result+1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - word << 16 (zero)",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0xFFFF,
|
||||
amountIsVar: false,
|
||||
amountValue: 16,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindWord,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #0",
|
||||
"\tsta result",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "byte variable << byte variable",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: true,
|
||||
sourceVarName: "value",
|
||||
sourceVarKind: compiler.KindByte,
|
||||
amountIsVar: true,
|
||||
amountVarName: "shift",
|
||||
amountVarKind: compiler.KindByte,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda value",
|
||||
"\tsta result",
|
||||
"\tldx shift",
|
||||
"\tbeq _L2",
|
||||
"_L1",
|
||||
"\tasl result",
|
||||
"\tdex",
|
||||
"\tbne _L1",
|
||||
"_L2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "word variable << byte variable",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 3, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: true,
|
||||
sourceVarName: "value",
|
||||
sourceVarKind: compiler.KindWord,
|
||||
amountIsVar: true,
|
||||
amountVarName: "shift",
|
||||
amountVarKind: compiler.KindByte,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindWord,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda value",
|
||||
"\tsta result",
|
||||
"\tlda value+1",
|
||||
"\tsta result+1",
|
||||
"\tldx shift",
|
||||
"\tbeq _L2",
|
||||
"_L1",
|
||||
"\tasl result",
|
||||
"\trol result+1",
|
||||
"\tdex",
|
||||
"\tbne _L1",
|
||||
"_L2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same source and dest",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftLCommand {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftLCommand{
|
||||
sourceIsVar: true,
|
||||
sourceVarName: "value",
|
||||
sourceVarKind: compiler.KindByte,
|
||||
amountIsVar: true,
|
||||
amountVarName: "shift",
|
||||
amountVarKind: compiler.KindByte,
|
||||
destVarName: "value",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tldx shift",
|
||||
"\tbeq _L2",
|
||||
"_L1",
|
||||
"\tasl value",
|
||||
"\tdex",
|
||||
"\tbne _L1",
|
||||
"_L2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := compiler.NewCompilerContext(preproc.NewPragma())
|
||||
cmd := tt.setup(ctx)
|
||||
|
||||
got, err := cmd.Generate(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
|
||||
if len(got) != len(tt.wantLines) {
|
||||
t.Errorf("Generate() got %d lines, want %d lines\nGot:\n%s\nWant:\n%s",
|
||||
len(got), len(tt.wantLines),
|
||||
strings.Join(got, "\n"), strings.Join(tt.wantLines, "\n"))
|
||||
return
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
// Skip exact label comparison (they're generated dynamically)
|
||||
if strings.HasPrefix(got[i], "_L") && strings.HasPrefix(tt.wantLines[i], "_L") {
|
||||
continue
|
||||
}
|
||||
if got[i] != tt.wantLines[i] {
|
||||
t.Errorf("Line %d:\ngot: %q\nwant: %q", i, got[i], tt.wantLines[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
348
internal/commands/shiftr.go
Normal file
348
internal/commands/shiftr.go
Normal file
|
|
@ -0,0 +1,348 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"c65gm/internal/compiler"
|
||||
"c65gm/internal/preproc"
|
||||
"c65gm/internal/utils"
|
||||
)
|
||||
|
||||
// ShiftRCommand handles logical shift right operations
|
||||
// Syntax:
|
||||
//
|
||||
// SHIFTR <source> BY <amount> GIVING <dest> # old syntax with BY/GIVING
|
||||
// SHIFTR <source> >> <amount> -> <dest> # old syntax with >>/->
|
||||
// <dest> = <source> >> <amount> # new syntax
|
||||
type ShiftRCommand struct {
|
||||
sourceVarName string
|
||||
sourceVarKind compiler.VarKind
|
||||
sourceValue uint16
|
||||
sourceIsVar bool
|
||||
|
||||
amountVarName string
|
||||
amountVarKind compiler.VarKind
|
||||
amountValue uint16
|
||||
amountIsVar bool
|
||||
|
||||
destVarName string
|
||||
destVarKind compiler.VarKind
|
||||
|
||||
line preproc.Line // Store line info for warnings
|
||||
}
|
||||
|
||||
func (c *ShiftRCommand) WillHandle(line preproc.Line) bool {
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
if err != nil || len(params) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Old syntax: SHIFTR ... (must have exactly 6 params)
|
||||
if strings.ToUpper(params[0]) == "SHIFTR" && len(params) == 6 {
|
||||
return true
|
||||
}
|
||||
|
||||
// New syntax: <dest> = <source> >> <amount>
|
||||
if len(params) == 5 && params[1] == "=" && params[3] == ">>" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *ShiftRCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
||||
// Clear state
|
||||
c.sourceVarName = ""
|
||||
c.sourceIsVar = false
|
||||
c.sourceValue = 0
|
||||
c.amountVarName = ""
|
||||
c.amountIsVar = false
|
||||
c.amountValue = 0
|
||||
c.destVarName = ""
|
||||
c.line = line // Store line for warnings
|
||||
|
||||
params, err := utils.ParseParams(line.Text)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
paramCount := len(params)
|
||||
scope := ctx.CurrentScope()
|
||||
|
||||
// Create constant lookup function
|
||||
constLookup := ctx.SymbolTable.ConstantLookupFunc(scope)
|
||||
|
||||
// Determine syntax and parse accordingly
|
||||
if strings.ToUpper(params[0]) == "SHIFTR" {
|
||||
// Old syntax: SHIFTR <source> BY/>> <amount> GIVING/-> <dest>
|
||||
if paramCount != 6 {
|
||||
return fmt.Errorf("SHIFTR: wrong number of parameters (%d), expected 6", paramCount)
|
||||
}
|
||||
|
||||
separator1 := strings.ToUpper(params[2])
|
||||
if separator1 != "BY" && separator1 != ">>" {
|
||||
return fmt.Errorf("SHIFTR: parameter #3 must be 'BY' or '>>', got %q", params[2])
|
||||
}
|
||||
|
||||
separator2 := strings.ToUpper(params[4])
|
||||
if separator2 != "GIVING" && separator2 != "->" {
|
||||
return fmt.Errorf("SHIFTR: parameter #5 must be 'GIVING' or '->', got %q", params[4])
|
||||
}
|
||||
|
||||
// Parse destination
|
||||
destName := params[5]
|
||||
destSym := ctx.SymbolTable.Lookup(destName, scope)
|
||||
if destSym == nil {
|
||||
return fmt.Errorf("SHIFTR: unknown variable %q", destName)
|
||||
}
|
||||
if destSym.IsConst() {
|
||||
return fmt.Errorf("SHIFTR: cannot assign to constant %q", destName)
|
||||
}
|
||||
c.destVarName = destSym.FullName()
|
||||
c.destVarKind = destSym.GetVarKind()
|
||||
|
||||
// Parse source
|
||||
var err error
|
||||
c.sourceVarName, c.sourceVarKind, c.sourceValue, c.sourceIsVar, err = compiler.ParseOperandParam(
|
||||
params[1], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTR: source: %w", err)
|
||||
}
|
||||
|
||||
// Parse amount
|
||||
c.amountVarName, c.amountVarKind, c.amountValue, c.amountIsVar, err = compiler.ParseOperandParam(
|
||||
params[3], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTR: amount: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// New syntax: <dest> = <source> >> <amount>
|
||||
if paramCount != 5 {
|
||||
return fmt.Errorf("SHIFTR: wrong number of parameters (%d), expected 5", paramCount)
|
||||
}
|
||||
|
||||
if params[1] != "=" {
|
||||
return fmt.Errorf("SHIFTR: expected '=' at position 2, got %q", params[1])
|
||||
}
|
||||
|
||||
if params[3] != ">>" {
|
||||
return fmt.Errorf("SHIFTR: expected '>>' at position 4, got %q", params[3])
|
||||
}
|
||||
|
||||
// Parse destination
|
||||
destName := params[0]
|
||||
destSym := ctx.SymbolTable.Lookup(destName, scope)
|
||||
if destSym == nil {
|
||||
return fmt.Errorf("SHIFTR: unknown variable %q", destName)
|
||||
}
|
||||
if destSym.IsConst() {
|
||||
return fmt.Errorf("SHIFTR: cannot assign to constant %q", destName)
|
||||
}
|
||||
c.destVarName = destSym.FullName()
|
||||
c.destVarKind = destSym.GetVarKind()
|
||||
|
||||
// Parse source
|
||||
var err error
|
||||
c.sourceVarName, c.sourceVarKind, c.sourceValue, c.sourceIsVar, err = compiler.ParseOperandParam(
|
||||
params[2], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTR: source: %w", err)
|
||||
}
|
||||
|
||||
// Parse amount
|
||||
c.amountVarName, c.amountVarKind, c.amountValue, c.amountIsVar, err = compiler.ParseOperandParam(
|
||||
params[4], ctx.SymbolTable, scope, constLookup)
|
||||
if err != nil {
|
||||
return fmt.Errorf("SHIFTR: amount: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate amount
|
||||
if c.amountIsVar {
|
||||
if c.amountVarKind == compiler.KindWord {
|
||||
return fmt.Errorf("SHIFTR: amount must be BYTE variable, got WORD %q", c.amountVarName)
|
||||
}
|
||||
} else {
|
||||
if c.amountValue > 255 {
|
||||
return fmt.Errorf("SHIFTR: amount constant %d out of BYTE range (0-255)", c.amountValue)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ShiftRCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||
var asm []string
|
||||
|
||||
// Check if shift amount >= bit width (result will be zero)
|
||||
bitWidth := 8
|
||||
if c.destVarKind == compiler.KindWord {
|
||||
bitWidth = 16
|
||||
}
|
||||
|
||||
amountZero := false
|
||||
if !c.amountIsVar {
|
||||
// Constant amount
|
||||
if c.amountValue >= uint16(bitWidth) {
|
||||
amountZero = true
|
||||
}
|
||||
}
|
||||
|
||||
if amountZero {
|
||||
// Result is zero, just store zero
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// Step 1: Copy source to destination if needed
|
||||
if c.sourceIsVar && c.sourceVarName == c.destVarName {
|
||||
// Same variable, no copy needed
|
||||
} else {
|
||||
copyAsm := c.generateCopy()
|
||||
asm = append(asm, copyAsm...)
|
||||
}
|
||||
|
||||
// Step 2: Apply shift
|
||||
shiftAsm, err := c.generateShift(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
asm = append(asm, shiftAsm...)
|
||||
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// generateCopy generates assembly to copy source to destination
|
||||
func (c *ShiftRCommand) generateCopy() []string {
|
||||
var asm []string
|
||||
|
||||
// If source is literal, just load it
|
||||
if !c.sourceIsVar {
|
||||
lo := uint8(c.sourceValue & 0xFF)
|
||||
hi := uint8((c.sourceValue >> 8) & 0xFF)
|
||||
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", lo))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
if lo != hi {
|
||||
asm = append(asm, fmt.Sprintf("\tlda #$%02x", hi))
|
||||
}
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
return asm
|
||||
}
|
||||
|
||||
// Source is variable
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
// Destination is byte
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
// Destination is word
|
||||
if c.sourceVarKind == compiler.KindByte {
|
||||
// Byte -> Word (zero-extend)
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
} else {
|
||||
// Word -> Word
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tlda %s+1", c.sourceVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
}
|
||||
|
||||
return asm
|
||||
}
|
||||
|
||||
// generateShift generates assembly to shift destination right by amount
|
||||
func (c *ShiftRCommand) generateShift(ctx *compiler.CompilerContext) ([]string, error) {
|
||||
var asm []string
|
||||
|
||||
// Constant amount
|
||||
if !c.amountIsVar {
|
||||
amount := c.amountValue
|
||||
if amount == 0 {
|
||||
return asm, nil // No shift needed
|
||||
}
|
||||
|
||||
// Determine bit width
|
||||
bitWidth := 8
|
||||
if c.destVarKind == compiler.KindWord {
|
||||
bitWidth = 16
|
||||
}
|
||||
|
||||
// Warn if shift amount >= bit width (but not for 0)
|
||||
if amount >= uint16(bitWidth) {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s:%d: warning: shift amount %d >= %d bits, value will be zero\n",
|
||||
c.line.Filename, c.line.LineNo, amount, bitWidth)
|
||||
}
|
||||
|
||||
if amount >= uint16(bitWidth) {
|
||||
// Shift all bits out -> zero
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, "\tlda #0")
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tsta %s+1", c.destVarName))
|
||||
}
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// Unroll shift loop
|
||||
for i := uint16(0); i < amount; i++ {
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, fmt.Sprintf("\tlsr %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, fmt.Sprintf("\tlsr %s+1", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tror %s", c.destVarName))
|
||||
}
|
||||
}
|
||||
return asm, nil
|
||||
}
|
||||
|
||||
// Variable amount
|
||||
// Generate labels
|
||||
loopLabel := ctx.GeneralStack.Push()
|
||||
ctx.GeneralStack.Pop()
|
||||
doneLabel := ctx.GeneralStack.Push()
|
||||
ctx.GeneralStack.Pop()
|
||||
|
||||
|
||||
// Load amount into X
|
||||
asm = append(asm, fmt.Sprintf("\tldx %s", c.amountVarName))
|
||||
|
||||
// Check for zero amount
|
||||
asm = append(asm, fmt.Sprintf("\tbeq %s", doneLabel))
|
||||
|
||||
// Shift loop
|
||||
asm = append(asm, loopLabel)
|
||||
if c.destVarKind == compiler.KindByte {
|
||||
asm = append(asm, fmt.Sprintf("\tlsr %s", c.destVarName))
|
||||
} else {
|
||||
asm = append(asm, fmt.Sprintf("\tlsr %s+1", c.destVarName))
|
||||
asm = append(asm, fmt.Sprintf("\tror %s", c.destVarName))
|
||||
}
|
||||
asm = append(asm, "\tdex")
|
||||
asm = append(asm, fmt.Sprintf("\tbne %s", loopLabel))
|
||||
|
||||
asm = append(asm, doneLabel)
|
||||
return asm, nil
|
||||
}
|
||||
551
internal/commands/shiftr_test.go
Normal file
551
internal/commands/shiftr_test.go
Normal file
|
|
@ -0,0 +1,551 @@
|
|||
package commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"c65gm/internal/compiler"
|
||||
"c65gm/internal/preproc"
|
||||
)
|
||||
|
||||
func TestShiftRCommand_WillHandle(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
text string
|
||||
want bool
|
||||
}{
|
||||
// Old syntax
|
||||
{"old syntax BY/GIVING", "SHIFTR a BY b GIVING c", true},
|
||||
{"old syntax >>/->", "SHIFTR a >> b -> c", true},
|
||||
{"old syntax mixed case", "shiftr x by y giving z", true},
|
||||
|
||||
// New syntax
|
||||
{"new syntax basic", "result = a >> b", true},
|
||||
{"new syntax with literals", "x = 10 >> 3", true},
|
||||
|
||||
// Should not handle
|
||||
{"not shiftr - shiftl", "SHIFTL a BY b GIVING c", false},
|
||||
{"not shiftr - add", "result = a + b", false},
|
||||
{"not shiftr - wrong params", "SHIFTR a b c", false},
|
||||
{"empty", "", false},
|
||||
{"just SHIFTR", "SHIFTR", false},
|
||||
{"assignment without shift", "x = y", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cmd := &ShiftRCommand{}
|
||||
line := preproc.Line{Text: tt.text}
|
||||
if got := cmd.WillHandle(line); got != tt.want {
|
||||
t.Errorf("WillHandle() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftRCommand_Interpret_OldSyntax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*compiler.CompilerContext)
|
||||
text string
|
||||
wantErr bool
|
||||
check func(*testing.T, *ShiftRCommand)
|
||||
}{
|
||||
{
|
||||
name: "byte >> byte -> byte",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 3, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a BY b GIVING c",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if !cmd.sourceIsVar || cmd.sourceVarName != "a" {
|
||||
t.Errorf("source should be var 'a'")
|
||||
}
|
||||
if !cmd.amountIsVar || cmd.amountVarName != "b" {
|
||||
t.Errorf("amount should be var 'b'")
|
||||
}
|
||||
if cmd.destVarName != "c" || cmd.destVarKind != compiler.KindByte {
|
||||
t.Errorf("dest should be byte 'c'")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "word >> byte -> word",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("x", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("n", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR x BY n GIVING result",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if cmd.sourceVarKind != compiler.KindWord {
|
||||
t.Errorf("source should be word")
|
||||
}
|
||||
if cmd.destVarKind != compiler.KindWord {
|
||||
t.Errorf("dest should be word")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "literal >> var -> byte",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR $FF BY shift GIVING result",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if cmd.sourceIsVar {
|
||||
t.Errorf("source should be literal")
|
||||
}
|
||||
if cmd.sourceValue != 0xFF {
|
||||
t.Errorf("source value = %d, want 255", cmd.sourceValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "var >> literal -> byte",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 8, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR value BY 3 GIVING result",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if cmd.amountIsVar {
|
||||
t.Errorf("amount should be literal")
|
||||
}
|
||||
if cmd.amountValue != 3 {
|
||||
t.Errorf("amount value = %d, want 3", cmd.amountValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "arrow syntax",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a >> b -> c",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if cmd.destVarName != "c" {
|
||||
t.Errorf("dest should be c")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown variable",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a BY b GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "assign to constant",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddConst("MAX", "", compiler.KindByte, 255, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a BY b GIVING MAX",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "word amount variable (error)",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindWord, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a BY b GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "amount constant > 255 (error)",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a BY 300 GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong separator #3",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a WITH b GIVING c",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong separator #5",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("c", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "SHIFTR a BY b INTO c",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := compiler.NewCompilerContext(preproc.NewPragma())
|
||||
if tt.setup != nil {
|
||||
tt.setup(ctx)
|
||||
}
|
||||
|
||||
cmd := &ShiftRCommand{}
|
||||
line := preproc.Line{Text: tt.text}
|
||||
|
||||
err := cmd.Interpret(line, ctx)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Interpret() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr && tt.check != nil {
|
||||
tt.check(t, cmd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftRCommand_Interpret_NewSyntax(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*compiler.CompilerContext)
|
||||
text string
|
||||
wantErr bool
|
||||
check func(*testing.T, *ShiftRCommand)
|
||||
}{
|
||||
{
|
||||
name: "dest = var >> var",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = a >> b",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if cmd.destVarName != "result" {
|
||||
t.Errorf("dest = %q, want 'result'", cmd.destVarName)
|
||||
}
|
||||
if !cmd.sourceIsVar || cmd.sourceVarName != "a" {
|
||||
t.Errorf("source should be var 'a'")
|
||||
}
|
||||
if !cmd.amountIsVar || cmd.amountVarName != "b" {
|
||||
t.Errorf("amount should be var 'b'")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dest = literal >> literal",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = $08 >> 3",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if cmd.sourceIsVar || cmd.amountIsVar {
|
||||
t.Errorf("both params should be literals")
|
||||
}
|
||||
if cmd.sourceValue != 8 || cmd.amountValue != 3 {
|
||||
t.Errorf("source=%d, amount=%d, want 8,3", cmd.sourceValue, cmd.amountValue)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "word destination",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 1, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = value >> shift",
|
||||
wantErr: false,
|
||||
check: func(t *testing.T, cmd *ShiftRCommand) {
|
||||
if cmd.destVarKind != compiler.KindWord {
|
||||
t.Errorf("dest should be word")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unknown dest variable",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = a >> b",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "wrong operator (not >>)",
|
||||
setup: func(ctx *compiler.CompilerContext) {
|
||||
ctx.SymbolTable.AddVar("a", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("b", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
},
|
||||
text: "result = a << b",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := compiler.NewCompilerContext(preproc.NewPragma())
|
||||
if tt.setup != nil {
|
||||
tt.setup(ctx)
|
||||
}
|
||||
|
||||
cmd := &ShiftRCommand{}
|
||||
line := preproc.Line{Text: tt.text}
|
||||
|
||||
err := cmd.Interpret(line, ctx)
|
||||
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Interpret() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.wantErr && tt.check != nil {
|
||||
tt.check(t, cmd)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShiftRCommand_Generate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(*compiler.CompilerContext) *ShiftRCommand
|
||||
wantLines []string
|
||||
}{
|
||||
{
|
||||
name: "constant folding - byte >> 0",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0x55,
|
||||
amountIsVar: false,
|
||||
amountValue: 0,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #$55",
|
||||
"\tsta result",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - byte >> 3",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0x08,
|
||||
amountIsVar: false,
|
||||
amountValue: 3,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #$08",
|
||||
"\tsta result",
|
||||
"\tlsr result",
|
||||
"\tlsr result",
|
||||
"\tlsr result",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - byte >> 8 (zero)",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0xFF,
|
||||
amountIsVar: false,
|
||||
amountValue: 8,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #0",
|
||||
"\tsta result",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - word >> 1",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0x1234,
|
||||
amountIsVar: false,
|
||||
amountValue: 1,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindWord,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #$34",
|
||||
"\tsta result",
|
||||
"\tlda #$12",
|
||||
"\tsta result+1",
|
||||
"\tlsr result+1",
|
||||
"\tror result",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "constant folding - word >> 16 (zero)",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: false,
|
||||
sourceValue: 0xFFFF,
|
||||
amountIsVar: false,
|
||||
amountValue: 16,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindWord,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda #0",
|
||||
"\tsta result",
|
||||
"\tsta result+1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "byte variable >> byte variable",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindByte, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: true,
|
||||
sourceVarName: "value",
|
||||
sourceVarKind: compiler.KindByte,
|
||||
amountIsVar: true,
|
||||
amountVarName: "shift",
|
||||
amountVarKind: compiler.KindByte,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda value",
|
||||
"\tsta result",
|
||||
"\tldx shift",
|
||||
"\tbeq _L2",
|
||||
"_L1",
|
||||
"\tlsr result",
|
||||
"\tdex",
|
||||
"\tbne _L1",
|
||||
"_L2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "word variable >> byte variable",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindWord, 1000, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 3, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("result", "", compiler.KindWord, 0, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: true,
|
||||
sourceVarName: "value",
|
||||
sourceVarKind: compiler.KindWord,
|
||||
amountIsVar: true,
|
||||
amountVarName: "shift",
|
||||
amountVarKind: compiler.KindByte,
|
||||
destVarName: "result",
|
||||
destVarKind: compiler.KindWord,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tlda value",
|
||||
"\tsta result",
|
||||
"\tlda value+1",
|
||||
"\tsta result+1",
|
||||
"\tldx shift",
|
||||
"\tbeq _L2",
|
||||
"_L1",
|
||||
"\tlsr result+1",
|
||||
"\tror result",
|
||||
"\tdex",
|
||||
"\tbne _L1",
|
||||
"_L2",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "same source and dest",
|
||||
setup: func(ctx *compiler.CompilerContext) *ShiftRCommand {
|
||||
ctx.SymbolTable.AddVar("value", "", compiler.KindByte, 10, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
ctx.SymbolTable.AddVar("shift", "", compiler.KindByte, 2, preproc.Line{Filename: "test.c65", LineNo: 1})
|
||||
return &ShiftRCommand{
|
||||
sourceIsVar: true,
|
||||
sourceVarName: "value",
|
||||
sourceVarKind: compiler.KindByte,
|
||||
amountIsVar: true,
|
||||
amountVarName: "shift",
|
||||
amountVarKind: compiler.KindByte,
|
||||
destVarName: "value",
|
||||
destVarKind: compiler.KindByte,
|
||||
}
|
||||
},
|
||||
wantLines: []string{
|
||||
"\tldx shift",
|
||||
"\tbeq _L2",
|
||||
"_L1",
|
||||
"\tlsr value",
|
||||
"\tdex",
|
||||
"\tbne _L1",
|
||||
"_L2",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ctx := compiler.NewCompilerContext(preproc.NewPragma())
|
||||
cmd := tt.setup(ctx)
|
||||
|
||||
got, err := cmd.Generate(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
|
||||
if len(got) != len(tt.wantLines) {
|
||||
t.Errorf("Generate() got %d lines, want %d lines\nGot:\n%s\nWant:\n%s",
|
||||
len(got), len(tt.wantLines),
|
||||
strings.Join(got, "\n"), strings.Join(tt.wantLines, "\n"))
|
||||
return
|
||||
}
|
||||
|
||||
for i := range got {
|
||||
// Skip exact label comparison (they're generated dynamically)
|
||||
if strings.HasPrefix(got[i], "_L") && strings.HasPrefix(tt.wantLines[i], "_L") {
|
||||
continue
|
||||
}
|
||||
if got[i] != tt.wantLines[i] {
|
||||
t.Errorf("Line %d:\ngot: %q\nwant: %q", i, got[i], tt.wantLines[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
41
language.md
41
language.md
|
|
@ -156,6 +156,8 @@ result = 100 - 5 // Subtraction
|
|||
mask = value & $FF // Bitwise AND
|
||||
flags = flags | $01 // Bitwise OR
|
||||
toggle = value ^ $FF // Bitwise XOR
|
||||
result = value << 3 // Shift left
|
||||
mask = flags >> 2 // Shift right
|
||||
```
|
||||
|
||||
### Increment and Decrement
|
||||
|
|
@ -280,7 +282,7 @@ FUNC initialize
|
|||
screen = temp
|
||||
FEND
|
||||
|
||||
FUNC setColor(color, brightness)
|
||||
FUNC setColor({BYTE color} {BYTE brightness})
|
||||
borderColor = color
|
||||
backgroundColor = brightness
|
||||
FEND
|
||||
|
|
@ -305,12 +307,13 @@ Parameters can have different access modes:
|
|||
- `io:` - Read-write
|
||||
|
||||
```c65
|
||||
FUNC add(in:a, in:b, out:result)
|
||||
FUNC add(in:{BYTE a} in:{BYTE b} out:{BYTE result})
|
||||
result = a + b
|
||||
FEND
|
||||
|
||||
FUNC swap(io:x, io:y)
|
||||
BYTE temp = x
|
||||
FUNC swap(io:{BYTE x} io:{BYTE y})
|
||||
BYTE temp
|
||||
temp = x
|
||||
x = y
|
||||
y = temp
|
||||
FEND
|
||||
|
|
@ -321,27 +324,19 @@ FEND
|
|||
Variables declared inside functions are local:
|
||||
|
||||
```c65
|
||||
FUNC calculate(value)
|
||||
FUNC calculate({BYTE value})
|
||||
BYTE local = 10 // Only exists inside this function
|
||||
WORD temp // Local temporary
|
||||
temp = value + local
|
||||
FEND
|
||||
```
|
||||
|
||||
### Curly Brace Syntax
|
||||
|
||||
Alternative parameter syntax using curly braces:
|
||||
|
||||
```c65
|
||||
FUNC process({BYTE value} out:{BYTE result})
|
||||
result = value + 1
|
||||
FEND
|
||||
```
|
||||
|
||||
### Returning from Functions
|
||||
|
||||
```c65
|
||||
FUNC checkValue(value)
|
||||
FUNC checkValue({BYTE value})
|
||||
IF value == 0
|
||||
EXIT // Return early
|
||||
ENDIF
|
||||
|
|
@ -445,12 +440,12 @@ ENDASM
|
|||
Local variables (inside FUNC) need pipe delimiters `|varname|`:
|
||||
|
||||
```c65
|
||||
FUNC calculate(value)
|
||||
FUNC calculate({BYTE value})
|
||||
BYTE local = 10
|
||||
ASM
|
||||
lda |local| // Reference local variable
|
||||
clc
|
||||
adc value // Global parameter
|
||||
adc value // Function parameter
|
||||
sta |local|
|
||||
ENDASM
|
||||
FEND
|
||||
|
|
@ -719,7 +714,7 @@ FUNC lib_mylib_initialize
|
|||
lib_mylib_buffer = $C000
|
||||
FEND
|
||||
|
||||
FUNC lib_mylib_process(value)
|
||||
FUNC lib_mylib_process({BYTE value})
|
||||
// Do something
|
||||
FEND
|
||||
|
||||
|
|
@ -804,7 +799,7 @@ FEND
|
|||
|
||||
```c65
|
||||
// Print null-terminated string
|
||||
FUNC printString(textPtr)
|
||||
FUNC printString({WORD textPtr})
|
||||
BYTE char
|
||||
char = PEEK textPtr[0]
|
||||
|
||||
|
|
@ -827,7 +822,7 @@ BYTE spriteX @ VIC2+0
|
|||
BYTE spriteY @ VIC2+1
|
||||
BYTE spriteEnable @ VIC2+21
|
||||
|
||||
FUNC enableSprite(spriteNum)
|
||||
FUNC enableSprite({BYTE spriteNum})
|
||||
BYTE mask
|
||||
mask = 1
|
||||
|
||||
|
|
@ -846,7 +841,7 @@ For frequently accessed pointers, use zero page:
|
|||
```c65
|
||||
WORD fastPtr @ $FB // Zero page = fast indexed access
|
||||
|
||||
FUNC processBuffer(buffer, size)
|
||||
FUNC processBuffer({WORD buffer} {BYTE size})
|
||||
POINTER fastPtr TO buffer
|
||||
|
||||
WHILE size > 0
|
||||
|
|
@ -920,7 +915,7 @@ ENDSCRIPT
|
|||
### Delay Loops
|
||||
|
||||
```c65
|
||||
FUNC delay(frames)
|
||||
FUNC delay({BYTE frames})
|
||||
BYTE raster @ $D012
|
||||
BYTE oldRaster
|
||||
|
||||
|
|
@ -1007,7 +1002,7 @@ Always use include guards in library files:
|
|||
|
||||
```c65
|
||||
// Explain what and why, not how
|
||||
FUNC calculateTrajectory(velocity, angle)
|
||||
FUNC calculateTrajectory({WORD velocity} {BYTE angle})
|
||||
// Use fixed-point math (8.8 format)
|
||||
// because we don't have floating point
|
||||
WORD xVel
|
||||
|
|
@ -1061,7 +1056,7 @@ FOR i = 0 TO 10
|
|||
NEXT
|
||||
|
||||
// Functions
|
||||
FUNC myFunc(in:param1, out:result)
|
||||
FUNC myFunc(in:{BYTE param1} out:{BYTE result})
|
||||
result = param1 + 1
|
||||
FEND
|
||||
|
||||
|
|
|
|||
2
main.go
2
main.go
|
|
@ -112,6 +112,8 @@ func registerCommands(comp *compiler.Compiler) {
|
|||
comp.Registry().Register(&commands.DefaultCommand{})
|
||||
comp.Registry().Register(&commands.EndSwitchCommand{})
|
||||
comp.Registry().Register(&commands.MacroCommand{})
|
||||
comp.Registry().Register(&commands.ShiftLCommand{})
|
||||
comp.Registry().Register(&commands.ShiftRCommand{})
|
||||
}
|
||||
|
||||
func writeOutput(filename string, lines []string) error {
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ ASM
|
|||
sta temp
|
||||
ENDASM
|
||||
|
||||
FUNC calculate(value)
|
||||
FUNC calculate({BYTE value})
|
||||
BYTE local = 10
|
||||
ASM
|
||||
lda |local| ; Reference local variable
|
||||
|
|
|
|||
Loading…
Reference in a new issue