Added right and left shift operators

This commit is contained in:
Mattias Hansson 2026-04-15 01:29:38 +02:00
parent bc6fdaf8f2
commit 0357df9873
11 changed files with 2350 additions and 37 deletions

View file

@ -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
View 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

View 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

View file

@ -0,0 +1 @@
x64 -autostartprgmode 1 main.prg

348
internal/commands/shiftl.go Normal file
View 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
}

View 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
View 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
}

View 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])
}
}
})
}
}

View file

@ -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

View file

@ -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 {

View file

@ -303,7 +303,7 @@ ASM
sta temp
ENDASM
FUNC calculate(value)
FUNC calculate({BYTE value})
BYTE local = 10
ASM
lda |local| ; Reference local variable