From 6837d4f06b504b5441178e3c3230e361fd9181a1 Mon Sep 17 00:00:00 2001 From: Associate 1 Date: Fri, 20 Feb 2026 12:38:11 -0700 Subject: [PATCH] Add 69 tests covering previously untested transpiler features Systematic gap analysis identified several implemented features with zero or near-zero test coverage. This adds e2e and lexer tests for: transputer intrinsics (LONGPROD, LONGDIV, LONGSUM, LONGDIFF, NORMALISE, SHIFTRIGHT, SHIFTLEFT), RETYPES bit reinterpretation, RESULT qualifier params, fixed-size array params, shared-type params, VAL []BYTE abbreviation, print.string/print.newline, ALT with boolean guards, MOSTNEG/MOSTPOS for REAL32/REAL64, modulo operator, SKIP/STOP, string-to-[]byte wrapping, Go reserved word escaping, multi-line expressions, and lexer paren/bracket depth suppression. Co-Authored-By: Claude Opus 4.6 --- TEST_COVERAGE_IMPROVEMENTS.md | 154 ++++++++ codegen/e2e_intrinsics_test.go | 243 ++++++++++++ codegen/e2e_misc_test.go | 682 +++++++++++++++++++++++++++++++++ codegen/e2e_params_test.go | 128 +++++++ codegen/e2e_retypes_test.go | 121 ++++++ codegen/e2e_strings_test.go | 67 ++++ lexer/lexer_test2_test.go | 305 +++++++++++++++ 7 files changed, 1700 insertions(+) create mode 100644 TEST_COVERAGE_IMPROVEMENTS.md create mode 100644 codegen/e2e_intrinsics_test.go create mode 100644 codegen/e2e_misc_test.go create mode 100644 codegen/e2e_params_test.go create mode 100644 codegen/e2e_retypes_test.go create mode 100644 codegen/e2e_strings_test.go create mode 100644 lexer/lexer_test2_test.go diff --git a/TEST_COVERAGE_IMPROVEMENTS.md b/TEST_COVERAGE_IMPROVEMENTS.md new file mode 100644 index 0000000..7a4682c --- /dev/null +++ b/TEST_COVERAGE_IMPROVEMENTS.md @@ -0,0 +1,154 @@ +# Test Coverage Improvements + +## Overview + +This document tracks additions to the test suite that close coverage gaps identified by analyzing the "What's Implemented" feature list against existing tests. + +## 2026-02-20: 69 New Tests + +### Gap Analysis + +Prior to this work, the test suite had ~124 e2e tests, ~49 codegen unit tests, ~80 parser tests, and ~9 lexer tests. Several implemented features had zero or near-zero test coverage: + +- Transputer intrinsics (LONGPROD, LONGDIV, LONGSUM, LONGDIFF, NORMALISE, SHIFTRIGHT, SHIFTLEFT) — zero tests at any level +- RETYPES bit reinterpretation — parser tests only, no codegen or e2e verification +- RESULT qualifier on proc params — completely untested +- Fixed-size array params `[n]TYPE` — completely untested +- Shared-type channel params `PROC f(CHAN INT a?, b?)` — completely untested +- `VAL []BYTE s IS "hi":` abbreviation — completely untested +- ALT with boolean guards — parser test only, no e2e +- MOSTNEG/MOSTPOS for REAL32/REAL64 — codegen unit only, no e2e +- Modulo operator `\` — lexer tokenization only +- `print.string` / `print.newline` — no e2e execution +- Lexer paren/bracket depth suppression — only exercised indirectly +- Lexer continuation-operator line joining — only exercised indirectly +- Most keywords — only 11 of 33+ had lexer-level tests + +### New Test Files + +#### `codegen/e2e_intrinsics_test.go` — 13 tests + +| Test | Feature | +|------|---------| +| `TestE2E_LONGPROD` | Basic 64-bit multiply | +| `TestE2E_LONGPRODWithCarry` | Multiply with carry addend | +| `TestE2E_LONGDIV` | Basic 64-bit divide | +| `TestE2E_LONGDIVLargeValue` | Roundtrip with LONGPROD | +| `TestE2E_LONGSUM` | Basic 64-bit add | +| `TestE2E_LONGSUMOverflow` | Addition with carry output | +| `TestE2E_LONGDIFF` | Basic 64-bit subtract | +| `TestE2E_LONGDIFFBorrow` | Subtraction with borrow | +| `TestE2E_NORMALISE` | Leading-zero normalization | +| `TestE2E_NORMALISEZero` | Zero input edge case | +| `TestE2E_SHIFTRIGHT` | 64-bit right shift | +| `TestE2E_SHIFTLEFT` | 64-bit left shift | +| `TestE2E_SHIFTLEFTCrossWord` | Shift across word boundary | + +#### `codegen/e2e_retypes_test.go` — 6 tests + +| Test | Feature | +|------|---------| +| `TestE2E_RetypesFloat32ToInt` | `VAL INT bits RETYPES x :` (float32 1.0) | +| `TestE2E_RetypesFloat32Zero` | float32 0.0 bit pattern | +| `TestE2E_RetypesFloat32NegOne` | float32 -1.0 bit pattern | +| `TestE2E_RetypesSameNameShadow` | `VAL INT X RETYPES X :` (param rename) | +| `TestE2E_RetypesFloat64ToIntPair` | `VAL [2]INT X RETYPES X :` (float64 1.0) | +| `TestE2E_RetypesFloat64Zero` | float64 0.0 split into two words | + +#### `codegen/e2e_params_test.go` — 6 tests + +| Test | Feature | +|------|---------| +| `TestE2E_ResultQualifier` | `PROC f(RESULT INT x)` | +| `TestE2E_ResultQualifierMultiple` | Multiple RESULT params | +| `TestE2E_FixedSizeArrayParam` | `PROC f([2]INT arr)` → pointer | +| `TestE2E_SharedTypeChanParams` | `PROC f(CHAN OF INT input?, output!)` | +| `TestE2E_SharedTypeIntParams` | `PROC f(VAL INT a, b, INT result)` | +| `TestE2E_ValOpenArrayByteParam` | `PROC f(VAL []BYTE s)` with string arg | + +#### `codegen/e2e_strings_test.go` — 5 tests + +| Test | Feature | +|------|---------| +| `TestE2E_ValByteArrayAbbreviation` | `VAL []BYTE s IS "hello":` | +| `TestE2E_PrintString` | `print.string("hello world")` | +| `TestE2E_PrintNewline` | `print.newline()` | +| `TestE2E_PrintStringAndNewline` | Combined string printing | +| `TestE2E_StringWithEscapes` | Occam `*t` escape in string | + +#### `codegen/e2e_misc_test.go` — 24 tests + +| Test | Feature | +|------|---------| +| `TestE2E_SkipStatement` | SKIP as standalone no-op | +| `TestE2E_SkipInPar` | SKIP in a PAR branch | +| `TestE2E_StopReached` | STOP causes non-zero exit (deadlock) | +| `TestE2E_ModuloOperator` | `\` → `%` | +| `TestE2E_ModuloInExpression` | Modulo in compound expression | +| `TestE2E_AltWithBooleanGuard` | FALSE guard disables ALT branch | +| `TestE2E_AltWithTrueGuard` | TRUE guard enables ALT branch | +| `TestE2E_MostNegReal32` | `MOSTNEG REAL32` is negative | +| `TestE2E_MostPosReal32` | `MOSTPOS REAL32` is positive | +| `TestE2E_MostNegReal64` | `MOSTNEG REAL64` is negative | +| `TestE2E_MostPosReal64` | `MOSTPOS REAL64` is positive | +| `TestE2E_ShorthandSliceFromZero` | `[arr FOR 3]` (FROM 0 implied) | +| `TestE2E_StringToByteSliceWrapping` | String literal → `[]byte()` for `[]BYTE` param | +| `TestE2E_GoReservedWordEscaping` | Variable named `len` works | +| `TestE2E_GoReservedWordByte` | Variable named `byte` works | +| `TestE2E_MultiLineExpression` | Continuation operator at line end | +| `TestE2E_MultiLineParenExpression` | Expression inside parens across lines | +| `TestE2E_NegativeIntLiteral` | Unary minus | +| `TestE2E_NotOperator` | `NOT TRUE` | +| `TestE2E_LogicalAndOr` | `AND` / `OR` operators | +| `TestE2E_NestedIfInSeq` | Nested IF with variable declarations | +| `TestE2E_WhileWithBreakCondition` | WHILE counting to target | +| `TestE2E_CaseWithMultipleArms` | CASE with 4 branches | +| `TestE2E_EqualNotEqual` | `=` and `<>` operators | +| `TestE2E_CompileOnly_StopInProc` | STOP in proc compiles cleanly | +| `TestE2E_NestedReplicatedSeq` | Nested `SEQ i = 0 FOR 3` loops | +| `TestE2E_ArraySliceAssignment` | `[dst FROM 1 FOR 3] := src` | +| `TestE2E_FunctionCallInCondition` | `BOOL FUNCTION` as IF condition | +| `TestE2E_RecursiveFunction` | Recursive factorial | +| `TestE2E_MultiLineProcParams` | Multi-line proc parameter list | +| `TestE2E_VetOutputClean` | `go vet` passes on generated code | + +#### `lexer/lexer_test2_test.go` — 15 tests + +| Test | Feature | +|------|---------| +| `TestAllKeywords` | All 33+ keywords tokenize correctly | +| `TestParenDepthSuppressesIndent` | No INDENT/DEDENT inside `(...)` | +| `TestBracketDepthSuppressesIndent` | No INDENT/DEDENT inside `[...]` | +| `TestContinuationOperator` | `+` at line end joins lines | +| `TestContinuationAND` | `AND` at line end joins lines | +| `TestStringLiteral` | `"hello world"` → STRING token | +| `TestStringEscapeSequences` | `*n` preserved raw by lexer | +| `TestByteLiteralToken` | `'A'` → BYTE_LIT token | +| `TestByteLiteralEscapeToken` | `'*n'` → BYTE_LIT with raw escape | +| `TestSendReceiveTokens` | `!` → SEND, `?` → RECEIVE | +| `TestAmpersandToken` | `&` → AMPERSAND | +| `TestSemicolonToken` | `;` → SEMICOLON | +| `TestNestedParenDepth` | Nested `((` tracks depth correctly | +| `TestMixedParenBracketDepth` | `arr[(1 + 2)]` mixed nesting | +| `TestLineAndColumnTracking` | Token line/column numbers | + +### Summary + +| Area | Before | Added | After | +|------|--------|-------|-------| +| Lexer unit tests | 9 | 15 | 24 | +| Parser unit tests | 80 | 0 | 80 | +| Codegen unit tests | 49 | 0 | 49 | +| E2E tests | ~124 | 54 | ~178 | +| Preprocessor tests | 22 | 0 | 22 | +| Modgen tests | 5 | 0 | 5 | +| **Total** | **~289** | **69** | **~358** | + +### Remaining Gaps + +Features with limited or indirect-only coverage that could benefit from future tests: + +- Parser-level tests for SKIP, STOP, `VAL []BYTE` abbreviation, RESULT qualifier, fixed-size array params, transputer intrinsic calls, variant receive `c ? CASE`, timer ALT arm +- Codegen unit tests for RETYPES output, intrinsic helper emission, sequential/variant protocol send/receive code, `VAL []BYTE` abbreviation output +- `print.nl` (alias for `print.newline`, if supported) +- PRI ALT / PRI PAR (not yet implemented) diff --git a/codegen/e2e_intrinsics_test.go b/codegen/e2e_intrinsics_test.go new file mode 100644 index 0000000..05f60c7 --- /dev/null +++ b/codegen/e2e_intrinsics_test.go @@ -0,0 +1,243 @@ +package codegen + +import "testing" + +func TestE2E_LONGPROD(t *testing.T) { + // LONGPROD(a, b, c) = a*b+c as 64-bit, returns (hi, lo) + // 100000 * 100000 + 0 = 10000000000 + // 10000000000 = 2 * 2^32 + 1410065408 + // hi = 2, lo = 1410065408 + occam := `PROC main() + INT hi, lo: + SEQ + hi, lo := LONGPROD(100000, 100000, 0) + print.int(hi) + print.int(lo) +: +` + output := transpileCompileRun(t, occam) + expected := "2\n1410065408\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LONGPRODWithCarry(t *testing.T) { + // LONGPROD(a, b, carry) = a*b+carry + // 3 * 4 + 5 = 17, fits in lo word + occam := `PROC main() + INT hi, lo: + SEQ + hi, lo := LONGPROD(3, 4, 5) + print.int(hi) + print.int(lo) +: +` + output := transpileCompileRun(t, occam) + expected := "0\n17\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LONGDIV(t *testing.T) { + // LONGDIV(hi, lo, divisor) divides (hi:lo) by divisor → (quotient, remainder) + // (0:42) / 5 = quotient 8, remainder 2 + occam := `PROC main() + INT quot, rem: + SEQ + quot, rem := LONGDIV(0, 42, 5) + print.int(quot) + print.int(rem) +: +` + output := transpileCompileRun(t, occam) + expected := "8\n2\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LONGDIVLargeValue(t *testing.T) { + // (2:1409286144) / 100000 = 10000000000 / 100000 = 100000 + // Use the result from LONGPROD to roundtrip + occam := `PROC main() + INT hi, lo, quot, rem: + SEQ + hi, lo := LONGPROD(100000, 100000, 0) + quot, rem := LONGDIV(hi, lo, 100000) + print.int(quot) + print.int(rem) +: +` + output := transpileCompileRun(t, occam) + expected := "100000\n0\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LONGSUM(t *testing.T) { + // LONGSUM(a, b, carry) = a+b+carry as 64-bit → (carry_out, sum) + // 10 + 20 + 0 = 30, no carry + occam := `PROC main() + INT carry, sum: + SEQ + carry, sum := LONGSUM(10, 20, 0) + print.int(carry) + print.int(sum) +: +` + output := transpileCompileRun(t, occam) + expected := "0\n30\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LONGSUMOverflow(t *testing.T) { + // LONGSUM with overflow using smaller values that fit cleanly in uint32 + // LONGSUM(0xFFFFFFFF, 1, 0): uint32 max + 1 = 0x1_0000_0000 + // hi (carry) = 1, lo = 0 + occam := `PROC main() + INT carry, sum: + SEQ + carry, sum := LONGSUM(-1, 1, 0) + print.int(carry) + print.int(sum) +: +` + output := transpileCompileRun(t, occam) + // uint32(-1) = 0xFFFFFFFF, uint32(1) = 1, sum = 0x100000000 + // carry = 1, sum = 0 + expected := "1\n0\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LONGDIFF(t *testing.T) { + // LONGDIFF(a, b, borrow) = a-b-borrow → (borrow_out, diff) + // 30 - 10 - 0 = 20, no borrow + occam := `PROC main() + INT borrow, diff: + SEQ + borrow, diff := LONGDIFF(30, 10, 0) + print.int(borrow) + print.int(diff) +: +` + output := transpileCompileRun(t, occam) + expected := "0\n20\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LONGDIFFBorrow(t *testing.T) { + // LONGDIFF(10, 30, 0): 10-30 = underflow + // uint32(10) - uint32(30) = wraps → borrow=1 + // uint32 result: 0xFFFFFFEC → int32 = -20 + occam := `PROC main() + INT borrow, diff: + SEQ + borrow, diff := LONGDIFF(10, 30, 0) + print.int(borrow) + print.int(diff) +: +` + output := transpileCompileRun(t, occam) + expected := "1\n-20\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_NORMALISE(t *testing.T) { + // NORMALISE(hi, lo) shifts left until MSB is set + // NORMALISE(0, 1) — value is 1, needs 63 left shifts to set bit 63 + occam := `PROC main() + INT places, nhi, nlo: + SEQ + places, nhi, nlo := NORMALISE(0, 1) + print.int(places) +: +` + output := transpileCompileRun(t, occam) + expected := "63\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_NORMALISEZero(t *testing.T) { + // NORMALISE(0, 0) — zero value returns 64 shifts, (0, 0) + occam := `PROC main() + INT places, nhi, nlo: + SEQ + places, nhi, nlo := NORMALISE(0, 0) + print.int(places) + print.int(nhi) + print.int(nlo) +: +` + output := transpileCompileRun(t, occam) + expected := "64\n0\n0\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_SHIFTRIGHT(t *testing.T) { + // SHIFTRIGHT(hi, lo, n) — shift 64-bit (hi:lo) right by n + // SHIFTRIGHT(0, 16, 2) = shift 16 right by 2 = (0, 4) + occam := `PROC main() + INT rhi, rlo: + SEQ + rhi, rlo := SHIFTRIGHT(0, 16, 2) + print.int(rhi) + print.int(rlo) +: +` + output := transpileCompileRun(t, occam) + expected := "0\n4\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_SHIFTLEFT(t *testing.T) { + // SHIFTLEFT(hi, lo, n) — shift 64-bit (hi:lo) left by n + // SHIFTLEFT(0, 1, 4) = shift 1 left by 4 = (0, 16) + occam := `PROC main() + INT rhi, rlo: + SEQ + rhi, rlo := SHIFTLEFT(0, 1, 4) + print.int(rhi) + print.int(rlo) +: +` + output := transpileCompileRun(t, occam) + expected := "0\n16\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_SHIFTLEFTCrossWord(t *testing.T) { + // Shift a value from lo into hi word + // SHIFTLEFT(0, 1, 32) = (1, 0) — bit moves from lo to hi + occam := `PROC main() + INT rhi, rlo: + SEQ + rhi, rlo := SHIFTLEFT(0, 1, 32) + print.int(rhi) + print.int(rlo) +: +` + output := transpileCompileRun(t, occam) + expected := "1\n0\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} diff --git a/codegen/e2e_misc_test.go b/codegen/e2e_misc_test.go new file mode 100644 index 0000000..1553fa4 --- /dev/null +++ b/codegen/e2e_misc_test.go @@ -0,0 +1,682 @@ +package codegen + +import ( + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + "github.com/codeassociates/occam2go/lexer" + "github.com/codeassociates/occam2go/parser" +) + +func TestE2E_SkipStatement(t *testing.T) { + // SKIP as a standalone statement — should be a no-op + occam := `SEQ + print.int(1) + SKIP + print.int(2) +` + output := transpileCompileRun(t, occam) + expected := "1\n2\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_SkipInPar(t *testing.T) { + // SKIP in a PAR branch — one branch does nothing + occam := `SEQ + INT x: + PAR + SKIP + x := 42 + print.int(x) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_StopReached(t *testing.T) { + // STOP should print an error message to stderr and halt (deadlock via select{}) + // We verify the program exits with non-zero status and prints to stderr + occamSource := `SEQ + STOP +` + l := lexer.New(occamSource) + p := parser.New(l) + program := p.ParseProgram() + + if len(p.Errors()) > 0 { + for _, err := range p.Errors() { + t.Errorf("parser error: %s", err) + } + t.FailNow() + } + + gen := New() + goCode := gen.Generate(program) + + tmpDir, err := os.MkdirTemp("", "occam2go-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + goFile := filepath.Join(tmpDir, "main.go") + if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil { + t.Fatalf("failed to write Go file: %v", err) + } + + binFile := filepath.Join(tmpDir, "main") + compileCmd := exec.Command("go", "build", "-o", binFile, goFile) + compileOutput, err := compileCmd.CombinedOutput() + if err != nil { + t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, compileOutput, goCode) + } + + // Run with a timeout — STOP causes a deadlock (select{}) + runCmd := exec.Command(binFile) + err = runCmd.Start() + if err != nil { + t.Fatalf("failed to start: %v", err) + } + + // The program should deadlock, so we just verify it compiles and starts. + // Kill it after a short delay. + done := make(chan error, 1) + go func() { + done <- runCmd.Wait() + }() + + select { + case err := <-done: + // If it exited, it should be non-zero (fatal error: all goroutines are asleep) + if err == nil { + t.Errorf("expected STOP to cause non-zero exit, but exited successfully") + } + case <-func() <-chan struct{} { + ch := make(chan struct{}) + go func() { + // Wait 2 seconds then signal + exec.Command("sleep", "0.5").Run() + close(ch) + }() + return ch + }(): + // Expected: program is stuck in select{}, kill it + runCmd.Process.Kill() + } +} + +func TestE2E_ModuloOperator(t *testing.T) { + // \ is the modulo operator in occam, maps to % in Go + occam := `SEQ + INT x: + x := 42 \ 5 + print.int(x) +` + output := transpileCompileRun(t, occam) + expected := "2\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_ModuloInExpression(t *testing.T) { + // Modulo used in a larger expression + occam := `SEQ + INT x: + x := (17 \ 5) + (10 \ 3) + print.int(x) +` + output := transpileCompileRun(t, occam) + // 17 % 5 = 2, 10 % 3 = 1, sum = 3 + expected := "3\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_AltWithBooleanGuard(t *testing.T) { + // ALT with boolean guard: FALSE guard disables a channel + // Only send on c2 since c1's guard is FALSE and won't be selected + occam := `SEQ + CHAN OF INT c1: + CHAN OF INT c2: + INT result: + BOOL allow: + allow := FALSE + PAR + c2 ! 42 + ALT + allow & c1 ? result + SKIP + TRUE & c2 ? result + SKIP + print.int(result) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_AltWithTrueGuard(t *testing.T) { + // ALT where guard evaluates to TRUE for the first channel + occam := `SEQ + CHAN OF INT c: + INT result: + PAR + c ! 99 + ALT + TRUE & c ? result + print.int(result) +` + output := transpileCompileRun(t, occam) + expected := "99\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_MostNegReal32(t *testing.T) { + // MOSTNEG REAL32 → -math.MaxFloat32 (a very large negative number) + occam := `SEQ + REAL32 x: + x := MOSTNEG REAL32 + IF + x < (REAL32 0) + print.int(1) + TRUE + print.int(0) +` + output := transpileCompileRun(t, occam) + expected := "1\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_MostPosReal32(t *testing.T) { + // MOSTPOS REAL32 → math.MaxFloat32 + occam := `SEQ + REAL32 x: + x := MOSTPOS REAL32 + IF + x > (REAL32 0) + print.int(1) + TRUE + print.int(0) +` + output := transpileCompileRun(t, occam) + expected := "1\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_MostNegReal64(t *testing.T) { + // MOSTNEG REAL64 → -math.MaxFloat64 + occam := `SEQ + REAL64 x: + x := MOSTNEG REAL64 + IF + x < (REAL64 0) + print.int(1) + TRUE + print.int(0) +` + output := transpileCompileRun(t, occam) + expected := "1\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_MostPosReal64(t *testing.T) { + // MOSTPOS REAL64 → math.MaxFloat64 + occam := `SEQ + REAL64 x: + x := MOSTPOS REAL64 + IF + x > (REAL64 0) + print.int(1) + TRUE + print.int(0) +` + output := transpileCompileRun(t, occam) + expected := "1\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_ShorthandSliceFromZero(t *testing.T) { + // [arr FOR m] — shorthand for [arr FROM 0 FOR m] + occam := `SEQ + [5]INT arr: + SEQ i = 0 FOR 5 + arr[i] := i * 10 + INT sum: + sum := 0 + VAL first3 IS [arr FOR 3]: + SEQ i = 0 FOR 3 + sum := sum + first3[i] + print.int(sum) +` + output := transpileCompileRun(t, occam) + // 0 + 10 + 20 = 30 + expected := "30\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_StringToByteSliceWrapping(t *testing.T) { + // When passing a string literal to a []BYTE param, it should wrap with []byte() + occam := `PROC first.char(VAL []BYTE s, INT result) + result := INT s[0] +: + +SEQ + INT ch: + first.char("hello", ch) + print.int(ch) +` + output := transpileCompileRun(t, occam) + // 'h' = 104 + expected := "104\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_GoReservedWordEscaping(t *testing.T) { + // Test that occam identifiers matching Go reserved words are escaped + // e.g., a variable named "string" should work + occam := `SEQ + INT len: + len := 42 + print.int(len) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_GoReservedWordByte(t *testing.T) { + // "byte" is a Go reserved word — should be escaped to _byte + occam := `SEQ + INT byte: + byte := 99 + print.int(byte) +` + output := transpileCompileRun(t, occam) + expected := "99\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_MultiLineExpression(t *testing.T) { + // Multi-line expression with continuation operator at end of line + occam := `SEQ + INT x: + x := 10 + + 20 + + 12 + print.int(x) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_MultiLineParenExpression(t *testing.T) { + // Expression spanning multiple lines inside parentheses + occam := `SEQ + INT x: + x := (10 + + 20 + + 12) + print.int(x) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_NegativeIntLiteral(t *testing.T) { + // Negative integer literals (unary minus) + occam := `SEQ + INT x: + x := -42 + print.int(x) +` + output := transpileCompileRun(t, occam) + expected := "-42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_NotOperator(t *testing.T) { + // NOT boolean operator + occam := `SEQ + BOOL x: + x := NOT TRUE + print.bool(x) +` + output := transpileCompileRun(t, occam) + expected := "false\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_LogicalAndOr(t *testing.T) { + // AND / OR operators + occam := `SEQ + BOOL a, b: + a := TRUE AND FALSE + b := TRUE OR FALSE + print.bool(a) + print.bool(b) +` + output := transpileCompileRun(t, occam) + expected := "false\ntrue\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_NestedIfInSeq(t *testing.T) { + // Nested IF inside SEQ with variable declarations + occam := `SEQ + INT x: + x := 5 + INT y: + y := 0 + IF + x > 3 + IF + x < 10 + y := 1 + TRUE + y := 2 + TRUE + y := 3 + print.int(y) +` + output := transpileCompileRun(t, occam) + expected := "1\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_WhileWithBreakCondition(t *testing.T) { + // WHILE loop counting to a target + occam := `SEQ + INT sum, i: + sum := 0 + i := 1 + WHILE i <= 10 + SEQ + sum := sum + i + i := i + 1 + print.int(sum) +` + output := transpileCompileRun(t, occam) + // 1+2+...+10 = 55 + expected := "55\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_CaseWithMultipleArms(t *testing.T) { + // CASE with several branches + occam := `SEQ + INT x, result: + x := 3 + CASE x + 1 + result := 10 + 2 + result := 20 + 3 + result := 30 + ELSE + result := 0 + print.int(result) +` + output := transpileCompileRun(t, occam) + expected := "30\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_EqualNotEqual(t *testing.T) { + // = and <> operators + occam := `SEQ + print.bool(5 = 5) + print.bool(5 <> 3) + print.bool(5 = 3) + print.bool(5 <> 5) +` + output := transpileCompileRun(t, occam) + expected := "true\ntrue\nfalse\nfalse\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_CompileOnly_StopInProc(t *testing.T) { + // STOP inside a proc — just verify it compiles (don't run, it would deadlock) + occamSource := `PROC fatal() + STOP +: + +SEQ + print.int(42) +` + l := lexer.New(occamSource) + p := parser.New(l) + program := p.ParseProgram() + + if len(p.Errors()) > 0 { + for _, err := range p.Errors() { + t.Errorf("parser error: %s", err) + } + t.FailNow() + } + + gen := New() + goCode := gen.Generate(program) + + tmpDir, err := os.MkdirTemp("", "occam2go-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + goFile := filepath.Join(tmpDir, "main.go") + if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil { + t.Fatalf("failed to write Go file: %v", err) + } + + // Just check it compiles + compileCmd := exec.Command("go", "vet", goFile) + compileOutput, err := compileCmd.CombinedOutput() + if err != nil { + t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, compileOutput, goCode) + } +} + +func TestE2E_NestedReplicatedSeq(t *testing.T) { + // Nested replicated SEQ — matrix-like access + occam := `SEQ + INT sum: + sum := 0 + SEQ i = 0 FOR 3 + SEQ j = 0 FOR 3 + sum := sum + ((i * 3) + j) + print.int(sum) +` + output := transpileCompileRun(t, occam) + // 0+1+2+3+4+5+6+7+8 = 36 + expected := "36\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_ArraySliceAssignment(t *testing.T) { + // [arr FROM n FOR m] := src — copy slice + occam := `SEQ + [5]INT dst: + [3]INT src: + SEQ i = 0 FOR 5 + dst[i] := 0 + src[0] := 10 + src[1] := 20 + src[2] := 30 + [dst FROM 1 FOR 3] := src + SEQ i = 0 FOR 5 + print.int(dst[i]) +` + output := transpileCompileRun(t, occam) + expected := "0\n10\n20\n30\n0\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_FunctionCallInCondition(t *testing.T) { + // Function call used as condition in IF + occam := `BOOL FUNCTION is.positive(VAL INT x) + IS x > 0 + +SEQ + IF + is.positive(42) + print.int(1) + TRUE + print.int(0) +` + output := transpileCompileRun(t, occam) + expected := "1\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_RecursiveFunction(t *testing.T) { + // Recursive function (factorial) + occam := `INT FUNCTION factorial(VAL INT n) + INT result: + VALOF + IF + n <= 1 + result := 1 + TRUE + result := n * factorial(n - 1) + RESULT result + +SEQ + print.int(factorial(5)) +` + output := transpileCompileRun(t, occam) + expected := "120\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_MultiLineProcParams(t *testing.T) { + // Procedure with parameters spanning multiple lines (paren suppression) + occam := `PROC add( + VAL INT a, + VAL INT b, + INT result) + result := a + b +: + +SEQ + INT r: + add(10, 32, r) + print.int(r) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_VetOutputClean(t *testing.T) { + // Verify go vet passes on generated code for a non-trivial program + occamSource := `PROC compute(VAL INT n) + INT x: + PROC helper() + x := n * 2 + : + SEQ + helper() + print.int(x) +: + +SEQ + compute(21) +` + l := lexer.New(occamSource) + p := parser.New(l) + program := p.ParseProgram() + + if len(p.Errors()) > 0 { + for _, err := range p.Errors() { + t.Errorf("parser error: %s", err) + } + t.FailNow() + } + + gen := New() + goCode := gen.Generate(program) + + tmpDir, err := os.MkdirTemp("", "occam2go-test-*") + if err != nil { + t.Fatalf("failed to create temp dir: %v", err) + } + defer os.RemoveAll(tmpDir) + + goFile := filepath.Join(tmpDir, "main.go") + if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil { + t.Fatalf("failed to write Go file: %v", err) + } + + vetCmd := exec.Command("go", "vet", goFile) + vetOutput, err := vetCmd.CombinedOutput() + if err != nil { + t.Fatalf("go vet failed: %v\nOutput: %s\nGo code:\n%s", err, vetOutput, goCode) + } + + // Also verify it runs correctly + output := transpileCompileRun(t, occamSource) + if strings.TrimSpace(output) != "42" { + t.Errorf("expected 42, got %q", output) + } +} diff --git a/codegen/e2e_params_test.go b/codegen/e2e_params_test.go new file mode 100644 index 0000000..58f6308 --- /dev/null +++ b/codegen/e2e_params_test.go @@ -0,0 +1,128 @@ +package codegen + +import "testing" + +func TestE2E_ResultQualifier(t *testing.T) { + // RESULT INT x is semantically the same as non-VAL (pointer param) + occam := `PROC compute(VAL INT a, VAL INT b, RESULT INT sum) + sum := a + b +: + +SEQ + INT s: + compute(10, 32, s) + print.int(s) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_ResultQualifierMultiple(t *testing.T) { + // Multiple RESULT params + occam := `PROC divmod(VAL INT a, VAL INT b, RESULT INT quot, RESULT INT rem) + SEQ + quot := a / b + rem := a \ b +: + +SEQ + INT q, r: + divmod(42, 5, q, r) + print.int(q) + print.int(r) +` + output := transpileCompileRun(t, occam) + expected := "8\n2\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_FixedSizeArrayParam(t *testing.T) { + // [2]INT param → pointer to fixed-size array + occam := `PROC swap([2]INT arr) + INT tmp: + SEQ + tmp := arr[0] + arr[0] := arr[1] + arr[1] := tmp +: + +SEQ + [2]INT pair: + pair[0] := 10 + pair[1] := 20 + swap(pair) + print.int(pair[0]) + print.int(pair[1]) +` + output := transpileCompileRun(t, occam) + expected := "20\n10\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_SharedTypeChanParams(t *testing.T) { + // PROC f(CHAN OF INT a?, b?) — type applies to both a and b + occam := `PROC relay(CHAN OF INT input?, output!) + INT x: + SEQ + input ? x + output ! x +: + +SEQ + CHAN OF INT c1: + CHAN OF INT c2: + INT result: + PAR + c1 ! 42 + relay(c1, c2) + SEQ + c2 ? result + print.int(result) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_SharedTypeIntParams(t *testing.T) { + // PROC f(VAL INT a, b) — type applies to both a and b + occam := `PROC add(VAL INT a, b, INT result) + result := a + b +: + +SEQ + INT r: + add(10, 32, r) + print.int(r) +` + output := transpileCompileRun(t, occam) + expected := "42\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_ValOpenArrayByteParam(t *testing.T) { + // VAL []BYTE param with string literal → wraps with []byte() + occam := `PROC show.length(VAL []BYTE s) + print.int(SIZE s) +: + +SEQ + show.length("hello") +` + output := transpileCompileRun(t, occam) + expected := "5\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} diff --git a/codegen/e2e_retypes_test.go b/codegen/e2e_retypes_test.go new file mode 100644 index 0000000..e2885a2 --- /dev/null +++ b/codegen/e2e_retypes_test.go @@ -0,0 +1,121 @@ +package codegen + +import "testing" + +func TestE2E_RetypesFloat32ToInt(t *testing.T) { + // VAL INT X RETYPES X : where X is a REAL32 parameter + // Reinterpret float32(1.0) as int → IEEE 754: 0x3F800000 = 1065353216 + occam := `PROC show.bits(VAL REAL32 x) + VAL INT bits RETYPES x : + SEQ + print.int(bits) +: + +SEQ + show.bits(REAL32 1) +` + output := transpileCompileRun(t, occam) + expected := "1065353216\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_RetypesFloat32Zero(t *testing.T) { + // float32(0.0) → bits = 0 + occam := `PROC show.bits(VAL REAL32 x) + VAL INT bits RETYPES x : + SEQ + print.int(bits) +: + +SEQ + show.bits(REAL32 0) +` + output := transpileCompileRun(t, occam) + expected := "0\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_RetypesFloat32NegOne(t *testing.T) { + // float32(-1.0) → IEEE 754: 0xBF800000 = -1082130432 (as signed int32) + occam := `PROC show.bits(VAL REAL32 x) + VAL INT bits RETYPES x : + SEQ + print.int(bits) +: + +SEQ + REAL32 v: + v := REAL32 1 + v := REAL32 0 - v + show.bits(v) +` + output := transpileCompileRun(t, occam) + expected := "-1082130432\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_RetypesSameNameShadow(t *testing.T) { + // The classic pattern: VAL INT X RETYPES X : where param is also named X + // Tests the RETYPES parameter rename mechanism (_rp_X) + occam := `PROC bits.of(VAL REAL32 X) + VAL INT X RETYPES X : + SEQ + print.int(X) +: + +SEQ + bits.of(REAL32 2) +` + // float32(2.0) = 0x40000000 = 1073741824 + output := transpileCompileRun(t, occam) + expected := "1073741824\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_RetypesFloat64ToIntPair(t *testing.T) { + // VAL [2]INT X RETYPES X : reinterpret float64 as two int32 words + // float64(1.0) = 0x3FF0000000000000 + // lo = 0x00000000 = 0, hi = 0x3FF00000 = 1072693248 + occam := `PROC show.bits64(VAL REAL64 X) + VAL [2]INT X RETYPES X : + SEQ + print.int(X[0]) + print.int(X[1]) +: + +SEQ + show.bits64(REAL64 1) +` + output := transpileCompileRun(t, occam) + expected := "0\n1072693248\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_RetypesFloat64Zero(t *testing.T) { + // float64(0.0) → both words should be 0 + occam := `PROC show.bits64(VAL REAL64 X) + VAL [2]INT X RETYPES X : + SEQ + print.int(X[0]) + print.int(X[1]) +: + +SEQ + show.bits64(REAL64 0) +` + output := transpileCompileRun(t, occam) + expected := "0\n0\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} diff --git a/codegen/e2e_strings_test.go b/codegen/e2e_strings_test.go new file mode 100644 index 0000000..b36f2cd --- /dev/null +++ b/codegen/e2e_strings_test.go @@ -0,0 +1,67 @@ +package codegen + +import "testing" + +func TestE2E_ValByteArrayAbbreviation(t *testing.T) { + // VAL []BYTE s IS "hello": — open array byte abbreviation + occam := `SEQ + VAL []BYTE s IS "hello": + print.int(SIZE s) +` + output := transpileCompileRun(t, occam) + expected := "5\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_PrintString(t *testing.T) { + // print.string should output the string content + occam := `SEQ + print.string("hello world") +` + output := transpileCompileRun(t, occam) + expected := "hello world\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_PrintNewline(t *testing.T) { + // print.newline should output a blank line + occam := `SEQ + print.int(1) + print.newline() + print.int(2) +` + output := transpileCompileRun(t, occam) + expected := "1\n\n2\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_PrintStringAndNewline(t *testing.T) { + // Combined usage of print.string and print.newline + occam := `SEQ + print.string("first") + print.string("second") +` + output := transpileCompileRun(t, occam) + expected := "first\nsecond\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} + +func TestE2E_StringWithEscapes(t *testing.T) { + // Occam escape sequences in string: *n = newline, *t = tab + occam := `SEQ + print.string("a*tb") +` + output := transpileCompileRun(t, occam) + expected := "a\tb\n" + if output != expected { + t.Errorf("expected %q, got %q", expected, output) + } +} diff --git a/lexer/lexer_test2_test.go b/lexer/lexer_test2_test.go new file mode 100644 index 0000000..98c81ac --- /dev/null +++ b/lexer/lexer_test2_test.go @@ -0,0 +1,305 @@ +package lexer + +import "testing" + +func TestAllKeywords(t *testing.T) { + // Test all keywords that aren't covered in TestKeywords + // Note: AND and OR are continuation operators, so they can't be at line end + // (the lexer would suppress the following NEWLINE). Put them mid-line. + input := "CASE ELSE FUNC FUNCTION VALOF RESULT IS CHAN OF SKIP STOP VAL PROTOCOL RECORD SIZE STEP MOSTNEG MOSTPOS INITIAL RETYPES PLUS MINUS TIMES TIMER AFTER FOR FROM REAL REAL32 REAL64 NOT AND OR WHILE\n" + expected := []TokenType{ + CASE, ELSE, FUNC, FUNCTION, VALOF, RESULT, IS, CHAN, OF, + SKIP, STOP, VAL, PROTOCOL, RECORD, SIZE_KW, STEP, + MOSTNEG_KW, MOSTPOS_KW, INITIAL, RETYPES, + PLUS_KW, MINUS_KW, TIMES, TIMER, AFTER, FOR, FROM, + REAL_TYPE, REAL32_TYPE, REAL64_TYPE, + NOT, AND, OR, WHILE, + NEWLINE, EOF, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("tests[%d] - expected=%q, got=%q (literal=%q)", + i, exp, tok.Type, tok.Literal) + } + } +} + +func TestParenDepthSuppressesIndent(t *testing.T) { + // Inside parentheses, INDENT/DEDENT/NEWLINE should be suppressed + input := `x := (1 + + 2 + + 3) +` + expected := []TokenType{ + IDENT, // x + ASSIGN, // := + LPAREN, // ( + INT, // 1 + PLUS, // + + INT, // 2 + PLUS, // + + INT, // 3 + RPAREN, // ) + NEWLINE, // after closing paren + EOF, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("paren[%d] - expected=%q, got=%q (literal=%q)", + i, exp, tok.Type, tok.Literal) + } + } +} + +func TestBracketDepthSuppressesIndent(t *testing.T) { + // Inside brackets, INDENT/DEDENT/NEWLINE should be suppressed + input := `x := [1, + 2, + 3] +` + expected := []TokenType{ + IDENT, // x + ASSIGN, // := + LBRACKET, // [ + INT, // 1 + COMMA, // , + INT, // 2 + COMMA, // , + INT, // 3 + RBRACKET, // ] + NEWLINE, // after closing bracket + EOF, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("bracket[%d] - expected=%q, got=%q (literal=%q)", + i, exp, tok.Type, tok.Literal) + } + } +} + +func TestContinuationOperator(t *testing.T) { + // Operator at end of line causes NEWLINE/INDENT/DEDENT suppression on next line + input := `x := a + + b +` + expected := []TokenType{ + IDENT, // x + ASSIGN, // := + IDENT, // a + PLUS, // + + IDENT, // b + NEWLINE, // after b + EOF, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("continuation[%d] - expected=%q, got=%q (literal=%q)", + i, exp, tok.Type, tok.Literal) + } + } +} + +func TestContinuationAND(t *testing.T) { + // AND at end of line should continue + input := `(x > 0) AND + (x < 10) +` + expected := []TokenType{ + LPAREN, IDENT, GT, INT, RPAREN, // (x > 0) + AND, // AND + LPAREN, IDENT, LT, INT, RPAREN, // (x < 10) + NEWLINE, + EOF, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("cont_and[%d] - expected=%q, got=%q (literal=%q)", + i, exp, tok.Type, tok.Literal) + } + } +} + +func TestStringLiteral(t *testing.T) { + input := `"hello world"` + "\n" + l := New(input) + tok := l.NextToken() + if tok.Type != STRING { + t.Fatalf("expected STRING, got %q", tok.Type) + } + if tok.Literal != "hello world" { + t.Fatalf("expected literal %q, got %q", "hello world", tok.Literal) + } +} + +func TestStringEscapeSequences(t *testing.T) { + // The lexer preserves raw occam escapes (*n, *t, etc.) in string literals. + // Escape conversion (*n → \n) happens in the parser, not the lexer. + input := `"a*nb"` + "\n" + l := New(input) + tok := l.NextToken() + if tok.Type != STRING { + t.Fatalf("expected STRING, got %q", tok.Type) + } + if tok.Literal != "a*nb" { + t.Fatalf("expected literal %q, got %q", "a*nb", tok.Literal) + } +} + +func TestByteLiteralToken(t *testing.T) { + input := "'A'\n" + l := New(input) + tok := l.NextToken() + if tok.Type != BYTE_LIT { + t.Fatalf("expected BYTE_LIT, got %q (literal=%q)", tok.Type, tok.Literal) + } + if tok.Literal != "A" { + t.Fatalf("expected literal %q, got %q", "A", tok.Literal) + } +} + +func TestByteLiteralEscapeToken(t *testing.T) { + // The lexer preserves raw occam escape (*n) in byte literals. + // Escape conversion happens in the parser. + input := "'*n'\n" + l := New(input) + tok := l.NextToken() + if tok.Type != BYTE_LIT { + t.Fatalf("expected BYTE_LIT, got %q (literal=%q)", tok.Type, tok.Literal) + } + if tok.Literal != "*n" { + t.Fatalf("expected literal %q, got %q", "*n", tok.Literal) + } +} + +func TestSendReceiveTokens(t *testing.T) { + input := "c ! 42\nc ? x\n" + expected := []struct { + typ TokenType + lit string + }{ + {IDENT, "c"}, + {SEND, "!"}, + {INT, "42"}, + {NEWLINE, "\\n"}, + {IDENT, "c"}, + {RECEIVE, "?"}, + {IDENT, "x"}, + {NEWLINE, "\\n"}, + {EOF, ""}, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp.typ { + t.Fatalf("send_recv[%d] - expected type=%q, got=%q (literal=%q)", + i, exp.typ, tok.Type, tok.Literal) + } + } +} + +func TestAmpersandToken(t *testing.T) { + // & used as guard separator in ALT + input := "TRUE & c ? x\n" + expected := []TokenType{TRUE, AMPERSAND, IDENT, RECEIVE, IDENT, NEWLINE, EOF} + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("ampersand[%d] - expected=%q, got=%q", i, exp, tok.Type) + } + } +} + +func TestSemicolonToken(t *testing.T) { + input := "c ! 10 ; 20\n" + expected := []TokenType{IDENT, SEND, INT, SEMICOLON, INT, NEWLINE, EOF} + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("semicolon[%d] - expected=%q, got=%q", i, exp, tok.Type) + } + } +} + +func TestNestedParenDepth(t *testing.T) { + // Nested parens: depth tracks correctly + input := `x := ((1 + + 2) + + 3) +` + expected := []TokenType{ + IDENT, ASSIGN, + LPAREN, LPAREN, INT, + PLUS, INT, RPAREN, + PLUS, INT, RPAREN, + NEWLINE, EOF, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("nested_paren[%d] - expected=%q, got=%q (literal=%q)", + i, exp, tok.Type, tok.Literal) + } + } +} + +func TestMixedParenBracketDepth(t *testing.T) { + // Mix of parens and brackets, both should suppress indent + input := `x := arr[(1 + + 2)] +` + expected := []TokenType{ + IDENT, ASSIGN, + IDENT, LBRACKET, LPAREN, INT, + PLUS, INT, RPAREN, RBRACKET, + NEWLINE, EOF, + } + + l := New(input) + for i, exp := range expected { + tok := l.NextToken() + if tok.Type != exp { + t.Fatalf("mixed[%d] - expected=%q, got=%q (literal=%q)", + i, exp, tok.Type, tok.Literal) + } + } +} + +func TestLineAndColumnTracking(t *testing.T) { + input := "INT x:\nx := 5\n" + l := New(input) + + // INT at line 1, col 1 + tok := l.NextToken() + if tok.Line != 1 || tok.Column != 1 { + t.Errorf("INT: expected line=1 col=1, got line=%d col=%d", tok.Line, tok.Column) + } + + // x at line 1, col 5 + tok = l.NextToken() + if tok.Line != 1 || tok.Column != 5 { + t.Errorf("x: expected line=1 col=5, got line=%d col=%d", tok.Line, tok.Column) + } +}