From e33460d84dab01ba15736a4062aa1ab6e876e2ec Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Fri, 19 Dec 2025 22:56:16 +0100 Subject: [PATCH] Added SWITCH/CASE/DEFAULT/ENDSWITCH --- .gitignore | 3 + Dockerfile | 2 + examples/switch_demo/cm.sh | 20 + examples/switch_demo/start_in_vice.sh | 1 + examples/switch_demo/switch_demo.c65 | 256 +++++++ internal/commands/case.go | 156 +++++ internal/commands/default.go | 78 +++ internal/commands/endswitch.go | 60 ++ internal/commands/switch.go | 82 +++ internal/commands/switch_test.go | 918 ++++++++++++++++++++++++++ internal/compiler/context.go | 14 +- internal/compiler/switchstack.go | 58 ++ main.go | 4 + 13 files changed, 1647 insertions(+), 5 deletions(-) create mode 100755 examples/switch_demo/cm.sh create mode 100644 examples/switch_demo/start_in_vice.sh create mode 100644 examples/switch_demo/switch_demo.c65 create mode 100644 internal/commands/case.go create mode 100644 internal/commands/default.go create mode 100644 internal/commands/endswitch.go create mode 100644 internal/commands/switch.go create mode 100644 internal/commands/switch_test.go create mode 100644 internal/compiler/switchstack.go diff --git a/.gitignore b/.gitignore index 37a00cb..cb9ad25 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ c65gm .claude/ .npm/ +*.sym +*.prg +*.s diff --git a/Dockerfile b/Dockerfile index f6a6d4b..653f077 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] diff --git a/examples/switch_demo/cm.sh b/examples/switch_demo/cm.sh new file mode 100755 index 0000000..5a5e431 --- /dev/null +++ b/examples/switch_demo/cm.sh @@ -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 diff --git a/examples/switch_demo/start_in_vice.sh b/examples/switch_demo/start_in_vice.sh new file mode 100644 index 0000000..865c48e --- /dev/null +++ b/examples/switch_demo/start_in_vice.sh @@ -0,0 +1 @@ +x64 -autostartprgmode 1 main.prg diff --git a/examples/switch_demo/switch_demo.c65 b/examples/switch_demo/switch_demo.c65 new file mode 100644 index 0000000..1bb9e0c --- /dev/null +++ b/examples/switch_demo/switch_demo.c65 @@ -0,0 +1,256 @@ +//----------------------------------------------------------- +// SWITCH/CASE Statement Demo +// Demonstrates the SWITCH/CASE control flow statement +// with implicit breaks and long jump pragma support +//----------------------------------------------------------- + +#INCLUDE +#INCLUDE +#INCLUDE + +#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 diff --git a/internal/commands/case.go b/internal/commands/case.go new file mode 100644 index 0000000..6a6762d --- /dev/null +++ b/internal/commands/case.go @@ -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 +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 ), 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 +} diff --git a/internal/commands/default.go b/internal/commands/default.go new file mode 100644 index 0000000..711ed78 --- /dev/null +++ b/internal/commands/default.go @@ -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 +} diff --git a/internal/commands/endswitch.go b/internal/commands/endswitch.go new file mode 100644 index 0000000..5265d88 --- /dev/null +++ b/internal/commands/endswitch.go @@ -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 +} diff --git a/internal/commands/switch.go b/internal/commands/switch.go new file mode 100644 index 0000000..f822e68 --- /dev/null +++ b/internal/commands/switch.go @@ -0,0 +1,82 @@ +package commands + +import ( + "fmt" + "strings" + + "c65gm/internal/compiler" + "c65gm/internal/preproc" + "c65gm/internal/utils" +) + +// SwitchCommand handles SWITCH statements +// Syntax: SWITCH +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 ), 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 +} diff --git a/internal/commands/switch_test.go b/internal/commands/switch_test.go new file mode 100644 index 0000000..bd16148 --- /dev/null +++ b/internal/commands/switch_test.go @@ -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 +} diff --git a/internal/compiler/context.go b/internal/compiler/context.go index 52dad72..0b489fe 100644 --- a/internal/compiler/context.go +++ b/internal/compiler/context.go @@ -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, } diff --git a/internal/compiler/switchstack.go b/internal/compiler/switchstack.go new file mode 100644 index 0000000..f9f9c66 --- /dev/null +++ b/internal/compiler/switchstack.go @@ -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) +} diff --git a/main.go b/main.go index 72622af..df67337 100644 --- a/main.go +++ b/main.go @@ -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 {