Added SWITCH/CASE/DEFAULT/ENDSWITCH

This commit is contained in:
Mattias Hansson 2025-12-19 22:56:16 +01:00
parent 4319373828
commit e33460d84d
13 changed files with 1647 additions and 5 deletions

3
.gitignore vendored
View file

@ -30,3 +30,6 @@ c65gm
.claude/
.npm/
*.sym
*.prg
*.s

View file

@ -1,4 +1,6 @@
FROM node:18-alpine
WORKDIR /app
RUN apk add --no-cache bash
ENV SHELL=/bin/bash
RUN npm install -g @anthropic-ai/claude-code
CMD ["claude"]

20
examples/switch_demo/cm.sh Executable file
View 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

View file

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

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

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

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

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

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

View file

@ -16,11 +16,13 @@ type CompilerContext struct {
ConstStrHandler *ConstantStringHandler
// Label stacks for control flow
LoopStartStack *LabelStack // Start of loop (like WHILE)
LoopEndStack *LabelStack // WHILE...WEND
IfStack *LabelStack // IF...ENDIF
GeneralStack *LabelStack // General purpose (GOSUB, etc)
ForStack *ForStack // For loop stack
LoopStartStack *LabelStack // Start of loop (like WHILE)
LoopEndStack *LabelStack // WHILE...WEND
IfStack *LabelStack // IF...ENDIF
GeneralStack *LabelStack // General purpose (GOSUB, etc)
ForStack *ForStack // For loop stack
SwitchStack *SwitchStack // Switch/case stack
CaseSkipStack *LabelStack // SWITCH/CASE skip labels
// Pragma access for per-line pragma lookup
Pragma *preproc.Pragma
@ -40,6 +42,8 @@ func NewCompilerContext(pragma *preproc.Pragma) *CompilerContext {
IfStack: NewLabelStack("_I"),
GeneralStack: generalStack,
ForStack: NewForStack(),
SwitchStack: NewSwitchStack(),
CaseSkipStack: NewLabelStack("_SKIPCASE"),
Pragma: pragma,
}

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

View file

@ -107,6 +107,10 @@ func registerCommands(comp *compiler.Compiler) {
comp.Registry().Register(&commands.GosubCommand{})
comp.Registry().Register(&commands.ForCommand{})
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 {