Added SWITCH/CASE/DEFAULT/ENDSWITCH
This commit is contained in:
parent
4319373828
commit
e33460d84d
13 changed files with 1647 additions and 5 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -30,3 +30,6 @@ c65gm
|
||||||
|
|
||||||
.claude/
|
.claude/
|
||||||
.npm/
|
.npm/
|
||||||
|
*.sym
|
||||||
|
*.prg
|
||||||
|
*.s
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
FROM node:18-alpine
|
FROM node:18-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache bash
|
||||||
|
ENV SHELL=/bin/bash
|
||||||
RUN npm install -g @anthropic-ai/claude-code
|
RUN npm install -g @anthropic-ai/claude-code
|
||||||
CMD ["claude"]
|
CMD ["claude"]
|
||||||
|
|
|
||||||
20
examples/switch_demo/cm.sh
Executable file
20
examples/switch_demo/cm.sh
Executable file
|
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Define filename as variable
|
||||||
|
PROGNAME="switch_demo"
|
||||||
|
# Only set C65LIBPATH if not already defined
|
||||||
|
if [ -z "$C65LIBPATH" ]; then
|
||||||
|
export C65LIBPATH=$(readlink -f "../../lib")
|
||||||
|
fi
|
||||||
|
# Compile
|
||||||
|
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
|
||||||
1
examples/switch_demo/start_in_vice.sh
Normal file
1
examples/switch_demo/start_in_vice.sh
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
x64 -autostartprgmode 1 main.prg
|
||||||
256
examples/switch_demo/switch_demo.c65
Normal file
256
examples/switch_demo/switch_demo.c65
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// SWITCH/CASE Statement Demo
|
||||||
|
// Demonstrates the SWITCH/CASE control flow statement
|
||||||
|
// with implicit breaks and long jump pragma support
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
|
||||||
|
#INCLUDE <c64start.c65>
|
||||||
|
#INCLUDE <c64defs.c65>
|
||||||
|
#INCLUDE <cbmiolib.c65>
|
||||||
|
|
||||||
|
#PRAGMA _P_USE_CBM_STRINGS 1
|
||||||
|
|
||||||
|
GOTO start
|
||||||
|
|
||||||
|
WORD result
|
||||||
|
BYTE test_var
|
||||||
|
|
||||||
|
BYTE CONST TEST_VAL1 = 10
|
||||||
|
BYTE CONST TEST_VAL2 = 20
|
||||||
|
BYTE CONST TEST_VAL3 = 30
|
||||||
|
BYTE CONST OFFSET = 5
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 1: Basic SWITCH with DEFAULT
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_basic_switch
|
||||||
|
LET test_var = 2
|
||||||
|
|
||||||
|
SWITCH test_var
|
||||||
|
CASE 1
|
||||||
|
LET result = $0A
|
||||||
|
CASE 2
|
||||||
|
LET result = $14
|
||||||
|
CASE 3
|
||||||
|
LET result = $1E
|
||||||
|
DEFAULT
|
||||||
|
LET result = $63
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
lib_cbmio_print("1.basic: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 0014)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 2: SWITCH without DEFAULT
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_no_default
|
||||||
|
LET test_var = 5
|
||||||
|
LET result = 0
|
||||||
|
|
||||||
|
SWITCH test_var
|
||||||
|
CASE 1
|
||||||
|
LET result = $64
|
||||||
|
CASE 5
|
||||||
|
LET result = $01F4
|
||||||
|
CASE 10
|
||||||
|
LET result = $03E8
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
lib_cbmio_print("2.no default: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 01f4)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 3: Nested SWITCH statements
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_nested_switch
|
||||||
|
BYTE outer
|
||||||
|
BYTE inner
|
||||||
|
|
||||||
|
LET outer = 2
|
||||||
|
LET inner = 3
|
||||||
|
LET result = 0
|
||||||
|
|
||||||
|
SWITCH outer
|
||||||
|
CASE 1
|
||||||
|
LET result = $01
|
||||||
|
CASE 2
|
||||||
|
SWITCH inner
|
||||||
|
CASE 2
|
||||||
|
LET result = $16
|
||||||
|
CASE 3
|
||||||
|
LET result = $17
|
||||||
|
DEFAULT
|
||||||
|
LET result = $14
|
||||||
|
ENDSWITCH
|
||||||
|
CASE 3
|
||||||
|
LET result = $03
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
lib_cbmio_print("3.nested: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 0017)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 4: SWITCH with long jump pragma
|
||||||
|
// (for cases where branches are far apart)
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_long_jump
|
||||||
|
#PRAGMA _P_USE_LONG_JUMP 1
|
||||||
|
|
||||||
|
LET test_var = 3
|
||||||
|
|
||||||
|
SWITCH test_var
|
||||||
|
CASE 1
|
||||||
|
LET result = $0B
|
||||||
|
CASE 2
|
||||||
|
LET result = $16
|
||||||
|
CASE 3
|
||||||
|
LET result = $21
|
||||||
|
CASE 4
|
||||||
|
LET result = $2C
|
||||||
|
DEFAULT
|
||||||
|
LET result = $00
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
#PRAGMA _P_USE_LONG_JUMP 0
|
||||||
|
|
||||||
|
lib_cbmio_print("4.long jump: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 0021)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 5: SWITCH with WORD values
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_word_switch
|
||||||
|
WORD big_value
|
||||||
|
|
||||||
|
LET big_value = 1000
|
||||||
|
LET result = 0
|
||||||
|
|
||||||
|
SWITCH big_value
|
||||||
|
CASE 100
|
||||||
|
LET result = $0001
|
||||||
|
CASE 1000
|
||||||
|
LET result = $0002
|
||||||
|
CASE 10000
|
||||||
|
LET result = $0003
|
||||||
|
DEFAULT
|
||||||
|
LET result = $0063
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
lib_cbmio_print("5.word: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 0002)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 6: SWITCH with constants and compile-time evaluation
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_constants
|
||||||
|
LET test_var = 25
|
||||||
|
LET result = 0
|
||||||
|
|
||||||
|
SWITCH test_var
|
||||||
|
CASE TEST_VAL1
|
||||||
|
LET result = $01
|
||||||
|
CASE TEST_VAL2
|
||||||
|
LET result = $02
|
||||||
|
CASE TEST_VAL2+OFFSET
|
||||||
|
LET result = $03
|
||||||
|
CASE TEST_VAL3
|
||||||
|
LET result = $04
|
||||||
|
DEFAULT
|
||||||
|
LET result = $63
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
lib_cbmio_print("6.constants: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 0003)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 7: SWITCH with variable cases (not just literals)
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_variables
|
||||||
|
BYTE match_val1
|
||||||
|
BYTE match_val2
|
||||||
|
WORD match_val3
|
||||||
|
|
||||||
|
LET match_val1 = 15
|
||||||
|
LET match_val2 = 42
|
||||||
|
LET match_val3 = 1000
|
||||||
|
|
||||||
|
LET test_var = 42
|
||||||
|
LET result = 0
|
||||||
|
|
||||||
|
SWITCH test_var
|
||||||
|
CASE match_val1
|
||||||
|
LET result = $01
|
||||||
|
CASE match_val2
|
||||||
|
LET result = $02
|
||||||
|
CASE match_val3
|
||||||
|
LET result = $03
|
||||||
|
DEFAULT
|
||||||
|
LET result = $63
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
lib_cbmio_print("7.variables: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 0002)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Test 8: SWITCH that actually executes DEFAULT
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC test_default_execution
|
||||||
|
LET test_var = 99
|
||||||
|
LET result = 0
|
||||||
|
|
||||||
|
SWITCH test_var
|
||||||
|
CASE 1
|
||||||
|
LET result = $0A
|
||||||
|
CASE 2
|
||||||
|
LET result = $14
|
||||||
|
CASE 3
|
||||||
|
LET result = $1E
|
||||||
|
DEFAULT
|
||||||
|
LET result = $03E7
|
||||||
|
ENDSWITCH
|
||||||
|
|
||||||
|
lib_cbmio_print("8.default exec: ")
|
||||||
|
lib_cbmio_hexoutw(result)
|
||||||
|
lib_cbmio_printlf(" (exp 03e7)")
|
||||||
|
FEND
|
||||||
|
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
// Main program
|
||||||
|
//-----------------------------------------------------------
|
||||||
|
FUNC main
|
||||||
|
lib_cbmio_cls()
|
||||||
|
|
||||||
|
lib_cbmio_printlf("switch/case demo")
|
||||||
|
lib_cbmio_lf()
|
||||||
|
|
||||||
|
test_basic_switch()
|
||||||
|
test_no_default()
|
||||||
|
test_nested_switch()
|
||||||
|
test_long_jump()
|
||||||
|
test_word_switch()
|
||||||
|
test_constants()
|
||||||
|
test_variables()
|
||||||
|
test_default_execution()
|
||||||
|
|
||||||
|
lib_cbmio_lf()
|
||||||
|
lib_cbmio_printlf("all tests complete!")
|
||||||
|
|
||||||
|
FEND
|
||||||
|
|
||||||
|
LABEL start
|
||||||
|
main()
|
||||||
|
SUBEND
|
||||||
156
internal/commands/case.go
Normal file
156
internal/commands/case.go
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"c65gm/internal/compiler"
|
||||||
|
"c65gm/internal/preproc"
|
||||||
|
"c65gm/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CaseCommand handles CASE statements within SWITCH
|
||||||
|
// Syntax: CASE <value>
|
||||||
|
type CaseCommand struct {
|
||||||
|
caseValue *operandInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaseCommand) WillHandle(line preproc.Line) bool {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil || len(params) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.ToUpper(params[0]) == "CASE"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaseCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) != 2 {
|
||||||
|
return fmt.Errorf("CASE: expected 2 parameters (CASE <value>), got %d", len(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're inside a SWITCH
|
||||||
|
if ctx.SwitchStack.IsEmpty() {
|
||||||
|
return fmt.Errorf("CASE: not inside a SWITCH statement")
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := ctx.CurrentScope()
|
||||||
|
constLookup := func(name string) (int64, bool) {
|
||||||
|
sym := ctx.SymbolTable.Lookup(name, scope)
|
||||||
|
if sym != nil && sym.IsConst() {
|
||||||
|
return int64(sym.Value), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the case value
|
||||||
|
varName, varKind, value, isVar, err := compiler.ParseOperandParam(
|
||||||
|
params[1], ctx.SymbolTable, scope, constLookup)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CASE: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.caseValue = &operandInfo{
|
||||||
|
varName: varName,
|
||||||
|
varKind: varKind,
|
||||||
|
value: value,
|
||||||
|
isVar: isVar,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get switch info to validate type compatibility
|
||||||
|
switchInfo, err := ctx.SwitchStack.Peek()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CASE: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error if case value is out of range for switch variable type
|
||||||
|
if !isVar && !switchInfo.SwitchOperand.IsVar {
|
||||||
|
// Both are constants/literals
|
||||||
|
if switchInfo.SwitchOperand.VarKind == compiler.KindByte && value > 255 {
|
||||||
|
return fmt.Errorf("CASE: constant value %d exceeds BYTE range (0-255)", value)
|
||||||
|
}
|
||||||
|
if switchInfo.SwitchOperand.VarKind == compiler.KindByte && value < 0 {
|
||||||
|
return fmt.Errorf("CASE: constant value %d below BYTE range (0-255)", value)
|
||||||
|
}
|
||||||
|
} else if !isVar {
|
||||||
|
// Case is literal, switch is variable
|
||||||
|
if switchInfo.SwitchOperand.VarKind == compiler.KindByte && value > 255 {
|
||||||
|
return fmt.Errorf("CASE: literal value %d will never match BYTE variable '%s' (valid range: 0-255)",
|
||||||
|
value, switchInfo.SwitchOperand.VarName)
|
||||||
|
}
|
||||||
|
if switchInfo.SwitchOperand.VarKind == compiler.KindByte && value < 0 {
|
||||||
|
return fmt.Errorf("CASE: literal value %d will never match BYTE variable '%s' (valid range: 0-255)",
|
||||||
|
value, switchInfo.SwitchOperand.VarName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CaseCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||||
|
switchInfo, err := ctx.SwitchStack.Peek()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("CASE: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if DEFAULT has already been seen
|
||||||
|
if switchInfo.HasDefault {
|
||||||
|
return nil, fmt.Errorf("CASE: cannot have CASE after DEFAULT")
|
||||||
|
}
|
||||||
|
|
||||||
|
var asm []string
|
||||||
|
|
||||||
|
// If there was a previous CASE, emit implicit break and skip label
|
||||||
|
if switchInfo.NeedsPendingCode {
|
||||||
|
// Implicit break: jump to end of switch
|
||||||
|
asm = append(asm, fmt.Sprintf("\tjmp %s", switchInfo.EndLabel))
|
||||||
|
|
||||||
|
// Emit the pending skip label
|
||||||
|
asm = append(asm, switchInfo.PendingSkipLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push a new skip label onto the CaseSkipStack (like IF does with ctx.IfStack)
|
||||||
|
skipLabel := ctx.CaseSkipStack.Push()
|
||||||
|
|
||||||
|
// Convert switch operand to operandInfo
|
||||||
|
switchOp := &operandInfo{
|
||||||
|
varName: switchInfo.SwitchOperand.VarName,
|
||||||
|
varKind: switchInfo.SwitchOperand.VarKind,
|
||||||
|
value: switchInfo.SwitchOperand.Value,
|
||||||
|
isVar: switchInfo.SwitchOperand.IsVar,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate comparison: if switch_var == case_value, execute case (don't jump)
|
||||||
|
// Otherwise jump to skipLabel
|
||||||
|
// comparisonGenerator jumps on FALSE, so we use opEqual:
|
||||||
|
// - When equal (TRUE): don't jump, execute case
|
||||||
|
// - When not equal (FALSE): jump to skip label
|
||||||
|
gen, err := newComparisonGenerator(
|
||||||
|
opEqual,
|
||||||
|
switchOp,
|
||||||
|
c.caseValue,
|
||||||
|
switchInfo.UseLongJump,
|
||||||
|
ctx.CaseSkipStack,
|
||||||
|
ctx.GeneralStack,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("CASE: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmpAsm, err := gen.generate()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("CASE: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
asm = append(asm, cmpAsm...)
|
||||||
|
|
||||||
|
// Mark that we need to emit break + skip label next time
|
||||||
|
switchInfo.NeedsPendingCode = true
|
||||||
|
switchInfo.PendingSkipLabel = skipLabel
|
||||||
|
|
||||||
|
return asm, nil
|
||||||
|
}
|
||||||
78
internal/commands/default.go
Normal file
78
internal/commands/default.go
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"c65gm/internal/compiler"
|
||||||
|
"c65gm/internal/preproc"
|
||||||
|
"c65gm/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultCommand handles DEFAULT statements within SWITCH
|
||||||
|
// Syntax: DEFAULT
|
||||||
|
type DefaultCommand struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DefaultCommand) WillHandle(line preproc.Line) bool {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil || len(params) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.ToUpper(params[0]) == "DEFAULT"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DefaultCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) != 1 {
|
||||||
|
return fmt.Errorf("DEFAULT: expected 1 parameter (DEFAULT), got %d", len(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're inside a SWITCH
|
||||||
|
if ctx.SwitchStack.IsEmpty() {
|
||||||
|
return fmt.Errorf("DEFAULT: not inside a SWITCH statement")
|
||||||
|
}
|
||||||
|
|
||||||
|
switchInfo, err := ctx.SwitchStack.Peek()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("DEFAULT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if DEFAULT has already been seen
|
||||||
|
if switchInfo.HasDefault {
|
||||||
|
return fmt.Errorf("DEFAULT: multiple DEFAULT statements in same SWITCH")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *DefaultCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||||
|
switchInfo, err := ctx.SwitchStack.Peek()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("DEFAULT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var asm []string
|
||||||
|
|
||||||
|
// If there was a previous CASE, emit implicit break and skip label
|
||||||
|
if switchInfo.NeedsPendingCode {
|
||||||
|
// Implicit break: jump to end of switch
|
||||||
|
asm = append(asm, fmt.Sprintf("\tjmp %s", switchInfo.EndLabel))
|
||||||
|
|
||||||
|
// Emit the pending skip label (where previous case jumps if not matched)
|
||||||
|
asm = append(asm, switchInfo.PendingSkipLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark that we've seen DEFAULT
|
||||||
|
switchInfo.HasDefault = true
|
||||||
|
|
||||||
|
// DEFAULT doesn't need a skip label, so clear pending code
|
||||||
|
switchInfo.NeedsPendingCode = false
|
||||||
|
switchInfo.PendingSkipLabel = ""
|
||||||
|
|
||||||
|
return asm, nil
|
||||||
|
}
|
||||||
60
internal/commands/endswitch.go
Normal file
60
internal/commands/endswitch.go
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"c65gm/internal/compiler"
|
||||||
|
"c65gm/internal/preproc"
|
||||||
|
"c65gm/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EndSwitchCommand handles ENDSWITCH statements
|
||||||
|
// Syntax: ENDSWITCH
|
||||||
|
type EndSwitchCommand struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EndSwitchCommand) WillHandle(line preproc.Line) bool {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil || len(params) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.ToUpper(params[0]) == "ENDSWITCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EndSwitchCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) != 1 {
|
||||||
|
return fmt.Errorf("ENDSWITCH: expected 1 parameter (ENDSWITCH), got %d", len(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we're inside a SWITCH
|
||||||
|
if ctx.SwitchStack.IsEmpty() {
|
||||||
|
return fmt.Errorf("ENDSWITCH: not inside a SWITCH statement")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *EndSwitchCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||||
|
switchInfo, err := ctx.SwitchStack.Pop()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ENDSWITCH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var asm []string
|
||||||
|
|
||||||
|
// If there's pending code (last CASE without DEFAULT), emit skip label
|
||||||
|
if switchInfo.NeedsPendingCode {
|
||||||
|
asm = append(asm, switchInfo.PendingSkipLabel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emit the ENDSWITCH label (where all breaks jump to)
|
||||||
|
asm = append(asm, switchInfo.EndLabel)
|
||||||
|
|
||||||
|
return asm, nil
|
||||||
|
}
|
||||||
82
internal/commands/switch.go
Normal file
82
internal/commands/switch.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"c65gm/internal/compiler"
|
||||||
|
"c65gm/internal/preproc"
|
||||||
|
"c65gm/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SwitchCommand handles SWITCH statements
|
||||||
|
// Syntax: SWITCH <variable>
|
||||||
|
type SwitchCommand struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchCommand) WillHandle(line preproc.Line) bool {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil || len(params) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.ToUpper(params[0]) == "SWITCH"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchCommand) Interpret(line preproc.Line, ctx *compiler.CompilerContext) error {
|
||||||
|
params, err := utils.ParseParams(line.Text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(params) != 2 {
|
||||||
|
return fmt.Errorf("SWITCH: expected 2 parameters (SWITCH <variable>), got %d", len(params))
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := ctx.CurrentScope()
|
||||||
|
constLookup := func(name string) (int64, bool) {
|
||||||
|
sym := ctx.SymbolTable.Lookup(name, scope)
|
||||||
|
if sym != nil && sym.IsConst() {
|
||||||
|
return int64(sym.Value), true
|
||||||
|
}
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the switch variable/expression
|
||||||
|
varName, varKind, value, isVar, err := compiler.ParseOperandParam(
|
||||||
|
params[1], ctx.SymbolTable, scope, constLookup)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("SWITCH: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check pragma for long jumps
|
||||||
|
ps := ctx.Pragma.GetPragmaSetByIndex(line.PragmaSetIndex)
|
||||||
|
longJumpPragma := ps.GetPragma("_P_USE_LONG_JUMP")
|
||||||
|
useLongJump := longJumpPragma != "" && longJumpPragma != "0"
|
||||||
|
|
||||||
|
// Create end label
|
||||||
|
endLabel := ctx.GeneralStack.Push()
|
||||||
|
|
||||||
|
// Create and push switch info
|
||||||
|
switchInfo := &compiler.SwitchInfo{
|
||||||
|
SwitchOperand: &compiler.OperandInfo{
|
||||||
|
VarName: varName,
|
||||||
|
VarKind: varKind,
|
||||||
|
Value: value,
|
||||||
|
IsVar: isVar,
|
||||||
|
},
|
||||||
|
EndLabel: endLabel,
|
||||||
|
NeedsPendingCode: false,
|
||||||
|
PendingSkipLabel: "",
|
||||||
|
HasDefault: false,
|
||||||
|
UseLongJump: useLongJump,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SwitchStack.Push(switchInfo)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *SwitchCommand) Generate(ctx *compiler.CompilerContext) ([]string, error) {
|
||||||
|
// SWITCH itself generates no assembly code
|
||||||
|
// The variable will be loaded and compared by each CASE
|
||||||
|
return []string{}, nil
|
||||||
|
}
|
||||||
918
internal/commands/switch_test.go
Normal file
918
internal/commands/switch_test.go
Normal file
|
|
@ -0,0 +1,918 @@
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"c65gm/internal/compiler"
|
||||||
|
"c65gm/internal/preproc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSwitchBasicByte(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setupVars func(*compiler.SymbolTable)
|
||||||
|
caseValue string
|
||||||
|
wantSwitch []string
|
||||||
|
wantCase []string
|
||||||
|
wantEndswitch []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "byte var with byte literal case",
|
||||||
|
setupVars: func(st *compiler.SymbolTable) {
|
||||||
|
st.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
},
|
||||||
|
caseValue: "10",
|
||||||
|
wantSwitch: []string{},
|
||||||
|
wantCase: []string{
|
||||||
|
"\tlda x",
|
||||||
|
"\tcmp #$0a",
|
||||||
|
"\tbne ",
|
||||||
|
},
|
||||||
|
wantEndswitch: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
tt.setupVars(ctx.SymbolTable)
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
caseCmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH Interpret() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switchAsm, err := switchCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SWITCH Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := caseCmd.Interpret(preproc.Line{Text: "CASE " + tt.caseValue, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE Interpret() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
caseAsm, err := caseCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CASE Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH Interpret() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endswitchAsm, err := endswitchCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !equalAsmSwitch(switchAsm, tt.wantSwitch) {
|
||||||
|
t.Errorf("SWITCH Generate() mismatch\ngot:\n%s\nwant:\n%s",
|
||||||
|
strings.Join(switchAsm, "\n"),
|
||||||
|
strings.Join(tt.wantSwitch, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// For CASE, check that expected instructions are present
|
||||||
|
if !containsInstructions(caseAsm, tt.wantCase) {
|
||||||
|
t.Errorf("CASE Generate() missing expected instructions\ngot:\n%s\nwant to contain:\n%s",
|
||||||
|
strings.Join(caseAsm, "\n"),
|
||||||
|
strings.Join(tt.wantCase, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENDSWITCH should emit at least one label
|
||||||
|
if len(endswitchAsm) == 0 {
|
||||||
|
t.Error("ENDSWITCH should generate at least end label")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchMultipleCases(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
case1Cmd := &CaseCommand{}
|
||||||
|
case2Cmd := &CaseCommand{}
|
||||||
|
case3Cmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 1 error = %v", err)
|
||||||
|
}
|
||||||
|
case1Asm, _ := case1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 2", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 2 error = %v", err)
|
||||||
|
}
|
||||||
|
case2Asm, _ := case2Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := case3Cmd.Interpret(preproc.Line{Text: "CASE 3", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 3 error = %v", err)
|
||||||
|
}
|
||||||
|
_, _ = case3Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
endswitchAsm, _ := endswitchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
// First CASE should not have JMP at the beginning
|
||||||
|
if len(case1Asm) > 0 && strings.Contains(case1Asm[0], "jmp") {
|
||||||
|
t.Error("First CASE should not start with JMP")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second CASE should have implicit break (JMP) from previous case
|
||||||
|
foundJmp := false
|
||||||
|
for _, line := range case2Asm {
|
||||||
|
if strings.Contains(line, "jmp") {
|
||||||
|
foundJmp = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundJmp {
|
||||||
|
t.Error("Second CASE should have implicit break JMP from previous case")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENDSWITCH should have end label
|
||||||
|
if len(endswitchAsm) == 0 {
|
||||||
|
t.Error("ENDSWITCH should have end label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchWithDefault(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
case1Cmd := &CaseCommand{}
|
||||||
|
defaultCmd := &DefaultCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE error = %v", err)
|
||||||
|
}
|
||||||
|
case1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("DEFAULT error = %v", err)
|
||||||
|
}
|
||||||
|
defaultAsm, err := defaultCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DEFAULT Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
endswitchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
// DEFAULT should emit implicit break and skip label
|
||||||
|
if len(defaultAsm) == 0 {
|
||||||
|
t.Error("DEFAULT should emit code for implicit break")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchCaseAfterDefault(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
case1Cmd := &CaseCommand{}
|
||||||
|
defaultCmd := &DefaultCommand{}
|
||||||
|
case2Cmd := &CaseCommand{}
|
||||||
|
|
||||||
|
switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
case1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
defaultCmd.Generate(ctx)
|
||||||
|
|
||||||
|
// Try to add CASE after DEFAULT - should fail
|
||||||
|
if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 2", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
// This is expected to fail during Interpret
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := case2Cmd.Generate(ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("CASE after DEFAULT should fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "after DEFAULT") {
|
||||||
|
t.Errorf("Wrong error message: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchMultipleDefaults(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
case1Cmd := &CaseCommand{}
|
||||||
|
default1Cmd := &DefaultCommand{}
|
||||||
|
default2Cmd := &DefaultCommand{}
|
||||||
|
|
||||||
|
switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
case1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
default1Cmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
default1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
// Try to add second DEFAULT - should fail
|
||||||
|
err := default2Cmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Multiple DEFAULT statements should fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "multiple DEFAULT") {
|
||||||
|
t.Errorf("Wrong error message: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchWithoutSwitch(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
caseCmd := &CaseCommand{}
|
||||||
|
err := caseCmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("CASE without SWITCH should fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "not inside") {
|
||||||
|
t.Errorf("Wrong error message: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultWithoutSwitch(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
defaultCmd := &DefaultCommand{}
|
||||||
|
err := defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("DEFAULT without SWITCH should fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "not inside") {
|
||||||
|
t.Errorf("Wrong error message: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndswitchWithoutSwitch(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("ENDSWITCH without SWITCH should fail")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "not inside") {
|
||||||
|
t.Errorf("Wrong error message: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchWrongParamCount(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"SWITCH",
|
||||||
|
"SWITCH x y",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, text := range tests {
|
||||||
|
cmd := &SwitchCommand{}
|
||||||
|
err := cmd.Interpret(preproc.Line{Text: text, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Should fail with wrong param count: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCaseWrongParamCount(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
tests := []string{
|
||||||
|
"CASE",
|
||||||
|
"CASE 1 2",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, text := range tests {
|
||||||
|
cmd := &CaseCommand{}
|
||||||
|
err := cmd.Interpret(preproc.Line{Text: text, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Should fail with wrong param count: %s", text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultWrongParamCount(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
cmd := &DefaultCommand{}
|
||||||
|
err := cmd.Interpret(preproc.Line{Text: "DEFAULT extra", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("DEFAULT with extra params should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEndswitchWrongParamCount(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
cmd := &EndSwitchCommand{}
|
||||||
|
err := cmd.Interpret(preproc.Line{Text: "ENDSWITCH extra", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("ENDSWITCH with extra params should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchWordType(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("big_val", "", compiler.KindWord, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
caseCmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH big_val", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := caseCmd.Interpret(preproc.Line{Text: "CASE 1000", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE error = %v", err)
|
||||||
|
}
|
||||||
|
caseAsm, err := caseCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CASE Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
endswitchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
// Should have high byte check for word
|
||||||
|
foundHighByteCheck := false
|
||||||
|
for _, inst := range caseAsm {
|
||||||
|
if strings.Contains(inst, "big_val+1") {
|
||||||
|
foundHighByteCheck = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundHighByteCheck {
|
||||||
|
t.Error("Expected high byte check for word comparison")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchWithConstant(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddConst("MAX_VAL", "", compiler.KindByte, 100)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
caseCmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := caseCmd.Interpret(preproc.Line{Text: "CASE MAX_VAL", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE error = %v", err)
|
||||||
|
}
|
||||||
|
caseAsm, err := caseCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CASE Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
endswitchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
// Constant should be folded to immediate value
|
||||||
|
found := false
|
||||||
|
for _, inst := range caseAsm {
|
||||||
|
if strings.Contains(inst, "#$64") { // 100 = 0x64
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Error("Constant should be folded to immediate value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchNested(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("outer", "", compiler.KindByte, 0)
|
||||||
|
ctx.SymbolTable.AddVar("inner", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switch1Cmd := &SwitchCommand{}
|
||||||
|
case1Cmd := &CaseCommand{}
|
||||||
|
switch2Cmd := &SwitchCommand{}
|
||||||
|
case2Cmd := &CaseCommand{}
|
||||||
|
endswitch2Cmd := &EndSwitchCommand{}
|
||||||
|
endswitch1Cmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switch1Cmd.Interpret(preproc.Line{Text: "SWITCH outer", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH 1 error = %v", err)
|
||||||
|
}
|
||||||
|
switch1Asm, _ := switch1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 1 error = %v", err)
|
||||||
|
}
|
||||||
|
case1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := switch2Cmd.Interpret(preproc.Line{Text: "SWITCH inner", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH 2 error = %v", err)
|
||||||
|
}
|
||||||
|
switch2Asm, _ := switch2Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 2", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 2 error = %v", err)
|
||||||
|
}
|
||||||
|
case2Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := endswitch2Cmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH 2 error = %v", err)
|
||||||
|
}
|
||||||
|
endswitch2Asm, _ := endswitch2Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := endswitch1Cmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH 1 error = %v", err)
|
||||||
|
}
|
||||||
|
endswitch1Asm, _ := endswitch1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
// Both switches should generate assembly
|
||||||
|
if len(switch1Asm) < 0 || len(switch2Asm) < 0 {
|
||||||
|
// SWITCHes don't generate asm, just setup state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both ENDSWITCHes should generate labels
|
||||||
|
if len(endswitch1Asm) == 0 || len(endswitch2Asm) == 0 {
|
||||||
|
t.Error("Nested switches should both generate end labels")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Labels should be different
|
||||||
|
label1 := ""
|
||||||
|
label2 := ""
|
||||||
|
if len(endswitch1Asm) > 0 {
|
||||||
|
label1 = endswitch1Asm[len(endswitch1Asm)-1]
|
||||||
|
}
|
||||||
|
if len(endswitch2Asm) > 0 {
|
||||||
|
label2 = endswitch2Asm[len(endswitch2Asm)-1]
|
||||||
|
}
|
||||||
|
|
||||||
|
if label1 == label2 && label1 != "" {
|
||||||
|
t.Error("Nested switches should have different end labels")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchLongJump(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
pragma.AddPragma("_P_USE_LONG_JUMP", "1")
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
case1Cmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 1 error = %v", err)
|
||||||
|
}
|
||||||
|
case1Asm, _ := case1Cmd.Generate(ctx)
|
||||||
|
|
||||||
|
endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
endswitchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
// In long jump mode, comparison should have pattern: short branch + jmp + label
|
||||||
|
// Look for both a short branch (beq/bne) and a jmp to _SKIPCASE
|
||||||
|
foundShortBranch := false
|
||||||
|
foundJmpToSkip := false
|
||||||
|
foundLabel := false
|
||||||
|
|
||||||
|
for _, inst := range case1Asm {
|
||||||
|
if strings.Contains(inst, "beq ") || strings.Contains(inst, "bne ") {
|
||||||
|
foundShortBranch = true
|
||||||
|
}
|
||||||
|
if strings.Contains(inst, "jmp ") && strings.Contains(inst, "_SKIPCASE") {
|
||||||
|
foundJmpToSkip = true
|
||||||
|
}
|
||||||
|
// Labels don't start with tab
|
||||||
|
trimmed := strings.TrimSpace(inst)
|
||||||
|
if !strings.HasPrefix(inst, "\t") && strings.Contains(inst, "_") && len(trimmed) > 0 {
|
||||||
|
foundLabel = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !foundShortBranch {
|
||||||
|
t.Error("Long jump mode should have short branch (beq/bne)")
|
||||||
|
}
|
||||||
|
if !foundJmpToSkip {
|
||||||
|
t.Error("Long jump mode should have JMP to skip label in comparison")
|
||||||
|
}
|
||||||
|
if !foundLabel {
|
||||||
|
t.Error("Long jump mode should have success label in comparison")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the pattern is different from normal mode
|
||||||
|
pragmaNormal := preproc.NewPragma()
|
||||||
|
ctxNormal := compiler.NewCompilerContext(pragmaNormal)
|
||||||
|
ctxNormal.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdxNormal := pragmaNormal.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmdNormal := &SwitchCommand{}
|
||||||
|
caseCmdNormal := &CaseCommand{}
|
||||||
|
|
||||||
|
switchCmdNormal.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdxNormal}, ctxNormal)
|
||||||
|
switchCmdNormal.Generate(ctxNormal)
|
||||||
|
|
||||||
|
caseCmdNormal.Interpret(preproc.Line{Text: "CASE 1", Kind: preproc.Source, PragmaSetIndex: pragmaIdxNormal}, ctxNormal)
|
||||||
|
normalAsm, _ := caseCmdNormal.Generate(ctxNormal)
|
||||||
|
|
||||||
|
// Normal mode should NOT have jmp in comparison (only short branch)
|
||||||
|
normalHasJmp := false
|
||||||
|
for _, inst := range normalAsm {
|
||||||
|
if strings.Contains(inst, "\tjmp") {
|
||||||
|
normalHasJmp = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if normalHasJmp {
|
||||||
|
t.Error("Normal mode should not have JMP in comparison code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchOnConstant(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddConst("VALUE", "", compiler.KindByte, 5)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
case1Cmd := &CaseCommand{}
|
||||||
|
case2Cmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH VALUE", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH on constant error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
// CASE with matching constant - should be optimized away (constant folding)
|
||||||
|
if err := case1Cmd.Interpret(preproc.Line{Text: "CASE 5", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 5 error = %v", err)
|
||||||
|
}
|
||||||
|
case1Asm, err := case1Cmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CASE 5 Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With constant folding, matching constant case generates no comparison code (optimization)
|
||||||
|
if len(case1Asm) != 0 {
|
||||||
|
t.Errorf("CASE with matching constant should be optimized away, got: %v", case1Asm)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CASE with non-matching constant - should generate JMP to skip
|
||||||
|
if err := case2Cmd.Interpret(preproc.Line{Text: "CASE 10", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("CASE 10 error = %v", err)
|
||||||
|
}
|
||||||
|
case2Asm, err := case2Cmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CASE 10 Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-matching constant should generate JMP to skip this case
|
||||||
|
if len(case2Asm) == 0 {
|
||||||
|
t.Error("CASE with non-matching constant should generate skip code")
|
||||||
|
}
|
||||||
|
foundJmp := false
|
||||||
|
for _, line := range case2Asm {
|
||||||
|
if strings.Contains(line, "jmp") {
|
||||||
|
foundJmp = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundJmp {
|
||||||
|
t.Error("Non-matching constant case should have JMP to skip")
|
||||||
|
}
|
||||||
|
|
||||||
|
endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
endswitchCmd.Generate(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchEmptyWithOnlyDefault(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
defaultCmd := &DefaultCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
if err := defaultCmd.Interpret(preproc.Line{Text: "DEFAULT", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("DEFAULT error = %v", err)
|
||||||
|
}
|
||||||
|
defaultAsm, err := defaultCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DEFAULT Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
endswitchAsm, err := endswitchCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ENDSWITCH Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DEFAULT without previous CASE should not emit JMP
|
||||||
|
hasJmp := false
|
||||||
|
for _, line := range defaultAsm {
|
||||||
|
if strings.Contains(line, "jmp") {
|
||||||
|
hasJmp = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasJmp {
|
||||||
|
t.Error("DEFAULT without previous CASE should not emit JMP")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ENDSWITCH should still emit end label
|
||||||
|
if len(endswitchAsm) == 0 {
|
||||||
|
t.Error("ENDSWITCH should emit end label")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchComparisonTypes(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
varType compiler.VarKind
|
||||||
|
caseValue string
|
||||||
|
shouldWork bool
|
||||||
|
}{
|
||||||
|
{"byte var byte case", compiler.KindByte, "10", true},
|
||||||
|
{"word var byte case", compiler.KindWord, "10", true},
|
||||||
|
{"byte var word case", compiler.KindByte, "1000", false},
|
||||||
|
{"word var word case", compiler.KindWord, "1000", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", tt.varType, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
caseCmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
err := caseCmd.Interpret(preproc.Line{Text: "CASE " + tt.caseValue, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if tt.shouldWork && err != nil {
|
||||||
|
t.Fatalf("CASE Interpret() unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
if !tt.shouldWork && err == nil {
|
||||||
|
t.Fatalf("CASE Interpret() should have failed but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.shouldWork {
|
||||||
|
_, err := caseCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CASE Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
endswitchCmd.Generate(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchWithVariableCase(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
switchType compiler.VarKind
|
||||||
|
caseType compiler.VarKind
|
||||||
|
shouldWork bool
|
||||||
|
}{
|
||||||
|
{"byte switch byte case", compiler.KindByte, compiler.KindByte, true},
|
||||||
|
{"byte switch word case", compiler.KindByte, compiler.KindWord, true},
|
||||||
|
{"word switch byte case", compiler.KindWord, compiler.KindByte, true},
|
||||||
|
{"word switch word case", compiler.KindWord, compiler.KindWord, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("switch_var", "", tt.switchType, 0)
|
||||||
|
ctx.SymbolTable.AddVar("case_var", "", tt.caseType, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
caseCmd := &CaseCommand{}
|
||||||
|
endswitchCmd := &EndSwitchCommand{}
|
||||||
|
|
||||||
|
if err := switchCmd.Interpret(preproc.Line{Text: "SWITCH switch_var", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx); err != nil {
|
||||||
|
t.Fatalf("SWITCH error = %v", err)
|
||||||
|
}
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
err := caseCmd.Interpret(preproc.Line{Text: "CASE case_var", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
if tt.shouldWork && err != nil {
|
||||||
|
t.Fatalf("CASE with variable unexpected error = %v", err)
|
||||||
|
}
|
||||||
|
if !tt.shouldWork && err == nil {
|
||||||
|
t.Fatal("CASE with variable should have failed but didn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.shouldWork {
|
||||||
|
caseAsm, err := caseCmd.Generate(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CASE Generate() error = %v", err)
|
||||||
|
}
|
||||||
|
// Verify assembly was generated for comparison
|
||||||
|
if len(caseAsm) == 0 {
|
||||||
|
t.Error("CASE with variable should generate comparison code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endswitchCmd.Interpret(preproc.Line{Text: "ENDSWITCH", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
endswitchCmd.Generate(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSwitchByteRangeValidation(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
caseValue string
|
||||||
|
expectError bool
|
||||||
|
errorContains string
|
||||||
|
}{
|
||||||
|
{"valid byte 0", "0", false, ""},
|
||||||
|
{"valid byte 255", "255", false, ""},
|
||||||
|
{"valid byte 100", "100", false, ""},
|
||||||
|
{"out of range 256", "256", true, "will never match BYTE variable"},
|
||||||
|
{"out of range 1000", "1000", true, "will never match BYTE variable"},
|
||||||
|
{"out of range 10000", "10000", true, "will never match BYTE variable"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
pragma := preproc.NewPragma()
|
||||||
|
ctx := compiler.NewCompilerContext(pragma)
|
||||||
|
ctx.SymbolTable.AddVar("x", "", compiler.KindByte, 0)
|
||||||
|
|
||||||
|
pragmaIdx := pragma.GetCurrentPragmaSetIndex()
|
||||||
|
|
||||||
|
switchCmd := &SwitchCommand{}
|
||||||
|
caseCmd := &CaseCommand{}
|
||||||
|
|
||||||
|
switchCmd.Interpret(preproc.Line{Text: "SWITCH x", Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
switchCmd.Generate(ctx)
|
||||||
|
|
||||||
|
err := caseCmd.Interpret(preproc.Line{Text: "CASE " + tt.caseValue, Kind: preproc.Source, PragmaSetIndex: pragmaIdx}, ctx)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error for CASE %s but got none", tt.caseValue)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), tt.errorContains) {
|
||||||
|
t.Errorf("Error message '%s' should contain '%s'", err.Error(), tt.errorContains)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error for CASE %s: %v", tt.caseValue, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to compare assembly output
|
||||||
|
func equalAsmSwitch(got, want []string) bool {
|
||||||
|
if len(got) != len(want) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range got {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to check if assembly contains expected instructions
|
||||||
|
func containsInstructions(asm []string, expected []string) bool {
|
||||||
|
for _, exp := range expected {
|
||||||
|
found := false
|
||||||
|
for _, line := range asm {
|
||||||
|
if strings.Contains(line, strings.TrimSpace(exp)) {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
@ -16,11 +16,13 @@ type CompilerContext struct {
|
||||||
ConstStrHandler *ConstantStringHandler
|
ConstStrHandler *ConstantStringHandler
|
||||||
|
|
||||||
// Label stacks for control flow
|
// Label stacks for control flow
|
||||||
LoopStartStack *LabelStack // Start of loop (like WHILE)
|
LoopStartStack *LabelStack // Start of loop (like WHILE)
|
||||||
LoopEndStack *LabelStack // WHILE...WEND
|
LoopEndStack *LabelStack // WHILE...WEND
|
||||||
IfStack *LabelStack // IF...ENDIF
|
IfStack *LabelStack // IF...ENDIF
|
||||||
GeneralStack *LabelStack // General purpose (GOSUB, etc)
|
GeneralStack *LabelStack // General purpose (GOSUB, etc)
|
||||||
ForStack *ForStack // For loop stack
|
ForStack *ForStack // For loop stack
|
||||||
|
SwitchStack *SwitchStack // Switch/case stack
|
||||||
|
CaseSkipStack *LabelStack // SWITCH/CASE skip labels
|
||||||
|
|
||||||
// Pragma access for per-line pragma lookup
|
// Pragma access for per-line pragma lookup
|
||||||
Pragma *preproc.Pragma
|
Pragma *preproc.Pragma
|
||||||
|
|
@ -40,6 +42,8 @@ func NewCompilerContext(pragma *preproc.Pragma) *CompilerContext {
|
||||||
IfStack: NewLabelStack("_I"),
|
IfStack: NewLabelStack("_I"),
|
||||||
GeneralStack: generalStack,
|
GeneralStack: generalStack,
|
||||||
ForStack: NewForStack(),
|
ForStack: NewForStack(),
|
||||||
|
SwitchStack: NewSwitchStack(),
|
||||||
|
CaseSkipStack: NewLabelStack("_SKIPCASE"),
|
||||||
Pragma: pragma,
|
Pragma: pragma,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
58
internal/compiler/switchstack.go
Normal file
58
internal/compiler/switchstack.go
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
package compiler
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// SwitchInfo stores information about a SWITCH statement
|
||||||
|
type SwitchInfo struct {
|
||||||
|
SwitchOperand *OperandInfo // The expression being switched on
|
||||||
|
EndLabel string // Label for end of switch (_ENDSWITCH)
|
||||||
|
NeedsPendingCode bool // True if we need to emit implicit break + skip label
|
||||||
|
PendingSkipLabel string // The skip label that needs to be emitted
|
||||||
|
HasDefault bool // True if DEFAULT has been encountered
|
||||||
|
UseLongJump bool // Whether to use long jumps
|
||||||
|
}
|
||||||
|
|
||||||
|
// SwitchStack manages the stack of SWITCH contexts
|
||||||
|
type SwitchStack struct {
|
||||||
|
stack []*SwitchInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSwitchStack creates a new SwitchStack
|
||||||
|
func NewSwitchStack() *SwitchStack {
|
||||||
|
return &SwitchStack{
|
||||||
|
stack: make([]*SwitchInfo, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push adds a new SWITCH context to the stack
|
||||||
|
func (ss *SwitchStack) Push(info *SwitchInfo) {
|
||||||
|
ss.stack = append(ss.stack, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peek returns the top SWITCH context without removing it
|
||||||
|
func (ss *SwitchStack) Peek() (*SwitchInfo, error) {
|
||||||
|
if len(ss.stack) == 0 {
|
||||||
|
return nil, fmt.Errorf("stack underflow: SWITCH stack is empty")
|
||||||
|
}
|
||||||
|
return ss.stack[len(ss.stack)-1], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pop removes and returns the top SWITCH context
|
||||||
|
func (ss *SwitchStack) Pop() (*SwitchInfo, error) {
|
||||||
|
if len(ss.stack) == 0 {
|
||||||
|
return nil, fmt.Errorf("stack underflow: SWITCH stack is empty")
|
||||||
|
}
|
||||||
|
info := ss.stack[len(ss.stack)-1]
|
||||||
|
ss.stack = ss.stack[:len(ss.stack)-1]
|
||||||
|
return info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the stack is empty
|
||||||
|
func (ss *SwitchStack) IsEmpty() bool {
|
||||||
|
return len(ss.stack) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the number of items on the stack
|
||||||
|
func (ss *SwitchStack) Size() int {
|
||||||
|
return len(ss.stack)
|
||||||
|
}
|
||||||
4
main.go
4
main.go
|
|
@ -107,6 +107,10 @@ func registerCommands(comp *compiler.Compiler) {
|
||||||
comp.Registry().Register(&commands.GosubCommand{})
|
comp.Registry().Register(&commands.GosubCommand{})
|
||||||
comp.Registry().Register(&commands.ForCommand{})
|
comp.Registry().Register(&commands.ForCommand{})
|
||||||
comp.Registry().Register(&commands.NextCommand{})
|
comp.Registry().Register(&commands.NextCommand{})
|
||||||
|
comp.Registry().Register(&commands.SwitchCommand{})
|
||||||
|
comp.Registry().Register(&commands.CaseCommand{})
|
||||||
|
comp.Registry().Register(&commands.DefaultCommand{})
|
||||||
|
comp.Registry().Register(&commands.EndSwitchCommand{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeOutput(filename string, lines []string) error {
|
func writeOutput(filename string, lines []string) error {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue