From 0357df98739db7a23038bf34236c0f6e336b4ee6 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Wed, 15 Apr 2026 01:29:38 +0200 Subject: [PATCH] Added right and left shift operators --- commands.md | 113 +++++- examples/shift_demo/cm.sh | 20 + examples/shift_demo/shift_demo.c65 | 410 ++++++++++++++++++++ examples/shift_demo/start_in_vice.sh | 1 + internal/commands/shiftl.go | 348 +++++++++++++++++ internal/commands/shiftl_test.go | 551 +++++++++++++++++++++++++++ internal/commands/shiftr.go | 348 +++++++++++++++++ internal/commands/shiftr_test.go | 551 +++++++++++++++++++++++++++ language.md | 41 +- main.go | 2 + syntax.md | 2 +- 11 files changed, 2350 insertions(+), 37 deletions(-) create mode 100755 examples/shift_demo/cm.sh create mode 100644 examples/shift_demo/shift_demo.c65 create mode 100644 examples/shift_demo/start_in_vice.sh create mode 100644 internal/commands/shiftl.go create mode 100644 internal/commands/shiftl_test.go create mode 100644 internal/commands/shiftr.go create mode 100644 internal/commands/shiftr_test.go diff --git a/commands.md b/commands.md index 8031ff3..79adb18 100644 --- a/commands.md +++ b/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 -FUNC ([,,...]) +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 BY GIVING +SHIFTL << -> + = << +``` + +**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 BY GIVING +SHIFTR >> -> + = >> +``` + +**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. diff --git a/examples/shift_demo/cm.sh b/examples/shift_demo/cm.sh new file mode 100755 index 0000000..00e7025 --- /dev/null +++ b/examples/shift_demo/cm.sh @@ -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 diff --git a/examples/shift_demo/shift_demo.c65 b/examples/shift_demo/shift_demo.c65 new file mode 100644 index 0000000..977b005 --- /dev/null +++ b/examples/shift_demo/shift_demo.c65 @@ -0,0 +1,410 @@ +//----------------------------------------------------------- +// Shift Operations Demo +// Practical examples of << and >> operators +// Shows real-world uses for bit manipulation +//----------------------------------------------------------- + +#INCLUDE +#INCLUDE +#INCLUDE + +#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 \ No newline at end of file diff --git a/examples/shift_demo/start_in_vice.sh b/examples/shift_demo/start_in_vice.sh new file mode 100644 index 0000000..1e9473f --- /dev/null +++ b/examples/shift_demo/start_in_vice.sh @@ -0,0 +1 @@ +x64 -autostartprgmode 1 main.prg \ No newline at end of file diff --git a/internal/commands/shiftl.go b/internal/commands/shiftl.go new file mode 100644 index 0000000..b1b5004 --- /dev/null +++ b/internal/commands/shiftl.go @@ -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 BY GIVING # old syntax with BY/GIVING +// SHIFTL << -> # old syntax with < +// = << # 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: = << + 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 BY/<< GIVING/-> + 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: = << + 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 +} \ No newline at end of file diff --git a/internal/commands/shiftl_test.go b/internal/commands/shiftl_test.go new file mode 100644 index 0000000..492bbc3 --- /dev/null +++ b/internal/commands/shiftl_test.go @@ -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]) + } + } + }) + } +} \ No newline at end of file diff --git a/internal/commands/shiftr.go b/internal/commands/shiftr.go new file mode 100644 index 0000000..ffa0e1b --- /dev/null +++ b/internal/commands/shiftr.go @@ -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 BY GIVING # old syntax with BY/GIVING +// SHIFTR >> -> # old syntax with >>/-> +// = >> # 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: = >> + 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 BY/>> GIVING/-> + 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: = >> + 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 +} \ No newline at end of file diff --git a/internal/commands/shiftr_test.go b/internal/commands/shiftr_test.go new file mode 100644 index 0000000..8d9ddb7 --- /dev/null +++ b/internal/commands/shiftr_test.go @@ -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]) + } + } + }) + } +} \ No newline at end of file diff --git a/language.md b/language.md index 2f6075b..c74878d 100644 --- a/language.md +++ b/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 diff --git a/main.go b/main.go index bbde085..0d0f96f 100644 --- a/main.go +++ b/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 { diff --git a/syntax.md b/syntax.md index d94d89d..3b2bd52 100644 --- a/syntax.md +++ b/syntax.md @@ -303,7 +303,7 @@ ASM sta temp ENDASM -FUNC calculate(value) +FUNC calculate({BYTE value}) BYTE local = 10 ASM lda |local| ; Reference local variable