Claude Code transcript - page 1/2

In a previous session you were investigating an infinite loop problem observed while transpiling the "course" module from the kroc project (cloned locally in ./kroc). You made some parser changes aimed at resolving the problem but after those changes the problem, or some variant of the problem remained. It's tricky to diagnose because rather than emit an error message, the process seems to consume memory until the system itself becomes unstable. Therefore I recommend treading carefully while diagnosing the issue.

Thinking

Let me understand the situation. There's an infinite loop problem when transpiling a "course" module from the kroc project. The process consumes memory until the system becomes unstable. I need to investigate this carefully.

Let me start by understanding: 1. What the "course" module is and where it is 2. What changes were made previously 3. What's causing the infinite loop

Let me first look at the current state of things.

Let me start by understanding the current state - what files are involved and what previous changes were made.

Task
Explore course module and recent changes
{
  "subagent_type": "Explore",
  "prompt": "I need to understand the context for an infinite loop bug in an occam-to-Go transpiler. Please:\n\n1. Find the \"course\" module files in ./kroc - look for course.occ or similar files\n2. Check recent git log for any parser changes related to infinite loops\n3. Look at any .module files related to \"course\" in the project\n4. Check if there are any test files or scripts that reference the course module transpilation\n\nBe thorough - check multiple locations and naming conventions.",
  "model": "haiku"
}
[
  {
    "type": "text",
    "text": "Perfect! Now I have all the information needed. Let me create a comprehensive summary report.\n\n## Summary Report: Infinite Loop Bug Fix in occam2go\n\n### Context Overview\n\nThe occam2go transpiler encountered an **infinite loop (Out of Memory) bug** when parsing occam programs with **scoped declarations inside branch bodies** for control flow constructs (IF/CASE/WHILE/ALT). This bug affected transpilation of the KRoC course module, particularly procedures like those in `demo_cycles.occ` and `utils.occ` which use control flow structures with nested declarations.\n\n---\n\n### The Course Module Context\n\n**Location:** `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/`\n\nThe course module provides educational occam utilities and contains:\n- **course.module** - Module definition file\n- **demo_cycles.occ** - Process network demonstrations with WHILE TRUE loops:\n  - `id()` - Identity process\n  - `succ()` - Successor process  \n  - `plus()` - Addition network\n  - `delta()` - Duplication network\n  - `prefix()` - Prefix generator\n  - `tail()` - Tail dropper\n  \n- **demo_nets.occ** - Network compositions (numbers, integrate, fibonacci, etc.)\n- **utils.occ** - String/character output utilities with complex IF/CASE structures containing scoped declarations\n- **random.occ** - Random number generation\n- **file_in.occ** - File input utilities\n- **float_io.occ** - Floating-point I/O\n- **consts.inc** - ASCII constants\n\n### The Bug - Root Cause\n\n**File:** `/home/david/projects/code-associates/occam2go/parser/parser.go`\n\n**Problem Pattern:** When parsing control flow bodies (IF choices, CASE branches, WHILE, ALT cases), the parser only called `parseStatement()` **once** per body:\n\n```go\n// OLD CODE - BUGGY\nchoice.Body = p.parseStatement()  // Only parsed first statement!\n\n// Tried to skip remaining statements with crude loop:\nfor !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n    p.nextToken()  // Just skips tokens, doesn't actually parse them\n}\n```\n\n**Problem Scenario:** When an occam program had:\n```occam\nIF\n  x > 0\n    INT y:         -- Scoped declaration (first statement)\n    y := 42        -- Second statement\n    print.int(y)   -- Third statement\n  TRUE\n    SKIP\n```\n\nThe parser would:\n1. Parse `INT y:` (first statement) ✓\n2. Try to skip remaining statements with the crude loop\n3. Get stuck in an infinite loop because the token stream isn't properly advanced through all statements\n4. Eventually run out of memory (OOM)\n\n### The Fix - Commit b088adc\n\n**Date:** Feb 19, 2026  \n**Title:** \"Support multi-statement bodies in IF/CASE/WHILE/ALT and call-site channel direction\"\n\n#### Key Changes:\n\n**1. AST Changes** (`ast/ast.go`):\nChanged all body fields from single `Statement` to `[]Statement`:\n\n```go\n// OLD: WhileLoop, IfChoice, CaseChoice, AltCase\nBody      Statement\n\n// NEW:\nBody      []Statement\n```\n\nAffected structures:\n- `WhileLoop.Body`\n- `IfChoice.Body`\n- `CaseChoice.Body`\n- `AltCase.Body`\n\n**2. Parser Changes** (`parser/parser.go`):\n\nIntroduced **`parseBodyStatements()`** function (lines 1696-1752):\n\n```go\nfunc (p *Parser) parseBodyStatements() []ast.Statement {\n    var statements []ast.Statement\n    startLevel := p.indentLevel\n    \n    for !p.curTokenIs(lexer.EOF) {\n        // Skip newlines\n        for p.curTokenIs(lexer.NEWLINE) {\n            p.nextToken()\n        }\n        \n        // Handle DEDENT tokens\n        for p.curTokenIs(lexer.DEDENT) {\n            if p.indentLevel < startLevel {\n                return statements\n            }\n            p.nextToken()\n        }\n        \n        // Skip any more newlines after DEDENT\n        for p.curTokenIs(lexer.NEWLINE) {\n            p.nextToken()\n        }\n        \n        if p.curTokenIs(lexer.EOF) {\n            break\n        }\n        \n        if p.indentLevel < startLevel {\n            break\n        }\n        \n        // Safety guard: record position before parsing\n        prevToken := p.curToken\n        prevPeek := p.peekToken\n        \n        stmt := p.parseStatement()\n        if stmt != nil {\n            statements = append(statements, stmt)\n        }\n        \n        // Advance past the last token\n        if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n            p.nextToken()\n        }\n        \n        // NO-PROGRESS GUARD: prevent infinite loop\n        if p.curToken == prevToken && p.peekToken == prevPeek {\n            break\n        }\n    }\n    \n    return statements\n}\n```\n\n**Key Features:**\n- Properly parses **all statements** at the body's indentation level\n- Uses `startLevel` pattern to distinguish nested DEDENTs from block-ending DEDENTs\n- Includes **no-progress safety guard** to detect and break from infinite loops\n- Returns slice of statements instead of single statement\n\n**3. Updated Function Calls:**\n\nAll parsing functions updated to use `parseBodyStatements()`:\n- `parseWhileLoop()` - line 2211\n- `parseIfStatement()` - line 2283  \n- `parseCaseStatement()` - replaced loops\n- `parseAltCase()` - line 1638\n\n**4. Codegen Changes** (`codegen/codegen.go`):\n\nUpdated all statement generators to iterate over body statements:\n\n```go\n// OLD: generateWhileLoop\nif loop.Body != nil {\n    g.generateStatement(loop.Body)\n}\n\n// NEW:\nfor _, s := range loop.Body {\n    g.generateStatement(s)\n}\n```\n\nUpdated in:\n- `generateWhileLoop()` - line 1543\n- `generateIfStatement()` - line 1591, 1614\n- `generateCaseStatement()` - line 1645\n- `generateAltBlock()` - line 1292\n\nAlso updated scanner functions:\n- `containsPar()` - lines 198-226\n- `containsPrint()` - lines 266-290\n- `containsTimer()` - lines 325-376\n- `containsStop()` - lines 405-449\n\n**5. Channel Direction at Call Sites** (bonus fix):\n\nAdded support for channel direction annotations at call sites:\n```go\n// Now accepts: foo(out!, in?)\n// The direction annotations (! and ?) are parsed but ignored in codegen\nif p.peekTokenIs(lexer.SEND) || p.peekTokenIs(lexer.RECEIVE) {\n    p.nextToken()\n}\n```\n\n**6. Tests Added** (`codegen/e2e_control_test.go`):\n\nNew end-to-end tests:\n- `TestE2E_MultiStatementIfBody()` - Lines 205-222\n- `TestE2E_MultiStatementCaseBody()` - Lines 224-243\n- `TestE2E_MultiStatementWhileBody()` - Lines 245-260\n- `TestE2E_ChannelDirAtCallSite()` - Lines 262-284\n\n**7. Parser Tests** (`parser/parser_test.go`):\n\nNew unit tests:\n- `TestMultiStatementIfBody()` - Validates parsing of scoped declarations in IF bodies\n- `TestChannelDirAtCallSite()` - Validates call-site channel direction parsing\n\n---\n\n### Affected Course Module Code\n\nThe fix enables proper transpilation of procedures like:\n\n**In demo_cycles.occ (WHILE TRUE patterns):**\n```occam\nPROC id (CHAN INT in?, out!)\n  WHILE TRUE\n    INT x:\n    SEQ\n      in ? x\n      out ! x\n:\n```\n\n**In utils.occ (Complex IF/CASE structures):**\n```occam\nPROC out.byte (VAL BYTE b, VAL INT field, CHAN BYTE out!)\n  VAL BYTE hundreds IS b/100:\n  VAL BYTE rest IS b\\100:\n  VAL BYTE tens IS rest/10:\n  VAL BYTE ones IS rest\\10:\n  IF\n    hundreds > 0\n      SEQ\n        out.repeat (' ', field - 3, out!)\n        out ! hundreds + '0'\n        ...\n```\n\n---\n\n### Files Changed by Commit b088adc\n\n1. **CLAUDE.md** - Updated feature list\n2. **TODO.md** - Updated completion status (course.module support now complete)\n3. **ast/ast.go** - Changed Body fields to `[]Statement` (22 lines changed)\n4. **parser/parser.go** - Added `parseBodyStatements()` and updated all callers (95 lines changed)\n5. **codegen/codegen.go** - Updated all statement generators and scanners (172 lines changed)\n6. **codegen/e2e_control_test.go** - Added 6 new e2e tests (81 lines added)\n7. **parser/parser_test.go** - Added 2 new unit tests (106 lines changed)\n\n**Total: 374 insertions, 115 deletions across 7 files**\n\n---\n\n### Impact & Significance\n\nThis fix was **critical** because it:\n\n1. **Enabled course module transpilation** - Resolved OOM errors preventing KRoC course library compilation\n2. **Maintained language completeness** - Occam code with scoped declarations in control flow is now fully supported\n3. **Added safety mechanism** - No-progress guard prevents similar infinite loops in future\n4. **Improved robustness** - Scanner functions updated consistently to handle new multi-statement bodies\n5. **Preserved existing tests** - All 374+ existing tests still pass with new Body type\n\n---\n\n### Key Technical Details\n\n**Indentation Pattern Used:**\n```\nstartLevel := p.indentLevel  // Save current level before entering body\n// ... loop while indentLevel >= startLevel\n// Exit when DEDENT brings us below startLevel\n```\n\n**No-Progress Guard Pattern:**\n```\nprevToken := p.curToken\nprevPeek := p.peekToken\nstmt := p.parseStatement()\n// ... advance token position\nif p.curToken == prevToken && p.peekToken == prevPeek {\n    break  // Prevent infinite loop\n}\n```\n\n---\n\n### Files Located\n\n**Course Module Files:**\n- `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/course.module`\n- `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_cycles.occ`\n- `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_nets.occ`\n- `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/utils.occ`\n- `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/consts.inc`\n- `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/random.occ`\n\n**Bug Fix Implementation:**\n- `/home/david/projects/code-associates/occam2go/parser/parser.go` - Line 1696-1752\n- `/home/david/projects/code-associates/occam2go/ast/ast.go` - Body type changes\n- `/home/david/projects/code-associates/occam2go/codegen/codegen.go` - Generator updates\n\n**Tests:**\n- `/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go` - Lines 205-284\n- `/home/david/projects/code-associates/occam2go/parser/parser_test.go` - New test functions"
  },
  {
    "type": "text",
    "text": "agentId: afbd6bf (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 44334\ntool_uses: 25\nduration_ms: 68307</usage>"
  }
]

Good context. Now let me look at the actual course module source files and try to reproduce the issue carefully - with a timeout to avoid the OOM problem.

$ Bash
List course module source files
ls /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/
Makefile.am
README
SConscript
configure.ac
consts.inc
course.module
demo_cycles.occ
demo_nets.occ
file_in.occ
float_io.occ
libcourse-cycles.3
libcourse-nets.3
libcourse-utils.3
random.doc
random.occ
shared_screen.inc
shared_screen.occ
string.occ
stringbuf.occ
utils.doc
utils.occ
$ Bash
Run existing tests to check current state
go test ./... 2>&1 | tail -20
?   	github.com/codeassociates/occam2go	[no test files]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	(cached)
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	(cached)
ok  	github.com/codeassociates/occam2go/preproc	(cached)

Tests pass. Now let me try to transpile the course module files one at a time with a strict timeout and memory limit to see which one triggers the infinite loop.

$ Bash
Build the transpiler
go build -o occam2go .
$ Bash
Try transpiling each .occ file with 5s timeout
for f in kroc/modules/course/libsrc/*.occ; do echo "=== Testing: $f ==="; timeout 5 ./occam2go "$f" -o /dev/null 2>&1; rc=$?; if [ $rc -eq 124 ]; then echo "TIMEOUT (likely infinite loop)"; elif [ $rc -ne 0 ]; then echo "Exit code: $rc"; fi; echo; done
=== Testing: kroc/modules/course/libsrc/demo_cycles.occ ===
Parse errors:
  line 33: expected type in parameter, got IDENT
  line 33: expected ), got !
  line 33: unexpected token: !
  line 33: unexpected token: )
  line 41: unexpected token: :
  line 44: expected type in parameter, got IDENT
  line 44: expected ), got !
  line 44: unexpected token: !
  line 44: unexpected token: )
  line 52: unexpected token: :
  line 55: expected type in parameter, got IDENT
  line 55: expected ), got ?
  line 55: unexpected token: ?
  line 55: unexpected token: ,
  line 55: unexpected token in expression: )
  line 65: unexpected token: :
  line 68: expected type in parameter, got IDENT
  line 68: expected ), got !
  line 68: unexpected token: !
  line 68: unexpected token: ,
  line 68: unexpected token in expression: )
  line 78: unexpected token: :
  line 81: expected type in parameter, got IDENT
  line 81: expected ), got !
  line 81: unexpected token: !
  line 81: unexpected token: )
  line 87: unexpected token: :
  line 90: expected type in parameter, got IDENT
  line 90: expected ), got !
  line 90: unexpected token: !
  line 90: unexpected token: )
  line 97: unexpected token: :
Exit code: 1

=== Testing: kroc/modules/course/libsrc/demo_nets.occ ===
Parse errors:
  line 46: expected type in parameter, got IDENT
  line 46: expected ), got !
  line 46: unexpected token: !
  line 46: unexpected token: )
  line 54: unexpected token: :
  line 57: expected type in parameter, got IDENT
  line 57: expected ), got !
  line 57: unexpected token: !
  line 57: unexpected token: )
  line 66: unexpected token: :
Exit code: 1

=== Testing: kroc/modules/course/libsrc/file_in.occ ===
TIMEOUT (likely infinite loop)

=== Testing: kroc/modules/course/libsrc/float_io.occ ===
Parse errors:
  line 47: expected type in parameter, got RESULT
  line 47: expected ), got INT
  line 47: expected IDENT, got RESULT
  line 47: unexpected token: RESULT
  line 47: unexpected token in expression: ]
  line 47: expected ], got BYTE
  line 47: expected IDENT, got VAL
  line 47: expected IS, got ,
  line 47: unexpected token: ,
  line 47: expected IS, got ,
  line 47: unexpected token: ,
  line 47: unexpected token: )
  line 990: unexpected token: :
  line 1015: expected type in parameter, got RESULT
  line 1015: expected ), got INT
  line 1015: expected IDENT, got RESULT
  line 1015: unexpected token: RESULT
  line 1015: unexpected token in expression: ]
  line 1015: expected ], got BYTE
  line 1015: expected IDENT, got VAL
  line 1015: expected IS, got ,
  line 1015: unexpected token: ,
  line 1015: expected IS, got ,
  line 1015: unexpected token: ,
  line 1015: unexpected token: )
  line 2189: unexpected token: :
  line 2245: expected type in parameter, got IDENT
  line 2245: expected ), got ,
  line 2245: unexpected token: ,
  line 2245: expected :, got !
  line 2245: unexpected token: !
  line 2245: unexpected token: )
  line 2252: unexpected token: :
  line 2260: expected type in parameter, got IDENT
  line 2260: expected ), got ,
  line 2260: unexpected token: ,
  line 2260: expected :, got !
  line 2260: unexpected token: !
  line 2260: unexpected token: )
  line 2267: unexpected token: :
Exit code: 1

=== Testing: kroc/modules/course/libsrc/random.occ ===
Parse errors:
  line 46: expected type in parameter, got IDENT
Exit code: 1

=== Testing: kroc/modules/course/libsrc/shared_screen.occ ===
TIMEOUT (likely infinite loop)

=== Testing: kroc/modules/course/libsrc/string.occ ===
TIMEOUT (likely infinite loop)

=== Testing: kroc/modules/course/libsrc/stringbuf.occ ===
Parse errors:
  line 36: expected FROM, got FOR
  line 36: unexpected token: FOR
  line 36: unexpected token: (
  line 36: unexpected token: (
  line 36: unexpected token: SIZE
  line 36: unexpected token: )
  line 36: unexpected token: +
  line 36: unexpected token: )
  line 36: unexpected token: ]
  line 185: expected type in parameter, got IDENT
  line 185: expected ), got ,
  line 185: unexpected token: ,
  line 185: expected IS, got )
  line 185: unexpected token: )
  line 190: expected FROM, got FOR
  line 190: expected ), got FOR
  line 190: unexpected token: FOR
  line 190: unexpected token: ]
  line 190: unexpected token: )
  line 191: unexpected token: :
  line 202: expected type in parameter, got IDENT
  line 202: expected ), got ,
  line 202: unexpected token: ,
  line 202: expected IS, got )
  line 202: unexpected token: )
  line 207: expected FROM, got FOR
  line 207: expected ), got FOR
  line 207: unexpected token: FOR
  line 207: unexpected token: ]
  line 207: unexpected token: )
  line 208: unexpected token: :
Exit code: 1

=== Testing: kroc/modules/course/libsrc/utils.occ ===
TIMEOUT (likely infinite loop)

Found it - three files trigger the infinite loop: file_in.occ, shared_screen.occ, string.occ, and utils.occ. Let me look at the simplest one to find the pattern causing the loop. Let me start by examining these files and trying to narrow down the issue.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/string.occ"
}
     1→--
     2→--	string functions
     3→--	Copyright (C) 1985  P.H.Welch
     4→--	Copyright (C) 2007  Adam Sampson <ats@offog.org>
     5→--
     6→--	This library is free software; you can redistribute it and/or
     7→--	modify it under the terms of the GNU Lesser General Public
     8→--	License as published by the Free Software Foundation; either
     9→--	version 2 of the License, or (at your option) any later version.
    10→--
    11→--	This library is distributed in the hope that it will be useful,
    12→--	but WITHOUT ANY WARRANTY; without even the implied warranty of
    13→--	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    14→--	Lesser General Public License for more details.
    15→--
    16→--	You should have received a copy of the GNU Lesser General Public
    17→--	License along with this library; if not, write to the Free Software
    18→--	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
    19→--
    20→
    21→--** @module course
    22→
    23→#INCLUDE "consts.inc"
    24→#USE "utils.tce"
    25→
    26→--{{{  PROC make.string ([]BYTE a,  VAL INT length)
    27→--* Converts a [@code BYTE] array into a string.
    28→-- This is needed for strings that have been input using a counted-array
    29→-- protocol (where we know the length, but the string characters have been
    30→-- left-justified and the elements of the array after the string need setting
    31→-- to [@ref NULL]).
    32→-- @param a Array to convert
    33→-- @param length Length of string in [@code a]
    34→PROC make.string ([]BYTE a,  VAL INT length)
    35→  --{{{  
    36→  IF
    37→    (SIZE a) > length
    38→      SEQ i = length FOR (SIZE a) - length
    39→        a[i] := NULL
    40→    TRUE
    41→      SKIP
    42→  --}}}
    43→:
    44→--}}}
    45→--{{{  PROC copy.string (VAL []BYTE a, []BYTE b)
    46→--* Copy a string.
    47→-- This copies the string from [@code a] into [@code b].  If [@code a] is
    48→-- shorter than [@code b], the string gets padded with [@ref NULL]s.  If
    49→-- [@code b] is shorter than [@code a], the string gets truncated.
    50→-- @param a Source string
    51→-- @param b Destination string
    52→PROC copy.string (VAL []BYTE a, []BYTE b)
    53→  --{{{  
    54→  INT min:
    55→  SEQ
    56→    --{{{  min := minimum (SIZE a, SIZE b)
    57→    IF
    58→      (SIZE a) > (SIZE b)
    59→        min := SIZE b
    60→      TRUE
    61→        min := SIZE a
    62→    --}}}
    63→    --{{{  copy a to b
    64→    [b FROM 0 FOR min] := [a FROM 0 FOR min]
    65→    --}}}
    66→    --{{{  pad with NULLs
    67→    SEQ i = min FOR (SIZE b) - min
    68→      b[i] := NULL
    69→    --}}}
    70→  --}}}
    71→:
    72→--}}}
    73→--{{{  BOOL FUNCTION equal.string (VAL []BYTE a, b)
    74→--* Compare two strings for equality.
    75→-- @param a One string
    76→-- @param b Other string
    77→-- @return Whether the two strings are equal
    78→BOOL FUNCTION equal.string (VAL []BYTE a, b)
    79→  --{{{  
    80→  BOOL result:
    81→  VALOF
    82→    --{{{  
    83→    INT length.a, length.b:
    84→    SEQ
    85→      --{{{  set length.a
    86→      SEQ
    87→        length.a := SIZE a
    88→        IF
    89→          IF i = 1 FOR SIZE a
    90→            a[length.a - i] <> NULL
    91→              length.a := length.a - (i - 1)
    92→          TRUE
    93→            length.a := 0
    94→      --}}}
    95→      --{{{  set length.b
    96→      SEQ
    97→        length.b := SIZE b
    98→        IF
    99→          IF i = 1 FOR SIZE b
   100→            b[length.b - i] <> NULL
   101→              length.b := length.b - (i - 1)
   102→          TRUE
   103→            length.b := 0
   104→      --}}}
   105→      --{{{  compare
   106→      IF
   107→        length.a <> length.b
   108→          result := FALSE
   109→        IF i = 0 FOR length.a
   110→          a[i] <> b[i]
   111→            result := FALSE
   112→        TRUE
   113→          result := TRUE
   114→      --}}}
   115→    --}}}
   116→    RESULT result
   117→  --}}}
   118→:
   119→--}}}
   120→--{{{  INT FUNCTION compare.string (VAL []BYTE a, b)
   121→--* Compares two strings for lexicographic ordering.
   122→-- @param a Left string
   123→-- @param b Right string
   124→-- @return [@ref string.less] if [@code a] < [@code b];
   125→--   [@ref string.same] if [@code a] = [@code b];
   126→--   [@ref string.more] otherwise
   127→INT FUNCTION compare.string (VAL []BYTE a, b)
   128→  --{{{  
   129→  INT result:
   130→  VALOF
   131→    --{{{  
   132→    INT length.a, length.b, min:
   133→    SEQ
   134→      --{{{  initialise
   135→      SEQ
   136→        --{{{  set length.a
   137→        SEQ
   138→          length.a := SIZE a
   139→          IF
   140→            IF i = 1 FOR SIZE a
   141→              a[length.a - i] <> NULL
   142→                length.a := length.a - (i - 1)
   143→            TRUE
   144→              length.a := 0
   145→        --}}}
   146→        --{{{  set length.b
   147→        SEQ
   148→          length.b := SIZE b
   149→          IF
   150→            IF i = 1 FOR SIZE b
   151→              b[length.b - i] <> NULL
   152→                length.b := length.b - (i - 1)
   153→            TRUE
   154→              length.b := 0
   155→        --}}}
   156→        IF
   157→          length.a < length.b
   158→            min := length.a
   159→          TRUE
   160→            min := length.b
   161→      --}}}
   162→      --{{{  compare
   163→      IF
   164→        IF i = 0 FOR min
   165→          a[i] <> b[i]
   166→            --{{{  different
   167→            IF
   168→              a[i] < b[i]
   169→                result := string.less
   170→              TRUE
   171→                result := string.more
   172→            --}}}
   173→        length.a < length.b
   174→          result := string.less
   175→        length.a > length.b
   176→          result := string.more
   177→        TRUE
   178→          result := string.same
   179→      --}}}
   180→    --}}}
   181→    RESULT result
   182→  --}}}
   183→:
   184→--}}}
   185→--{{{  BOOL FUNCTION starts.with.string
   186→--* Check if one string is a prefix of another.
   187→-- @param needle Possible prefix
   188→-- @param haystack String to test
   189→-- @return [@code TRUE] if [@code needle] is prefix of [@code haystack],
   190→--   [@code FALSE] otherwise
   191→BOOL FUNCTION starts.with.string (VAL []BYTE needle, haystack)
   192→  BOOL result:
   193→  VALOF
   194→    VAL INT nsize IS SIZE needle:
   195→    IF
   196→      nsize > (SIZE haystack)
   197→        result := FALSE
   198→      IF i = 0 FOR nsize
   199→        needle[i] <> haystack[i]
   200→          result := FALSE
   201→      TRUE
   202→        result := TRUE
   203→    RESULT result
   204→:
   205→--}}}
   206→--{{{  BOOL FUNCTION ends.with.string
   207→--* Check if one string is a suffix of another.
   208→-- @param needle Possible suffix
   209→-- @param haystack String to test
   210→-- @return [@code TRUE] if [@code needle] is suffix of [@code haystack],
   211→--   [@code FALSE] otherwise
   212→BOOL FUNCTION ends.with.string (VAL []BYTE needle, haystack)
   213→  BOOL result:
   214→  VALOF
   215→    VAL INT nsize IS SIZE needle:
   216→    IF
   217→      nsize > (SIZE haystack)
   218→        result := FALSE
   219→      IF i = 0 FOR nsize
   220→        needle[i] <> haystack[((SIZE haystack) - (SIZE needle)) + i]
   221→          result := FALSE
   222→      TRUE
   223→        result := TRUE
   224→    RESULT result
   225→:
   226→--}}}
   227→--{{{  BOOL FUNCTION byte.in.string
   228→--* Check if a string contains the given byte.
   229→-- @param needle Byte to search for
   230→-- @param haystack String to search
   231→-- @return [@code TRUE] if [@code needle] is in [@code haystack],
   232→--   [@code FALSE] otherwise
   233→BOOL FUNCTION byte.in.string (VAL BYTE needle, VAL []BYTE haystack)
   234→  BOOL b:
   235→  VALOF
   236→    IF
   237→      IF i = 0 FOR SIZE haystack
   238→        haystack[i] = needle
   239→          b := TRUE
   240→      TRUE
   241→        b := FALSE
   242→    RESULT b
   243→:
   244→--}}}
   245→--{{{  BOOL, INT FUNCTION string.to.int
   246→--* Convert a string containing a decimal integer to an [@code INT].
   247→-- @param s String to convert
   248→-- @return [@code TRUE] if the conversion succeeded, [@code FALSE] otherwise
   249→-- @return The decimal number represented by the string, or [@code 0] if
   250→--   the string could not be converted
   251→BOOL, INT FUNCTION string.to.int (VAL []BYTE s)
   252→  BOOL b:
   253→  INT i:
   254→  VALOF
   255→    SEQ
   256→      i := 0
   257→      IF
   258→        IF i = 0 FOR SIZE s
   259→          NOT byte.in.string (s[i], "0123456789+-")
   260→            b := FALSE
   261→        NOT check.num.chars (s, SIZE s)
   262→          b := FALSE
   263→        TRUE
   264→          b, i := convert.num.chars (s)
   265→    RESULT b, i
   266→:
   267→--}}}
   268→--{{{  PROC write.string (VAL []BYTE a,  VAL INT field,  CHAN BYTE out!)
   269→--* Write a [@ref NULL]-padded string to a channel.
   270→-- This is similar to [@ref out.string], but correctly right-justifies the
   271→-- string when trailing [@ref NULL]s are present.
   272→-- @param a String to write
   273→-- @param field Field width to right-justify in
   274→-- @param out Channel to write to
   275→PROC write.string (VAL []BYTE a,  VAL INT field,  CHAN BYTE out!)
   276→  --{{{  
   277→  INT length.a:
   278→  SEQ
   279→    --{{{  set length.a
   280→    length.a := SIZE a
   281→    IF
   282→      IF i = 1 FOR SIZE a
   283→        a[length.a - i] <> NULL
   284→          length.a := length.a - (i - 1)
   285→      TRUE
   286→        length.a := 0
   287→    --}}}
   288→    --{{{  leading spaces
   289→    IF
   290→      field > length.a
   291→        SEQ i = 0 FOR field - length.a
   292→          out ! ' '
   293→      TRUE
   294→        SKIP
   295→    --}}}
   296→    --{{{  output a
   297→    SEQ i = 0 FOR length.a
   298→      out ! a[i]
   299→    --}}}
   300→  --}}}
   301→:
   302→--}}}
   303→
   304→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/file_in.occ"
}
     1→--
     2→--	Input functionality
     3→--	Copyright (C) 1985 P.H. Welch
     4→--
     5→--	This library is free software; you can redistribute it and/or
     6→--	modify it under the terms of the GNU Lesser General Public
     7→--	License as published by the Free Software Foundation; either
     8→--	version 2 of the License, or (at your option) any later version.
     9→--
    10→--	This library is distributed in the hope that it will be useful,
    11→--	but WITHOUT ANY WARRANTY; without even the implied warranty of
    12→--	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    13→--	Lesser General Public License for more details.
    14→--
    15→--	You should have received a copy of the GNU Lesser General Public
    16→--	License along with this library; if not, write to the Free Software
    17→--	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
    18→--
    19→
    20→--** @module course
    21→
    22→#INCLUDE "consts.inc"
    23→
    24→--{{{  PROC file.in.string
    25→--* Read a string from a channel non-interactively.
    26→-- Lines are terminated by newline characters.
    27→-- @param s Buffer to read into
    28→-- @param length Number of bytes read
    29→-- @param max Maximum number of bytes to read
    30→-- @param in Channel to read from
    31→PROC file.in.string ([]BYTE s,  INT length,  VAL INT max,  CHAN BYTE in?)
    32→  --{{{  non-interactive input of s using in (maximum of max characters)
    33→  INT cut.max:
    34→  BOOL ok:
    35→  BYTE ch:
    36→  SEQ
    37→    --{{{  initialise
    38→    SEQ
    39→      IF
    40→        max > (SIZE s)
    41→          cut.max := SIZE s
    42→        TRUE
    43→          cut.max := max
    44→      ok := FALSE
    45→      length := 0
    46→      in ? ch
    47→    --}}}
    48→    --{{{  main cycle
    49→    WHILE ch <> '*n'
    50→      SEQ
    51→        --{{{  process ch
    52→        IF
    53→          length = cut.max
    54→            --{{{  
    55→            SKIP
    56→            --}}}
    57→          (' ' <= ch) AND (ch <= '~')
    58→            --{{{  
    59→            SEQ
    60→              s [length] := ch
    61→              length := length + 1
    62→            --}}}
    63→          TRUE
    64→            --{{{  
    65→            SKIP
    66→            --}}}
    67→        --}}}
    68→        in ? ch
    69→    --}}}
    70→    --{{{  finalisation
    71→    SEQ i = length FOR (SIZE s) - length
    72→      s[i] := NULL
    73→    --}}}
    74→  --}}}
    75→:
    76→--}}}
    77→--{{{  PROC file.in.token
    78→--* Read a token from a channel non-interactively.
    79→-- Tokens are terminated by whitespace.
    80→-- @param t Buffer to read into
    81→-- @param length Number of bytes read
    82→-- @param max Maximum number of bytes to read
    83→-- @param in Channel to read from
    84→PROC file.in.token ([]BYTE t,  INT length,  VAL INT max,  CHAN BYTE in?)
    85→  --{{{  non-interactive input of t using in (maximum of max characters)
    86→  INT cut.max:
    87→  BOOL ok:
    88→  BYTE ch:
    89→  SEQ
    90→    --{{{  initialise
    91→    SEQ
    92→      IF
    93→        max > (SIZE t)
    94→          cut.max := SIZE t
    95→        TRUE
    96→          cut.max := max
    97→      ok := FALSE
    98→      length := 0
    99→      in ? ch
   100→    --}}}
   101→    --{{{  find start of token
   102→    WHILE (ch = ' ') OR (ch = '*t') OR (ch = '*c') OR (ch = '*n')
   103→      in ? ch
   104→    --}}}
   105→    --{{{  find end of token
   106→    WHILE NOT ((ch = ' ') OR (ch = '*t') OR (ch = '*c') OR (ch = '*n') OR
   107→               (ch = END.OF.FILE))
   108→      SEQ
   109→        --{{{  process ch
   110→        IF
   111→          length = cut.max
   112→            --{{{  
   113→            SKIP
   114→            --}}}
   115→          (' ' <= ch) AND (ch <= '~')
   116→            --{{{  
   117→            SEQ
   118→              t[length] := ch
   119→              length := length + 1
   120→            --}}}
   121→          TRUE
   122→            --{{{  
   123→            SKIP
   124→            --}}}
   125→        --}}}
   126→        in ? ch
   127→    --}}}
   128→    --{{{  finalisation
   129→    SEQ i = length FOR (SIZE t) - length
   130→      t[i] := NULL
   131→    --}}}
   132→  --}}}
   133→:
   134→--}}}
   135→--{{{  BOOL FUNCTION file.check.num.chars
   136→--* Checks that a string has the format of a legal integer.
   137→-- This is a low-level routine used by [@ref file.in.int].
   138→-- @param s String to check
   139→-- @param length Length of string
   140→-- @return Whether the string is valid
   141→-- @private
   142→BOOL FUNCTION file.check.num.chars (VAL []BYTE s,  VAL INT length)
   143→  --{{{  
   144→  BOOL ok:
   145→  VALOF
   146→    --{{{  is it ok?
   147→    IF
   148→      (('0' > s[0]) OR (s[0] > '9')) AND ((s[0] <> '+') AND (s[0] <> '-'))
   149→        ok := FALSE
   150→      IF i = 1 FOR length - 1
   151→        ('0' > s[i]) OR (s[i] > '9')
   152→          ok := FALSE
   153→      length > 1
   154→        ok := TRUE
   155→      length = 1
   156→        ok := ('0' <= s[0]) AND (s[0] <= '9')
   157→      TRUE
   158→        ok := FALSE
   159→    --}}}
   160→    RESULT ok
   161→  --}}}
   162→:
   163→--}}}
   164→--{{{  INT FUNCTION file.convert.num.chars
   165→--* Parse an integer value stored in decimal.
   166→-- This assumes that [@code s] has the format of a legal integer.  It computes
   167→-- that integer and returns it.  It does [@em not] guard against overflow.
   168→-- This is a low-level routine used by [@ref file.in.int].
   169→-- @param s String to convert
   170→-- @param length Length of string
   171→-- @return Integer value of string
   172→-- @private
   173→INT FUNCTION file.convert.num.chars (VAL []BYTE s,  VAL INT length)
   174→  --{{{  
   175→  INT x:
   176→  VALOF
   177→    --{{{  convert to x
   178→    INT start, n.digits:
   179→    BOOL positive:
   180→    SEQ
   181→      --{{{  check for sign
   182→      IF
   183→        s[0] = '+'
   184→          --{{{  
   185→          SEQ
   186→            positive := TRUE
   187→            start := 1
   188→            n.digits := length - 1
   189→          --}}}
   190→        s[0] = '-'
   191→          --{{{  
   192→          SEQ
   193→            positive := FALSE
   194→            start := 1
   195→            n.digits := length - 1
   196→          --}}}
   197→        TRUE
   198→          --{{{  
   199→          SEQ
   200→            positive := TRUE
   201→            start := 0
   202→            n.digits := length
   203→          --}}}
   204→      --}}}
   205→      --{{{  compute number
   206→      SEQ
   207→        x := 0
   208→        SEQ i = start FOR n.digits
   209→          x := (10*x) + ( (INT s[i]) - (INT '0') )
   210→      --}}}
   211→      --{{{  set the sign
   212→      IF
   213→        positive
   214→          SKIP
   215→        TRUE
   216→          x := -x
   217→      --}}}
   218→    --}}}
   219→    RESULT x
   220→  --}}}
   221→:
   222→--}}}
   223→--{{{  PROC file.in.int
   224→--* Read an integer from a channel non-interactively.
   225→-- @param n Integer read
   226→-- @param max Maximum number of bytes to read
   227→-- @param ok Whether a number was successfully read
   228→-- @param in Channel to read from
   229→PROC file.in.int (INT n,  VAL INT max,  BOOL ok,  CHAN BYTE in?)
   230→  --{{{  non-interactive input of n using in (up to max digits) (sets ok flag)
   231→  VAL INT max.num.digits IS 10:
   232→  [max.num.digits]BYTE s:
   233→  INT length, cut.max:
   234→  SEQ
   235→    --{{{  cut.max := min (max, max.num.digits)
   236→    IF
   237→      max > max.num.digits
   238→        cut.max := max.num.digits
   239→      TRUE
   240→        cut.max := max
   241→    --}}}
   242→    file.in.token (s, length, cut.max, in?)
   243→    #PRAGMA DEFINED s
   244→    ok := file.check.num.chars (s, length)
   245→    IF
   246→      ok
   247→        n := file.convert.num.chars (s, length)
   248→      TRUE
   249→        SKIP
   250→  --}}}
   251→:
   252→--}}}
   253→
   254→--{{{  deprecated stuff
   255→--* Synonym for [@ref file.in.int].
   256→-- @deprecated
   257→PROC file.in.number (INT n, VAL INT max, BOOL ok, CHAN BYTE in?)
   258→  file.in.int (n, max, ok, in?)
   259→:
   260→--}}}  
   261→
   262→
Thinking

Let me look at what constructs these files use that might cause the infinite loop. I notice:

  1. string.occ has #USE "utils.tce" - which the preprocessor would try to process
  2. file_in.occ has #INCLUDE "consts.inc" and #PRAGMA DEFINED s
  3. Both have BOOL FUNCTION, VALOF/RESULT patterns
  4. file_in.occ has byte literals with '*n', '*t', '*c'

Let me try to narrow down the issue.

So the hanging files are file_in.occ, shared_screen.occ, string.occ, and utils.occ, while the ones that parse but have errors are demo_cycles.occ, demo_nets.occ, float_io.occ, random.occ, and stringbuf.occ. I need to create a minimal test case to isolate what's causing the parser to hang, and I should examine what the #USE directive does in the preprocessor since that might be triggering the issue.

Let me try running the preprocessor on smaller sections of these files to narrow down which specific constructs are causing the infinite loop.

Let me create minimal test cases to isolate what construct triggers the loop.

$ Bash
Test BOOL FUNCTION with VALOF
cat > /tmp/test_minimal1.occ << 'EOF'
BOOL FUNCTION test.func (VAL INT x)
  BOOL result:
  VALOF
    IF
      x > 0
        result := TRUE
      TRUE
        result := FALSE
    RESULT result
:
EOF
timeout 5 ./occam2go /tmp/test_minimal1.occ -o /dev/null 2>&1; echo "Exit: $?"
$ Bash
Test open array param with SIZE
cat > /tmp/test_minimal2.occ << 'EOF'
PROC test ([]BYTE s, VAL INT length)
  IF
    (SIZE s) > length
      SEQ i = length FOR (SIZE s) - length
        s[i] := 0
    TRUE
      SKIP
:
EOF
timeout 5 ./occam2go /tmp/test_minimal2.occ -o /dev/null 2>&1; echo "Exit: $?"
$ Bash
Test byte literal '*n' escape
cat > /tmp/test_minimal3.occ << 'EOF'
PROC test (CHAN BYTE in?)
  BYTE ch:
  SEQ
    in ? ch
    WHILE ch <> '*n'
      SEQ
        in ? ch
:
EOF
timeout 5 ./occam2go /tmp/test_minimal3.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func test.func(x int) bool {
	var result bool
	if (x > 0) {
		result = true
	} else if true {
		result = false
	}
	return result
}

Exit: 0
package main

func test(s []byte, length int) {
	if (len(s) > length) {
		for i := length; i < length + (len(s) - length); i++ {
			s[i] = 0
		}
	} else if true {
		// SKIP
	}
}

Exit: 0
package main

func test(in <-chan byte) {
	var ch byte
	ch = <-in
	for (ch != byte(10)) {
		ch = <-in
	}
}

Exit: 0

Those work. Let me try the #USE directive and multi-function files, and also try BOOL, INT FUNCTION (multi-result).

$ Bash
Test #USE directive
cat > /tmp/test_minimal4.occ << 'EOF'
#USE "utils.tce"
PROC test ()
  SKIP
:
EOF
timeout 5 ./occam2go /tmp/test_minimal4.occ -o /dev/null 2>&1; echo "Exit: $?"
$ Bash
Test multi-result function
cat > /tmp/test_minimal5.occ << 'EOF'
BOOL, INT FUNCTION string.to.int (VAL INT x)
  BOOL b:
  INT i:
  VALOF
    SEQ
      i := 0
      IF
        x > 0
          b := TRUE
        TRUE
          b := FALSE
    RESULT b, i
:
EOF
timeout 5 ./occam2go /tmp/test_minimal5.occ -o /dev/null 2>&1; echo "Exit: $?"
$ Bash
Test multiple PROCs with slice assignment
cat > /tmp/test_minimal6.occ << 'EOF'
PROC make.string ([]BYTE a, VAL INT length)
  IF
    (SIZE a) > length
      SEQ i = length FOR (SIZE a) - length
        a[i] := 0
    TRUE
      SKIP
:
PROC copy.string (VAL []BYTE a, []BYTE b)
  INT min:
  SEQ
    IF
      (SIZE a) > (SIZE b)
        min := SIZE b
      TRUE
        min := SIZE a
    [b FROM 0 FOR min] := [a FROM 0 FOR min]
    SEQ i = min FOR (SIZE b) - min
      b[i] := 0
:
EOF
timeout 5 ./occam2go /tmp/test_minimal6.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func test() {
	// SKIP
}

Exit: 0
package main

func string.to.int(x int) (bool, int) {
	var b bool
	var i int
	i = 0
	if (x > 0) {
		b = true
	} else if true {
		b = false
	}
	return b, i
}

Exit: 0
package main

func make.string(a []byte, length int) {
	if (len(a) > length) {
		for i := length; i < length + (len(a) - length); i++ {
			a[i] = 0
		}
	} else if true {
		// SKIP
	}
}

func copy.string(a []byte, b []byte) {
	var min int
	if (len(a) > len(b)) {
		min = len(b)
	} else if true {
		min = len(a)
	}
	copy(b[0 : 0 + min], a[0 : 0 + min])
	for i := min; i < min + (len(b) - min); i++ {
		b[i] = 0
	}
}

Exit: 0

Those all work fine. Let me try reproducing with more of the actual string.occ content - specifically the BOOL FUNCTION with replicated IF and VALOF, which seems more complex.

$ Bash
Test replicated IF with nested IF
cat > /tmp/test_minimal7.occ << 'EOF'
BOOL FUNCTION equal.string (VAL []BYTE a, b)
  BOOL result:
  VALOF
    INT length.a, length.b:
    SEQ
      SEQ
        length.a := SIZE a
        IF
          IF i = 1 FOR SIZE a
            a[length.a - i] <> 0
              length.a := length.a - (i - 1)
          TRUE
            length.a := 0
      SEQ
        length.b := SIZE b
        IF
          IF i = 1 FOR SIZE b
            b[length.b - i] <> 0
              length.b := length.b - (i - 1)
          TRUE
            length.b := 0
      IF
        length.a <> length.b
          result := FALSE
        IF i = 0 FOR length.a
          a[i] <> b[i]
            result := FALSE
        TRUE
          result := TRUE
    RESULT result
:
EOF
timeout 5 ./occam2go /tmp/test_minimal7.occ -o /dev/null 2>&1; echo "Exit: $?"
Parse errors:
  line 1: expected type in parameter, got IDENT
Exit: 1
Thinking

The error is "expected type in parameter, got IDENT" - this is about the second parameter b which is declared as VAL []BYTE a, b - that's a shorthand where b shares the same type as a. This is a feature where multiple parameters of the same type can be listed with commas. The parser doesn't handle this.

But the question is - does this parsing error lead to the infinite loop, or is it something else? The demo_cycles.occ had similar parse errors but didn't hang. Let me try with a fuller version.

Wait, let me re-examine. The files that hang are:

  • file_in.occ
  • shared_screen.occ
  • string.occ
  • utils.occ

The files that DON'T hang (but have parse errors) are: - demo_cycles.occ - demo_nets.occ - float_io.occ - random.occ - stringbuf.occ

So what's different? Let me look at file_in.occ more carefully. It had #INCLUDE "consts.inc" and the first proc parameter is []BYTE s which should be supported. Let me check what happens with the CHAN BYTE in? parameter type - that's a channel type, which might be triggering something different in the parser that causes it to loop.

Actually, looking at the test output for test_minimal7.occ, the error "expected type in parameter, got IDENT" suggests the parser can't handle comma-separated parameters that share a type like VAL []BYTE a, b. Let me try isolating file_in.occ's first function without using that shorthand syntax to see if that's what's causing the hang.

The VAL []BYTE a, b shorthand syntax isn't supported. But that causes a parse error, not necessarily a hang. Let me try with file_in.occ content which uses #INCLUDE "consts.inc".

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/consts.inc"
}
     1→--
     2→--	Constants
     3→--	Copyright (C) 1984  P.H. Welch
     4→--
     5→--	This library is free software; you can redistribute it and/or
     6→--	modify it under the terms of the GNU Lesser General Public
     7→--	License as published by the Free Software Foundation; either
     8→--	version 2 of the License, or (at your option) any later version.
     9→--
    10→--	This library is distributed in the hope that it will be useful,
    11→--	but WITHOUT ANY WARRANTY; without even the implied warranty of
    12→--	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    13→--	Lesser General Public License for more details.
    14→--
    15→--	You should have received a copy of the GNU Lesser General Public
    16→--	License along with this library; if not, write to the Free Software
    17→--	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
    18→--
    19→
    20→--** Course library.
    21→--
    22→-- The course library provides various simple utilities for occam programmers.
    23→--
    24→-- Strings (as handled by this library) are [@code BYTE] arrays left-justified
    25→-- with trailing [@ref NULL]s. It is usually preferably to use mobile byte
    26→-- arrays ([@code MOBILE []BYTE]) to represent strings in modern occam-pi
    27→-- programs; the string features in this library exist for backwards
    28→-- compatibility.
    29→--
    30→-- To use this library:
    31→--
    32→-- [@code
    33→-- #INCLUDE "course.module"]
    34→--
    35→-- @module course
    36→
    37→--*{{{  ASCII Character values.
    38→VAL BYTE NULL IS 0:          --* ASCII NUL
    39→VAL BYTE BELL IS 7:          --* ASCII BEL - terminal bell
    40→VAL BYTE BACK IS 8:          --* ASCII BS - backspace key
    41→VAL BYTE ESCAPE IS 27:       --* ASCII ESC - escape key
    42→VAL BYTE DELETE IS 127:      --* ASCII DEL - delete key
    43→--*}}}
    44→
    45→--{{{  special constants
    46→VAL BYTE FLUSH IS 255:       --* Flush output buffer
    47→VAL BYTE END.OF.FILE IS 255: --* End of file
    48→--}}}
    49→
    50→--*{{{  COMPARE.STRING Return values from [@ref compare.string].
    51→VAL INT string.less IS -1:   --* Left less than right
    52→VAL INT string.same IS 0:    --* Left and right the same
    53→VAL INT string.more IS 1:    --* Left more than right
    54→--*}}}
    55→
    56→
$ Bash
Test file_in.occ first proc with constants
cat > /tmp/test_minimal8.occ << 'EOF'
VAL BYTE NULL IS 0:
VAL BYTE BELL IS 7:
VAL BYTE BACK IS 8:
VAL BYTE ESCAPE IS 27:
VAL BYTE DELETE IS 127:
VAL BYTE FLUSH IS 255:
VAL BYTE END.OF.FILE IS 255:
VAL INT string.less IS -1:
VAL INT string.same IS 0:
VAL INT string.more IS 1:

PROC file.in.string ([]BYTE s, INT length, VAL INT max, CHAN BYTE in?)
  INT cut.max:
  BOOL ok:
  BYTE ch:
  SEQ
    SEQ
      IF
        max > (SIZE s)
          cut.max := SIZE s
        TRUE
          cut.max := max
      ok := FALSE
      length := 0
      in ? ch
    WHILE ch <> '*n'
      SEQ
        IF
          length = cut.max
            SKIP
          (' ' <= ch) AND (ch <= '~')
            SEQ
              s[length] := ch
              length := length + 1
          TRUE
            SKIP
        in ? ch
    SEQ i = length FOR (SIZE s) - length
      s[i] := NULL
:
EOF
timeout 5 ./occam2go /tmp/test_minimal8.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func file.in.string(s []byte, length *int, max int, in <-chan byte) {
	var cut.max int
	var ok bool
	var ch byte
	if (max > len(s)) {
		cut.max = len(s)
	} else if true {
		cut.max = max
	}
	ok = false
	*length = 0
	ch = <-in
	for (ch != byte(10)) {
		if (length == cut.max) {
			// SKIP
		} else if ((byte(32) <= ch) && (ch <= byte(126))) {
			s[length] = ch
			*length = (length + 1)
		} else if true {
			// SKIP
		}
		ch = <-in
	}
	for i := length; i < length + (len(s) - length); i++ {
		s[i] = NULL
	}
}

func main() {
	NULL := 0
	BELL := 7
	BACK := 8
	ESCAPE := 27
	DELETE := 127
	FLUSH := 255
	END.OF.FILE := 255
	string.less := - 1
	string.same := 0
	string.more := 1
}
Exit: 0
Thinking

OK, that parsed fine. So the individual proc works. The issue may be when there are multiple procs in sequence, or when the #INCLUDE doesn't find the file. Let me try to figure out what exactly causes the hang.

Let me try with the actual file_in.occ:

Single proc works fine. Let me now try the actual file with its #INCLUDE:

$ Bash
Try file_in.occ with include path
timeout 5 ./occam2go -I kroc/modules/course/libsrc kroc/modules/course/libsrc/file_in.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Hangs with the include path. Let me check if the preprocessor is the issue by running without the include path:

$ Bash
Try file_in.occ without include path
timeout 5 ./occam2go kroc/modules/course/libsrc/file_in.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Also hangs without include path. Let me try creating the content manually with just the first PROC from file_in.occ (including the #INCLUDE and #USE directives):

$ Bash
Test two procs from file_in.occ
cat > /tmp/test_minimal9.occ << 'EOF'
PROC file.in.string ([]BYTE s, INT length, VAL INT max, CHAN BYTE in?)
  INT cut.max:
  BOOL ok:
  BYTE ch:
  SEQ
    SEQ
      IF
        max > (SIZE s)
          cut.max := SIZE s
        TRUE
          cut.max := max
      ok := FALSE
      length := 0
      in ? ch
    WHILE ch <> '*n'
      SEQ
        IF
          length = cut.max
            SKIP
          (' ' <= ch) AND (ch <= '~')
            SEQ
              s[length] := ch
              length := length + 1
          TRUE
            SKIP
        in ? ch
    SEQ i = length FOR (SIZE s) - length
      s[i] := 0
:

PROC file.in.token ([]BYTE t, INT length, VAL INT max, CHAN BYTE in?)
  INT cut.max:
  BOOL ok:
  BYTE ch:
  SEQ
    SEQ
      IF
        max > (SIZE t)
          cut.max := SIZE t
        TRUE
          cut.max := max
      ok := FALSE
      length := 0
      in ? ch
    WHILE (ch = ' ') OR (ch = '*t') OR (ch = '*c') OR (ch = '*n')
      in ? ch
    WHILE NOT ((ch = ' ') OR (ch = '*t') OR (ch = '*c') OR (ch = '*n'))
      SEQ
        IF
          length = cut.max
            SKIP
          (' ' <= ch) AND (ch <= '~')
            SEQ
              t[length] := ch
              length := length + 1
          TRUE
            SKIP
        in ? ch
    SEQ i = length FOR (SIZE t) - length
      t[i] := 0
:
EOF
timeout 5 ./occam2go /tmp/test_minimal9.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func file.in.string(s []byte, length *int, max int, in <-chan byte) {
	var cut.max int
	var ok bool
	var ch byte
	if (max > len(s)) {
		cut.max = len(s)
	} else if true {
		cut.max = max
	}
	ok = false
	*length = 0
	ch = <-in
	for (ch != byte(10)) {
		if (length == cut.max) {
			// SKIP
		} else if ((byte(32) <= ch) && (ch <= byte(126))) {
			s[length] = ch
			*length = (length + 1)
		} else if true {
			// SKIP
		}
		ch = <-in
	}
	for i := length; i < length + (len(s) - length); i++ {
		s[i] = 0
	}
}

func file.in.token(t []byte, length *int, max int, in <-chan byte) {
	var cut.max int
	var ok bool
	var ch byte
	if (max > len(t)) {
		cut.max = len(t)
	} else if true {
		cut.max = max
	}
	ok = false
	*length = 0
	ch = <-in
	for ((((ch == byte(32)) || (ch == byte(9))) || (ch == byte(13))) || (ch == byte(10))) {
		ch = <-in
	}
	for !((((ch == byte(32)) || (ch == byte(9))) || (ch == byte(13))) || (ch == byte(10))) {
		if (length == cut.max) {
			// SKIP
		} else if ((byte(32) <= ch) && (ch <= byte(126))) {
			t[length] = ch
			*length = (length + 1)
		} else if true {
			// SKIP
		}
		ch = <-in
	}
	for i := length; i < length + (len(t) - length); i++ {
		t[i] = 0
	}
}

Exit: 0

That works. So it's something specific to the actual file_in.occ content that isn't in my test. Let me try the actual file but preprocess manually to see the full expanded content:

$ Bash
Test fold markers
# Let me try to figure out what's different - maybe the fold markers --{{{ --}}}
cat > /tmp/test_fold.occ << 'EOF'
--{{{ PROC test
PROC test ()
  SKIP
:
--}}}
--{{{ PROC test2
PROC test2 ()
  SKIP
:
--}}}
EOF
timeout 5 ./occam2go /tmp/test_fold.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func test() {
	// SKIP
}

func test2() {
	// SKIP
}

Exit: 0
Thinking

The fold markers work. Let me look more carefully at file_in.occ. The key difference is: 1. It has #INCLUDE "consts.inc" 2. The consts.inc has VAL BYTE NULL IS 0: etc.

But I already tested the constants manually and they worked. What else?

It also uses #PRAGMA DEFINED s on line 243 and file.in.string (s, length, cut.max, in?) which passes in? at the call site.

Wait - the difference in my manual test vs the actual file is that the actual file has #INCLUDE "consts.inc". Let me preprocess the file and see what the actual expanded text looks like.

I should try using the preprocessor directly or embed the consts.inc content inline to create a test file that more closely matches the real one.

$ Bash
Test BOOL FUNCTION with complex conditions
# Let me try with the BOOL FUNCTION from file_in.occ (file.check.num.chars)
# This one has the check_num_chars function which starts with multi-condition
cat > /tmp/test_boolfunc.occ << 'EOF'
BOOL FUNCTION file.check.num.chars (VAL []BYTE s, VAL INT length)
  BOOL ok:
  VALOF
    IF
      (('0' > s[0]) OR (s[0] > '9')) AND ((s[0] <> '+') AND (s[0] <> '-'))
        ok := FALSE
      IF i = 1 FOR length - 1
        ('0' > s[i]) OR (s[i] > '9')
          ok := FALSE
      length > 1
        ok := TRUE
      length = 1
        ok := ('0' <= s[0]) AND (s[0] <= '9')
      TRUE
        ok := FALSE
    RESULT ok
:
EOF
timeout 5 ./occam2go /tmp/test_boolfunc.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Found a reproducer! This BOOL FUNCTION with a replicated IF (IF i = 1 FOR length - 1) followed by additional conditions hangs. Let me narrow it down further:

$ Bash
Test replicated IF followed by other branches
# Simpler case - just replicated IF followed by other conditions
cat > /tmp/test_repif.occ << 'EOF'
PROC test (VAL []BYTE s, VAL INT length)
  BOOL ok:
  SEQ
    IF
      IF i = 1 FOR length - 1
        s[i] > '9'
          ok := FALSE
      length > 1
        ok := TRUE
      TRUE
        ok := FALSE
:
EOF
timeout 5 ./occam2go /tmp/test_repif.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

That also hangs! Let me simplify even more:

$ Bash
Minimal replicated IF test
# Even simpler
cat > /tmp/test_repif2.occ << 'EOF'
PROC test (VAL INT n)
  INT x:
  SEQ
    IF
      IF i = 0 FOR n
        i > 5
          x := i
      TRUE
        x := 0
:
EOF
timeout 5 ./occam2go /tmp/test_repif2.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Excellent - very minimal reproducer. A replicated IF (i.e., IF i = 0 FOR n) used as a branch inside a larger IF block, followed by additional branches (TRUE), causes the infinite loop. Let me look at how replicated IF is parsed:

$ Bash
Test replicated IF alone (no sibling branches)
# First, does standalone replicated IF work?
cat > /tmp/test_repif3.occ << 'EOF'
PROC test (VAL INT n)
  INT x:
  SEQ
    IF
      IF i = 0 FOR n
        i > 5
          x := i
:
EOF
timeout 5 ./occam2go /tmp/test_repif3.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Even the standalone replicated IF hangs! Let me try without the body:

$ Bash
Simplest replicated IF
# Simplest possible replicated IF  
cat > /tmp/test_repif4.occ << 'EOF'
PROC test ()
  INT x:
  SEQ
    IF
      IF i = 0 FOR 3
        i > 1
          x := i
:
EOF
timeout 5 ./occam2go /tmp/test_repif4.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

OK so any replicated IF (an IF with a replicator IF i = 0 FOR n) causes the hang. Now let me look at the parser code to understand how IF statements are parsed:

Grep
{
  "pattern": "func.*parseIfStatement",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content"
}
2216:func (p *Parser) parseIfStatement() *ast.IfStatement {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 2216,
  "limit": 120
}
  2216→func (p *Parser) parseIfStatement() *ast.IfStatement {
  2217→	stmt := &ast.IfStatement{Token: p.curToken}
  2218→
  2219→	// Check for replicator: IF i = start FOR count
  2220→	if p.peekTokenIs(lexer.IDENT) {
  2221→		p.nextToken() // move to identifier
  2222→		if p.peekTokenIs(lexer.EQ) {
  2223→			stmt.Replicator = p.parseReplicator()
  2224→		} else {
  2225→			p.addError("unexpected identifier after IF")
  2226→			return stmt
  2227→		}
  2228→	}
  2229→
  2230→	// Skip to next line
  2231→	for p.peekTokenIs(lexer.NEWLINE) {
  2232→		p.nextToken()
  2233→	}
  2234→
  2235→	// Expect INDENT
  2236→	if !p.peekTokenIs(lexer.INDENT) {
  2237→		p.addError("expected indented block after IF")
  2238→		return stmt
  2239→	}
  2240→	p.nextToken() // consume INDENT
  2241→	startLevel := p.indentLevel
  2242→	p.nextToken() // move into block
  2243→
  2244→	// Parse if choices (condition -> body pairs)
  2245→	for !p.curTokenIs(lexer.EOF) {
  2246→		// Skip newlines
  2247→		for p.curTokenIs(lexer.NEWLINE) {
  2248→			p.nextToken()
  2249→		}
  2250→
  2251→		// Handle DEDENT tokens
  2252→		for p.curTokenIs(lexer.DEDENT) {
  2253→			if p.indentLevel < startLevel {
  2254→				return stmt
  2255→			}
  2256→			p.nextToken()
  2257→		}
  2258→
  2259→		// Skip any more newlines after DEDENT
  2260→		for p.curTokenIs(lexer.NEWLINE) {
  2261→			p.nextToken()
  2262→		}
  2263→
  2264→		if p.curTokenIs(lexer.EOF) {
  2265→			break
  2266→		}
  2267→
  2268→		if p.indentLevel < startLevel {
  2269→			break
  2270→		}
  2271→
  2272→		choice := ast.IfChoice{}
  2273→		choice.Condition = p.parseExpression(LOWEST)
  2274→
  2275→		// Skip newlines and expect INDENT for body
  2276→		for p.peekTokenIs(lexer.NEWLINE) {
  2277→			p.nextToken()
  2278→		}
  2279→
  2280→		if p.peekTokenIs(lexer.INDENT) {
  2281→			p.nextToken() // consume INDENT
  2282→			p.nextToken() // move to body
  2283→			choice.Body = p.parseBodyStatements()
  2284→		}
  2285→
  2286→		stmt.Choices = append(stmt.Choices, choice)
  2287→	}
  2288→
  2289→	return stmt
  2290→}
  2291→
  2292→func (p *Parser) parseCaseStatement() *ast.CaseStatement {
  2293→	stmt := &ast.CaseStatement{Token: p.curToken}
  2294→
  2295→	// Parse selector expression on the same line
  2296→	p.nextToken()
  2297→	stmt.Selector = p.parseExpression(LOWEST)
  2298→
  2299→	// Skip to next line
  2300→	for p.peekTokenIs(lexer.NEWLINE) {
  2301→		p.nextToken()
  2302→	}
  2303→
  2304→	// Expect INDENT
  2305→	if !p.peekTokenIs(lexer.INDENT) {
  2306→		p.addError("expected indented block after CASE")
  2307→		return stmt
  2308→	}
  2309→	p.nextToken() // consume INDENT
  2310→	startLevel := p.indentLevel
  2311→	p.nextToken() // move into block
  2312→
  2313→	// Parse case choices
  2314→	for !p.curTokenIs(lexer.EOF) {
  2315→		// Skip newlines
  2316→		for p.curTokenIs(lexer.NEWLINE) {
  2317→			p.nextToken()
  2318→		}
  2319→
  2320→		// Handle DEDENT tokens
  2321→		for p.curTokenIs(lexer.DEDENT) {
  2322→			if p.indentLevel < startLevel {
  2323→				return stmt
  2324→			}
  2325→			p.nextToken()
  2326→		}
  2327→
  2328→		// Skip any more newlines after DEDENT
  2329→		for p.curTokenIs(lexer.NEWLINE) {
  2330→			p.nextToken()
  2331→		}
  2332→
  2333→		if p.curTokenIs(lexer.EOF) {
  2334→			break
  2335→		}
Thinking

OK, I see the issue now. Let me trace what happens with the replicated IF:

IF
  IF i = 0 FOR 3
    i > 1
      x := i
  1. parseIfStatement() is called for the outer IF
  2. It checks p.peekTokenIs(lexer.IDENT) - NO, the peek token is NEWLINE (after outer IF)
  3. Skips newlines, consumes INDENT
  4. Now we're at the condition-parsing loop at line 2245
  5. Current token is IF (the inner replicated IF)

  6. At line 2273, parseExpression(LOWEST) gets called with IF as the current token, but the expression parser likely has no prefix handler for IF tokens, so it probably returns nil or an unexpected value instead of properly parsing the replicated IF construct.

I see the issue. When the parser encounters a nested/replicated IF inside an outer IF block, line 2273 tries to parse it as an expression (choice.Condition = p.parseExpression(LOWEST)). Let me check what happens when parseExpression encounters an IF token:

Grep
{
  "pattern": "func.*parseExpression",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content"
}
2369:func (p *Parser) parseExpression(precedence int) ast.Expression {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 2369,
  "limit": 120
}
  2369→func (p *Parser) parseExpression(precedence int) ast.Expression {
  2370→	var left ast.Expression
  2371→
  2372→	switch p.curToken.Type {
  2373→	case lexer.IDENT:
  2374→		if p.peekTokenIs(lexer.LPAREN) {
  2375→			left = p.parseFuncCallExpr()
  2376→		} else {
  2377→			left = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
  2378→		}
  2379→	case lexer.INT:
  2380→		base := 10
  2381→		literal := p.curToken.Literal
  2382→		if strings.HasPrefix(literal, "0x") || strings.HasPrefix(literal, "0X") {
  2383→			base = 16
  2384→			literal = literal[2:]
  2385→		}
  2386→		val, err := strconv.ParseInt(literal, base, 64)
  2387→		if err != nil {
  2388→			p.addError(fmt.Sprintf("could not parse %q as integer", p.curToken.Literal))
  2389→			return nil
  2390→		}
  2391→		left = &ast.IntegerLiteral{Token: p.curToken, Value: val}
  2392→	case lexer.TRUE:
  2393→		left = &ast.BooleanLiteral{Token: p.curToken, Value: true}
  2394→	case lexer.FALSE:
  2395→		left = &ast.BooleanLiteral{Token: p.curToken, Value: false}
  2396→	case lexer.STRING:
  2397→		left = &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal}
  2398→	case lexer.BYTE_LIT:
  2399→		b, err := p.parseByteLiteralValue(p.curToken.Literal)
  2400→		if err != nil {
  2401→			p.addError(err.Error())
  2402→			return nil
  2403→		}
  2404→		left = &ast.ByteLiteral{Token: p.curToken, Value: b}
  2405→	case lexer.LPAREN:
  2406→		p.nextToken()
  2407→		left = p.parseExpression(LOWEST)
  2408→		if !p.expectPeek(lexer.RPAREN) {
  2409→			return nil
  2410→		}
  2411→	case lexer.MINUS:
  2412→		token := p.curToken
  2413→		p.nextToken()
  2414→		left = &ast.UnaryExpr{
  2415→			Token:    token,
  2416→			Operator: "-",
  2417→			Right:    p.parseExpression(PREFIX),
  2418→		}
  2419→	case lexer.NOT:
  2420→		token := p.curToken
  2421→		p.nextToken()
  2422→		left = &ast.UnaryExpr{
  2423→			Token:    token,
  2424→			Operator: "NOT",
  2425→			Right:    p.parseExpression(PREFIX),
  2426→		}
  2427→	case lexer.BITNOT:
  2428→		token := p.curToken
  2429→		p.nextToken()
  2430→		left = &ast.UnaryExpr{
  2431→			Token:    token,
  2432→			Operator: "~",
  2433→			Right:    p.parseExpression(PREFIX),
  2434→		}
  2435→	case lexer.LBRACKET:
  2436→		// Slice expression: [arr FROM start FOR length]
  2437→		lbracket := p.curToken
  2438→		p.nextToken() // move past [
  2439→		arrayExpr := p.parseExpression(LOWEST)
  2440→		if !p.expectPeek(lexer.FROM) {
  2441→			return nil
  2442→		}
  2443→		p.nextToken() // move past FROM
  2444→		startExpr := p.parseExpression(LOWEST)
  2445→		if !p.expectPeek(lexer.FOR) {
  2446→			return nil
  2447→		}
  2448→		p.nextToken() // move past FOR
  2449→		lengthExpr := p.parseExpression(LOWEST)
  2450→		if !p.expectPeek(lexer.RBRACKET) {
  2451→			return nil
  2452→		}
  2453→		left = &ast.SliceExpr{
  2454→			Token:  lbracket,
  2455→			Array:  arrayExpr,
  2456→			Start:  startExpr,
  2457→			Length: lengthExpr,
  2458→		}
  2459→	case lexer.SIZE_KW:
  2460→		token := p.curToken
  2461→		p.nextToken()
  2462→		left = &ast.SizeExpr{
  2463→			Token: token,
  2464→			Expr:  p.parseExpression(PREFIX),
  2465→		}
  2466→	case lexer.MOSTNEG_KW, lexer.MOSTPOS_KW:
  2467→		token := p.curToken
  2468→		isNeg := token.Type == lexer.MOSTNEG_KW
  2469→		// Expect a type name next
  2470→		if !p.peekTokenIs(lexer.INT_TYPE) && !p.peekTokenIs(lexer.BYTE_TYPE) &&
  2471→			!p.peekTokenIs(lexer.BOOL_TYPE) && !p.peekTokenIs(lexer.REAL_TYPE) &&
  2472→			!p.peekTokenIs(lexer.REAL32_TYPE) && !p.peekTokenIs(lexer.REAL64_TYPE) {
  2473→			p.addError(fmt.Sprintf("expected type after %s, got %s", token.Literal, p.peekToken.Type))
  2474→			return nil
  2475→		}
  2476→		p.nextToken()
  2477→		left = &ast.MostExpr{
  2478→			Token:    token,
  2479→			ExprType: p.curToken.Literal,
  2480→			IsNeg:    isNeg,
  2481→		}
  2482→	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
  2483→		token := p.curToken
  2484→		p.nextToken()
  2485→		left = &ast.TypeConversion{
  2486→			Token:      token,
  2487→			TargetType: token.Literal,
  2488→			Expr:       p.parseExpression(PREFIX),
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 2489,
  "limit": 60
}
  2489→		}
  2490→	default:
  2491→		p.addError(fmt.Sprintf("unexpected token in expression: %s", p.curToken.Type))
  2492→		return nil
  2493→	}
  2494→
  2495→	// Parse infix expressions
  2496→	for !p.peekTokenIs(lexer.NEWLINE) && !p.peekTokenIs(lexer.EOF) &&
  2497→		precedence < p.peekPrecedence() {
  2498→
  2499→		switch p.peekToken.Type {
  2500→		case lexer.PLUS, lexer.MINUS, lexer.MULTIPLY, lexer.DIVIDE, lexer.MODULO,
  2501→			lexer.PLUS_KW, lexer.MINUS_KW, lexer.TIMES,
  2502→			lexer.EQ, lexer.NEQ, lexer.LT, lexer.GT, lexer.LE, lexer.GE,
  2503→			lexer.AND, lexer.OR, lexer.AFTER,
  2504→			lexer.BITAND, lexer.BITOR, lexer.BITXOR, lexer.LSHIFT, lexer.RSHIFT:
  2505→			p.nextToken()
  2506→			left = p.parseBinaryExpr(left)
  2507→		case lexer.LBRACKET:
  2508→			p.nextToken()
  2509→			left = p.parseIndexExpression(left)
  2510→		default:
  2511→			return left
  2512→		}
  2513→	}
  2514→
  2515→	return left
  2516→}
  2517→
  2518→func (p *Parser) parseBinaryExpr(left ast.Expression) ast.Expression {
  2519→	expr := &ast.BinaryExpr{
  2520→		Token:    p.curToken,
  2521→		Left:     left,
  2522→		Operator: p.curToken.Literal,
  2523→	}
  2524→
  2525→	prec := p.curPrecedence()
  2526→	p.nextToken()
  2527→	expr.Right = p.parseExpression(prec)
  2528→
  2529→	return expr
  2530→}
  2531→
Thinking

Now I understand the issue. In parseIfStatement(), at line 2273:

choice.Condition = p.parseExpression(LOWEST)

When the current token is IF (for a nested replicated IF), parseExpression falls through to the default case (line 2490-2492):

default:
    p.addError(fmt.Sprintf("unexpected token in expression: %s", p.curToken.Type))
    return nil

This adds an error and returns nil. Then control returns to parseIfStatement(), which continues:

The code then tries to skip newlines and consume an INDENT token to parse the body, but since parseExpression returned nil without advancing past the IF token, the peek might not find NEWLINE or INDENT at the expected position, causing the body parsing to fail or skip entirely.

Now I'm tracing through what happens in the main parsing loop: the cursor stays on the IF token since parseExpression didn't advance it, the peek shows the replicator variable instead of NEWLINE or INDENT, so the body-parsing block gets skipped, the choice gets added with no condition or body, and the loop returns to the top still positioned on IF — creating an infinite loop since the cursor never moves forward.

The real problem is that parseExpression returns nil without advancing when it hits an unknown token like IF, and the main loop doesn't compensate by advancing either. What's actually happening here is that the inner IF is a valid nested construct in occam — a replicated IF that acts as one of the choices in the outer IF — but the parser isn't recognizing it as such.

I need to check how IfChoice is structured in the AST to understand how to properly handle this nested IF scenario.

Now I see the problem. When parseIfStatement() encounters a nested IF token as a condition (line 2273), parseExpression(LOWEST) doesn't recognize IF as an expression, adds an error, returns nil without advancing the cursor. The main loop at line 2245 never makes progress - the token position doesn't change, creating the infinite loop.

In occam, nested IF within an IF block is a valid construct (a nested choice group or replicated IF), not a condition-body pair. Let me look at the AST to understand how to represent this:

Grep
{
  "pattern": "type IfChoice|type IfStatement",
  "path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
  "output_mode": "content",
  "-A": 10
}
199:type IfStatement struct {
200-	Token      lexer.Token // the IF token
201-	Choices    []IfChoice
202-	Replicator *Replicator // optional replicator for IF i = start FOR count
203-}
204-
205:type IfChoice struct {
206-	Condition Expression
207-	Body      []Statement
208-}
209-
210-func (i *IfStatement) statementNode()       {}
211-func (i *IfStatement) TokenLiteral() string { return i.Token.Literal }
212-
213-// CaseStatement represents a CASE statement
214-type CaseStatement struct {
215-	Token    lexer.Token  // the CASE token

Now let me understand the full picture. In occam, within an IF block you can have: 1. condition + body (normal choice) 2. IF i = 0 FOR n + choices (replicated IF - expands choices) 3. Nested IF + choices (nested IF block)

The current parser only handles case 1. Cases 2 and 3 are what cause the hang. Let me also check how parseStatement() handles the IF keyword to understand the complete flow:

Grep
{
  "pattern": "case lexer.IF:",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-A": 3
}
197:	case lexer.IF:
198-		return p.parseIfStatement()
199-	case lexer.CASE:
200-		return p.parseCaseStatement()

Good. Now let me also verify where the parseBodyStatements function is and check its progress-guard behavior:

Grep
{
  "pattern": "func.*parseBodyStatements",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content"
}
1700:func (p *Parser) parseBodyStatements() []ast.Statement {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1700,
  "limit": 60
}
  1700→func (p *Parser) parseBodyStatements() []ast.Statement {
  1701→	var statements []ast.Statement
  1702→	startLevel := p.indentLevel
  1703→
  1704→	for !p.curTokenIs(lexer.EOF) {
  1705→		// Skip newlines
  1706→		for p.curTokenIs(lexer.NEWLINE) {
  1707→			p.nextToken()
  1708→		}
  1709→
  1710→		// Handle DEDENT tokens
  1711→		for p.curTokenIs(lexer.DEDENT) {
  1712→			if p.indentLevel < startLevel {
  1713→				return statements
  1714→			}
  1715→			p.nextToken()
  1716→		}
  1717→
  1718→		// Skip any more newlines after DEDENT
  1719→		for p.curTokenIs(lexer.NEWLINE) {
  1720→			p.nextToken()
  1721→		}
  1722→
  1723→		if p.curTokenIs(lexer.EOF) {
  1724→			break
  1725→		}
  1726→
  1727→		if p.indentLevel < startLevel {
  1728→			break
  1729→		}
  1730→
  1731→		// Safety guard: record position before parsing to detect no-progress
  1732→		prevToken := p.curToken
  1733→		prevPeek := p.peekToken
  1734→
  1735→		stmt := p.parseStatement()
  1736→		if stmt != nil {
  1737→			statements = append(statements, stmt)
  1738→		}
  1739→
  1740→		// Advance past the last token of the statement if needed
  1741→		if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1742→			p.nextToken()
  1743→		}
  1744→
  1745→		// No-progress guard: if we haven't moved, break to prevent infinite loop
  1746→		if p.curToken == prevToken && p.peekToken == prevPeek {
  1747→			break
  1748→		}
  1749→	}
  1750→
  1751→	return statements
  1752→}
  1753→
  1754→func (p *Parser) parseProcDecl() *ast.ProcDecl {
  1755→	proc := &ast.ProcDecl{Token: p.curToken}
  1756→
  1757→	if !p.expectPeek(lexer.IDENT) {
  1758→		return nil
  1759→	}
Thinking

OK so the parseBodyStatements has a no-progress guard. But the infinite loop is in parseIfStatement(), specifically in the main loop at line 2245 which does NOT have a progress guard.

Let me trace the exact scenario more carefully:

PROC test ()
  INT x:
  SEQ
    IF              <-- outer IF
      IF i = 0 FOR 3   <-- inner replicated IF (should be a choice within outer IF)
        i > 1
          x := i
:

The lexer will produce tokens roughly like:

PROC IDENT('test') LPAREN RPAREN NEWLINE


INDENT INT_TYPE IDENT('x') COLON NEWLINE
SEQ NEWLINE
INDENT IF NEWLINE
INDENT IF IDENT('i') EQ INT(0) FOR INT(3) NEWLINE
INDENT IDENT('i') GT INT(1) NEWLINE
INDENT IDENT('x') ASSIGN INT_KW(i) NEWLINE
DEDENT DEDENT DEDENT DEDENT DEDENT ...
COLON

When parseIfStatement() is called for the outer IF, it starts with the outer IF token and begins parsing the condition. The parser encounters the inner IF token where it expects a condition expression, which triggers the infinite loop because the main parsing loop doesn't advance when it encounters an unexpected token. TokenIs(lexer.IDENT)- NO (peek is NEWLINE) 3. No replicator 4. Skip newlines, consume INDENT 5.startLevel := p.indentLevel(let's say level 4) 6.p.nextToken()- move into block, curToken = IF (inner) 7. Main loop starts at line 2245 8. curToken = IF, not EOF 9. Not NEWLINE, not DEDENT, not EOF 10. indentLevel is still at startLevel 11. Line 2273:choice.Condition = p.parseExpression(LOWEST)` - called with curToken = IF

parseExpression(LOWEST): - curToken is IF - Falls to default case (line 2490) - Adds error "unexpected token in expression: IF" - Returns nil

Back in parseIfStatement(): the condition ends up nil since parseExpression failed on the IF token, and since peek is IDENT rather than NEWLINE or INDENT, the body never gets parsed either. The incomplete choice gets appended to the list and we loop back to process the next statement.

But here's the problem: curToken is still IF because parseExpression doesn't advance when it hits the default case. We're stuck in an infinite loop, repeatedly trying to parse the same IF token as an expression and failing each time.

The fix needs to either prevent this infinite loop with a progress guard, or properly handle nested IF statements by recognizing when curToken is IF and parsing it as a nested choice rather than trying to parse it as an expression.

In occam, nested IFs are actually valid—they let you group conditions together. So the right approach is to detect when we encounter an IF token while parsing choices, recursively parse it as a nested IF statement, and either embed it in the AST or flatten its choices into the parent. For codegen, I could add a NestedIf field to IfChoice to handle this cleanly. Continuing with replicated IF, I need to handle a loop that iterates through n conditions and executes the first matching body, falling back to a default if none match. I'm generating Go code that tracks whether a condition was found and breaks after executing the matching body, then handles the default case if needed. Looking at the existing AST structure, the IfStatement already has support for replicators at the top level...

Defining the nested structure... I need to revisit how the existing replicated IF codegen handles this, then figure out whether to add a no-progress guard to prevent the infinite loop or properly detect and parse nested IFs. Let me also consider what a plain nested IF looks like in occam—where an IF statement appears directly inside another IF's condition block—since that's also valid syntax and needs handling in the parseIfStatement loop when we encounter a nested IF token.

The cleanest approach would be adding a NestedIf field to the IfChoice struct to represent when a choice is actually a nested IF rather than a condition-body pair, then during codegen emit it either as a for loop with conditional break if it has a replicator, or inline the choices as additional branches if not. But I should think about what minimizes the actual code changes needed here.

Actually, I could treat the nested IF as a choice with a nil condition and the nested statement as its body—the codegen would just need to recognize that a nil condition means the choice always executes, essentially treating it as an unconditional entry into that nested IF.

But that's not quite right semantically. In occam, a nested IF should flatten its choices into the parent's sequence—so cond1 → body1, then the nested IF's conditions and bodies inline, then cond2 → body2. For replicated IFs though, I can't flatten at compile time, so I'd need a different approach for those.

Looking at the replicated case, I'd generate a loop that tries each condition in sequence, sets a flag when one matches, and breaks. Then after the loop, if nothing matched, execute the fallback. For non-replicated nested IFs, I can just inline them directly as chained if-else statements.

I need to tackle this in two phases: first, make sure the parser correctly identifies nested IF statements and stores them in the AST so we don't hit that infinite loop. Then, update the code generation to handle both cases—flattening the choices for simple nesting, and using the flag-based loop approach for replicated conditions.

Now I understand the root cause. Let me plan the fix. First let me check how the current codegen handles replicated IF:

Grep
{
  "pattern": "func.*generateIfStatement",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content"
}
1551:func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1551,
  "limit": 100
}
  1551→func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
  1552→	if stmt.Replicator != nil {
  1553→		// Replicated IF: IF i = start FOR count → for loop with break on first match
  1554→		v := stmt.Replicator.Variable
  1555→		if stmt.Replicator.Step != nil {
  1556→			counter := "_repl_" + v
  1557→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1558→			g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
  1559→			g.generateExpression(stmt.Replicator.Count)
  1560→			g.write(fmt.Sprintf("; %s++ {\n", counter))
  1561→			g.indent++
  1562→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1563→			g.write(fmt.Sprintf("%s := ", v))
  1564→			g.generateExpression(stmt.Replicator.Start)
  1565→			g.write(fmt.Sprintf(" + %s * ", counter))
  1566→			g.generateExpression(stmt.Replicator.Step)
  1567→			g.write("\n")
  1568→		} else {
  1569→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1570→			g.write(fmt.Sprintf("for %s := ", v))
  1571→			g.generateExpression(stmt.Replicator.Start)
  1572→			g.write(fmt.Sprintf("; %s < ", v))
  1573→			g.generateExpression(stmt.Replicator.Start)
  1574→			g.write(" + ")
  1575→			g.generateExpression(stmt.Replicator.Count)
  1576→			g.write(fmt.Sprintf("; %s++ {\n", v))
  1577→			g.indent++
  1578→		}
  1579→
  1580→		for i, choice := range stmt.Choices {
  1581→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1582→			if i == 0 {
  1583→				g.write("if ")
  1584→			} else {
  1585→				g.write("} else if ")
  1586→			}
  1587→			g.generateExpression(choice.Condition)
  1588→			g.write(" {\n")
  1589→			g.indent++
  1590→
  1591→			for _, s := range choice.Body {
  1592→				g.generateStatement(s)
  1593→			}
  1594→			g.writeLine("break")
  1595→
  1596→			g.indent--
  1597→		}
  1598→		g.writeLine("}")
  1599→
  1600→		g.indent--
  1601→		g.writeLine("}")
  1602→	} else {
  1603→		for i, choice := range stmt.Choices {
  1604→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1605→			if i == 0 {
  1606→				g.write("if ")
  1607→			} else {
  1608→				g.write("} else if ")
  1609→			}
  1610→			g.generateExpression(choice.Condition)
  1611→			g.write(" {\n")
  1612→			g.indent++
  1613→
  1614→			for _, s := range choice.Body {
  1615→				g.generateStatement(s)
  1616→			}
  1617→
  1618→			g.indent--
  1619→		}
  1620→		g.writeLine("}")
  1621→	}
  1622→}
  1623→
  1624→func (g *Generator) generateCaseStatement(stmt *ast.CaseStatement) {
  1625→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1626→	g.write("switch ")
  1627→	g.generateExpression(stmt.Selector)
  1628→	g.write(" {\n")
  1629→
  1630→	for _, choice := range stmt.Choices {
  1631→		if choice.IsElse {
  1632→			g.writeLine("default:")
  1633→		} else {
  1634→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1635→			g.write("case ")
  1636→			for i, val := range choice.Values {
  1637→				if i > 0 {
  1638→					g.write(", ")
  1639→				}
  1640→				g.generateExpression(val)
  1641→			}
  1642→			g.write(":\n")
  1643→		}
  1644→		g.indent++
  1645→		for _, s := range choice.Body {
  1646→			g.generateStatement(s)
  1647→		}
  1648→		g.indent--
  1649→	}
  1650→

Good. Now I have a clear picture. Let me plan the implementation:

Root cause: parseIfStatement() loop at line 2245 doesn't handle when curToken is IF (nested/replicated IF). parseExpression returns nil without advancing, and the loop has no progress guard, so it loops forever.

Fix strategy: 1. Add a NestedIf *IfStatement field to IfChoice for when a choice is actually a nested IF 2. In parseIfStatement, detect curToken == IF and recursively parse it 3. In codegen, handle nested IF: for non-replicated, inline the choices; for replicated, emit a loop with a _matched flag 4. Add a progress guard to the parseIfStatement loop as a safety net

Let me implement this:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
  "offset": 199,
  "limit": 15
}
   199→type IfStatement struct {
   200→	Token      lexer.Token // the IF token
   201→	Choices    []IfChoice
   202→	Replicator *Replicator // optional replicator for IF i = start FOR count
   203→}
   204→
   205→type IfChoice struct {
   206→	Condition Expression
   207→	Body      []Statement
   208→}
   209→
   210→func (i *IfStatement) statementNode()       {}
   211→func (i *IfStatement) TokenLiteral() string { return i.Token.Literal }
   212→
   213→// CaseStatement represents a CASE statement
✏️ Edit ast.go
/home/david/projects/code-associates/occam2go/ast/ast.go
type IfChoice struct {
	Condition Expression
	Body      []Statement
}
+
type IfChoice struct {
	Condition Expression
	Body      []Statement
	NestedIf  *IfStatement // non-nil when this choice is a nested/replicated IF
}
The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.

Now update the parser to detect nested IF:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 2244,
  "limit": 50
}
  2244→	// Parse if choices (condition -> body pairs)
  2245→	for !p.curTokenIs(lexer.EOF) {
  2246→		// Skip newlines
  2247→		for p.curTokenIs(lexer.NEWLINE) {
  2248→			p.nextToken()
  2249→		}
  2250→
  2251→		// Handle DEDENT tokens
  2252→		for p.curTokenIs(lexer.DEDENT) {
  2253→			if p.indentLevel < startLevel {
  2254→				return stmt
  2255→			}
  2256→			p.nextToken()
  2257→		}
  2258→
  2259→		// Skip any more newlines after DEDENT
  2260→		for p.curTokenIs(lexer.NEWLINE) {
  2261→			p.nextToken()
  2262→		}
  2263→
  2264→		if p.curTokenIs(lexer.EOF) {
  2265→			break
  2266→		}
  2267→
  2268→		if p.indentLevel < startLevel {
  2269→			break
  2270→		}
  2271→
  2272→		choice := ast.IfChoice{}
  2273→		choice.Condition = p.parseExpression(LOWEST)
  2274→
  2275→		// Skip newlines and expect INDENT for body
  2276→		for p.peekTokenIs(lexer.NEWLINE) {
  2277→			p.nextToken()
  2278→		}
  2279→
  2280→		if p.peekTokenIs(lexer.INDENT) {
  2281→			p.nextToken() // consume INDENT
  2282→			p.nextToken() // move to body
  2283→			choice.Body = p.parseBodyStatements()
  2284→		}
  2285→
  2286→		stmt.Choices = append(stmt.Choices, choice)
  2287→	}
  2288→
  2289→	return stmt
  2290→}
  2291→
  2292→func (p *Parser) parseCaseStatement() *ast.CaseStatement {
  2293→	stmt := &ast.CaseStatement{Token: p.curToken}
✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	// Parse if choices (condition -> body pairs)
	for !p.curTokenIs(lexer.EOF) {
		// Skip newlines
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		// Handle DEDENT tokens
		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		// Skip any more newlines after DEDENT
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		choice := ast.IfChoice{}
		choice.Condition = p.parseExpression(LOWEST)

		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			choice.Body = p.parseBodyStatements()
		}

		stmt.Choices = append(stmt.Choices, choice)
	}

	return stmt
}
+
	// Parse if choices (condition -> body pairs)
	for !p.curTokenIs(lexer.EOF) {
		// Skip newlines
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		// Handle DEDENT tokens
		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		// Skip any more newlines after DEDENT
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		// Safety guard: record position before parsing to detect no-progress
		prevToken := p.curToken
		prevPeek := p.peekToken

		choice := ast.IfChoice{}

		// Nested IF (plain or replicated) used as a choice within this IF
		if p.curTokenIs(lexer.IF) {
			nestedIf := p.parseIfStatement()
			choice.NestedIf = nestedIf
		} else {
			choice.Condition = p.parseExpression(LOWEST)

			// Skip newlines and expect INDENT for body
			for p.peekTokenIs(lexer.NEWLINE) {
				p.nextToken()
			}

			if p.peekTokenIs(lexer.INDENT) {
				p.nextToken() // consume INDENT
				p.nextToken() // move to body
				choice.Body = p.parseBodyStatements()
			}
		}

		stmt.Choices = append(stmt.Choices, choice)

		// No-progress guard: if we haven't moved, break to prevent infinite loop
		if p.curToken == prevToken && p.peekToken == prevPeek {
			break
		}
	}

	return stmt
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now update the codegen to handle nested IF choices. Let me also update the scanner functions that traverse the AST:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1551,
  "limit": 75
}
  1551→func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
  1552→	if stmt.Replicator != nil {
  1553→		// Replicated IF: IF i = start FOR count → for loop with break on first match
  1554→		v := stmt.Replicator.Variable
  1555→		if stmt.Replicator.Step != nil {
  1556→			counter := "_repl_" + v
  1557→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1558→			g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
  1559→			g.generateExpression(stmt.Replicator.Count)
  1560→			g.write(fmt.Sprintf("; %s++ {\n", counter))
  1561→			g.indent++
  1562→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1563→			g.write(fmt.Sprintf("%s := ", v))
  1564→			g.generateExpression(stmt.Replicator.Start)
  1565→			g.write(fmt.Sprintf(" + %s * ", counter))
  1566→			g.generateExpression(stmt.Replicator.Step)
  1567→			g.write("\n")
  1568→		} else {
  1569→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1570→			g.write(fmt.Sprintf("for %s := ", v))
  1571→			g.generateExpression(stmt.Replicator.Start)
  1572→			g.write(fmt.Sprintf("; %s < ", v))
  1573→			g.generateExpression(stmt.Replicator.Start)
  1574→			g.write(" + ")
  1575→			g.generateExpression(stmt.Replicator.Count)
  1576→			g.write(fmt.Sprintf("; %s++ {\n", v))
  1577→			g.indent++
  1578→		}
  1579→
  1580→		for i, choice := range stmt.Choices {
  1581→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1582→			if i == 0 {
  1583→				g.write("if ")
  1584→			} else {
  1585→				g.write("} else if ")
  1586→			}
  1587→			g.generateExpression(choice.Condition)
  1588→			g.write(" {\n")
  1589→			g.indent++
  1590→
  1591→			for _, s := range choice.Body {
  1592→				g.generateStatement(s)
  1593→			}
  1594→			g.writeLine("break")
  1595→
  1596→			g.indent--
  1597→		}
  1598→		g.writeLine("}")
  1599→
  1600→		g.indent--
  1601→		g.writeLine("}")
  1602→	} else {
  1603→		for i, choice := range stmt.Choices {
  1604→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1605→			if i == 0 {
  1606→				g.write("if ")
  1607→			} else {
  1608→				g.write("} else if ")
  1609→			}
  1610→			g.generateExpression(choice.Condition)
  1611→			g.write(" {\n")
  1612→			g.indent++
  1613→
  1614→			for _, s := range choice.Body {
  1615→				g.generateStatement(s)
  1616→			}
  1617→
  1618→			g.indent--
  1619→		}
  1620→		g.writeLine("}")
  1621→	}
  1622→}
  1623→
  1624→func (g *Generator) generateCaseStatement(stmt *ast.CaseStatement) {
  1625→	g.builder.WriteString(strings.Repeat("\t", g.indent))
Thinking

Now I need to update the codegen. The key issue is handling nested IF choices. Let me think about how the non-replicated case works.

For the non-replicated codegen (line 1602-1621), when we encounter a choice with a NestedIf:

Case 1: Non-replicated nested IF (no replicator on the nested IF)

IF
  x > 0
    body1
  IF
    y > 0
      body2
    z > 0
      body3
  TRUE
    body4

The nested IF's choices should be inlined into the parent's if-else chain:

if x > 0 {
    body1
} else if y > 0 {
    body2
} else if z > 0 {
    body3
} else if true {
    body4
}

Case 2: Replicated nested IF

IF
  x > 0
    body1
  IF i = 0 FOR n
    arr[i] > 0
      body2
  TRUE
    body3

This requires a flag variable to track whether the replicated loop matched anything, then fall through to the remaining choices if it didn't.

The tricky part is understanding occam's IF semantics properly—all choices get evaluated in order, including those from nested IFs, and the first one that succeeds gets executed. So when you have a replicated IF followed by more choices, I need to flatten that into a sequence where the loop conditions are tried first, and only if none match do we proceed to the subsequent conditions.

For the Go translation, I'm nesting the replicated loop inside an else block, using a flag to track whether any iteration matched, then checking remaining conditions only if the flag stays false. But this approach creates deep nesting quickly with multiple nested IFs, so I should explore a cleaner strategy instead.

I think the simplest solution is to inline non-replicated nested choices directly as additional else-if branches, while handling replicated nested IFs with a flag-based loop approach where unmatched conditions continue in a wrapped else block. The key insight is that replicated cases can't be flattened into a simple else-if chain since they're loops, so I need to group choices by whether they're separated by a replicated IF boundary.

Looking at the actual occam patterns from the course, the most common case is a replicated IF with a TRUE default—essentially a search loop that falls back if nothing matches. I can translate this to Go by setting a matched flag, iterating through the replicated choices, and wrapping the remaining choices in an if-not-matched block.

Generating the Go translation... I'm realizing that occam's IF construct works differently than a traditional if-else chain—it's really about finding the first matching guard and executing its body. When a replicated IF appears as one of the choices, I need to handle it specially: emit the replicated loop with a _matched flag, then wrap subsequent choices in if !_matched conditions. The cleanest approach is to use a general block-scoped strategy where I track whether anything has matched and conditionally execute each choice based on that flag.

But I'm noticing this would be a significant departure from the current clean if-else chain output, which is much more readable. I think a hybrid approach makes more sense: keep the simple if-else chain when there are no nested IF choices, and only switch to the flag-based approach when I actually have replicated nested IFs to handle.

Looking at the real occam examples, the second case is particularly tricky—it has a normal condition followed by a replicated IF, then more conditions after that. I need to figure out how to handle this mixed scenario in the Go output.

My approach is to process the choices sequentially: when I hit a nested replicated IF, all the choices before it become an if-else chain, and then I need to figure out how to integrate the remaining conditions. Let me work through this systematically to get the translation right. I'm working through a simpler approach: collect all flat choices first—keeping normal ones as-is and inlining non-replicated nested IFs—then handle replicated nested IFs as special segments that split the choice list and use a flag-based approach for the remaining choices.

Writing the flattening function... Writing the flag-based choice handler...

Actually, I'm realizing the if !_matched && true pattern for the default case is inelegant—I should handle the TRUE condition specially to avoid that redundancy, maybe by checking if it's the final default case and emitting it without the extra condition check.

Looking at the actual patterns in the codebase, I see two main scenarios: replicated IFs appearing as the first choice with TRUE as the default, and replicated IFs mixed with other conditions. The cleanest approach would be to emit preceding choices as a normal if-else chain, then handle the replicated IF separately, and finally emit any remaining choices inside an if !_matched block.

Generating Go code examples... I see the algorithm needs to handle multiple levels of replicated nested IFs recursively. I'm sketching out a recursive approach where I find the first replicated IF, emit the choices before it as a standard chain, then recursively process the remaining choices to handle any additional replicated IFs that might be nested deeper.

The key insight is using a _ifmatched flag at each level to track whether the replicated loop matched anything, so we can fall through to the next set of choices if needed. Though I'm realizing the flag naming could cause collisions if there are multiple levels...

actually, no—each nested block creates its own scope, so the names won't collide. Let me just implement this approach where I flatten non-replicated nested IFs first, then handle the resulting choice list with the replicated IF logic.

For now I'll handle the straightforward case and add a TODO for when a replicated IF itself contains nested IFs—that's an edge case I can address later.

Implementing the core change in generateIfStatement...

Writing the flattening logic... Writing the if-else chain logic... Still writing the loop logic... Still writing code generation logic...

I'm realizing there's a subtlety with nested replicated IFs—when they're inside another IF statement, I need to set _ifmatched = true and break out of the loop in each choice body, but the existing codegen for top-level replicated IFs already handles the break statement. The question is whether I need to adjust how this works when it's nested versus at the top level.

I'm also thinking about scope and naming—if I have multiple replicated IFs nested at the same level within a parent IF, they'd each use _ifmatched, but since they're in different block scopes, there shouldn't be a collision. However, I need to be careful about the case where multiple replicated IFs appear as siblings in the same parent IF, since they'd share the same scope and could conflict.

Actually, looking at how I'd structure this with Go's block scoping—the first replicated IF gets its own _ifmatched variable in its block, then the remaining choices go into an if !_ifmatched { ... } block where the second replicated IF would have its own _ifmatched in a different scope. This should work fine.

I'm confident enough in the approach now to start implementing. I'll handle the common patterns we've actually encountered in the course module for now, and I can extend it later if needed.

Now I need to implement the codegen changes. This is the most complex part. Let me replace the generateIfStatement function to handle nested IFs:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
	if stmt.Replicator != nil {
		// Replicated IF: IF i = start FOR count → for loop with break on first match
		v := stmt.Replicator.Variable
		if stmt.Replicator.Step != nil {
			counter := "_repl_" + v
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
			g.generateExpression(stmt.Replicator.Count)
			g.write(fmt.Sprintf("; %s++ {\n", counter))
			g.indent++
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			g.write(fmt.Sprintf("%s := ", v))
			g.generateExpression(stmt.Replicator.Start)
			g.write(fmt.Sprintf(" + %s * ", counter))
			g.generateExpression(stmt.Replicator.Step)
			g.write("\n")
		} else {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			g.write(fmt.Sprintf("for %s := ", v))
			g.generateExpression(stmt.Replicator.Start)
			g.write(fmt.Sprintf("; %s < ", v))
			g.generateExpression(stmt.Replicator.Start)
			g.write(" + ")
			g.generateExpression(stmt.Replicator.Count)
			g.write(fmt.Sprintf("; %s++ {\n", v))
			g.indent++
		}

		for i, choice := range stmt.Choices {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++

			for _, s := range choice.Body {
				g.generateStatement(s)
			}
			g.writeLine("break")

			g.indent--
		}
		g.writeLine("}")

		g.indent--
		g.writeLine("}")
	} else {
		for i, choice := range stmt.Choices {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++

			for _, s := range choice.Body {
				g.generateStatement(s)
			}

			g.indent--
		}
		g.writeLine("}")
	}
}
+
func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
	if stmt.Replicator != nil {
		// Replicated IF: IF i = start FOR count → for loop with break on first match
		g.generateReplicatedIfLoop(stmt, false)
	} else {
		// Flatten non-replicated nested IFs into the parent choice list
		choices := g.flattenIfChoices(stmt.Choices)
		g.generateIfChoiceChain(choices, true)
	}
}

// flattenIfChoices inlines choices from non-replicated nested IFs into a flat list.
// Replicated nested IFs are preserved as-is (they need special loop codegen).
func (g *Generator) flattenIfChoices(choices []ast.IfChoice) []ast.IfChoice {
	var flat []ast.IfChoice
	for _, c := range choices {
		if c.NestedIf != nil && c.NestedIf.Replicator == nil {
			// Non-replicated nested IF: inline its choices recursively
			flat = append(flat, g.flattenIfChoices(c.NestedIf.Choices)...)
		} else {
			flat = append(flat, c)
		}
	}
	return flat
}

// generateReplicatedIfLoop emits a for loop that breaks on first matching choice.
// When withinFlag is true, it sets _ifmatched = true before breaking.
func (g *Generator) generateReplicatedIfLoop(stmt *ast.IfStatement, withinFlag bool) {
	repl := stmt.Replicator
	v := repl.Variable
	if repl.Step != nil {
		counter := "_repl_" + v
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
		g.generateExpression(repl.Count)
		g.write(fmt.Sprintf("; %s++ {\n", counter))
		g.indent++
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		g.write(fmt.Sprintf("%s := ", v))
		g.generateExpression(repl.Start)
		g.write(fmt.Sprintf(" + %s * ", counter))
		g.generateExpression(repl.Step)
		g.write("\n")
	} else {
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		g.write(fmt.Sprintf("for %s := ", v))
		g.generateExpression(repl.Start)
		g.write(fmt.Sprintf("; %s < ", v))
		g.generateExpression(repl.Start)
		g.write(" + ")
		g.generateExpression(repl.Count)
		g.write(fmt.Sprintf("; %s++ {\n", v))
		g.indent++
	}

	for i, choice := range stmt.Choices {
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		if i == 0 {
			g.write("if ")
		} else {
			g.write("} else if ")
		}
		g.generateExpression(choice.Condition)
		g.write(" {\n")
		g.indent++

		for _, s := range choice.Body {
			g.generateStatement(s)
		}
		if withinFlag {
			g.writeLine("_ifmatched = true")
		}
		g.writeLine("break")

		g.indent--
	}
	g.writeLine("}")

	g.indent--
	g.writeLine("}")
}

// generateIfChoiceChain emits a chain of if/else-if for the given choices.
// When a replicated nested IF is encountered, it splits the chain and uses
// a _ifmatched flag to determine whether remaining choices should be tried.
func (g *Generator) generateIfChoiceChain(choices []ast.IfChoice, isFirst bool) {
	// Find first replicated nested IF
	replIdx := -1
	for i, c := range choices {
		if c.NestedIf != nil && c.NestedIf.Replicator != nil {
			replIdx = i
			break
		}
	}

	if replIdx == -1 {
		// No replicated nested IFs — simple if/else-if chain
		for i, choice := range choices {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 && isFirst {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++

			for _, s := range choice.Body {
				g.generateStatement(s)
			}

			g.indent--
		}
		if len(choices) > 0 {
			g.writeLine("}")
		}
		return
	}

	// Split at the replicated nested IF
	before := choices[:replIdx]
	replChoice := choices[replIdx]
	after := choices[replIdx+1:]

	// Emit choices before the replicated IF as a normal if-else chain
	if len(before) > 0 {
		for i, choice := range before {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 && isFirst {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++
			for _, s := range choice.Body {
				g.generateStatement(s)
			}
			g.indent--
		}
		// Open else block for the replicated IF + remaining choices
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		g.write("} else {\n")
		g.indent++
	}

	// Emit the replicated nested IF with a flag
	needFlag := len(after) > 0
	if needFlag {
		g.writeLine("_ifmatched := false")
	}
	g.generateReplicatedIfLoop(replChoice.NestedIf, needFlag)

	// Emit remaining choices inside if !_ifmatched (recursive for multiple)
	if len(after) > 0 {
		g.builder.WriteString(strings.Repeat("\t", g.indent))
		g.write("if !_ifmatched {\n")
		g.indent++
		g.generateIfChoiceChain(after, true) // recursive for remaining
		g.indent--
		g.writeLine("}")
	}

	if len(before) > 0 {
		g.indent--
		g.writeLine("}")
	}
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now I need to update the scanner functions (containsPar, containsPrint, containsTimer, containsStop) to handle the new NestedIf field in IfChoice. Let me find them:

Grep
{
  "pattern": "func.*contains(Par|Print|Timer|Stop)",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content"
}
186:func (g *Generator) containsPar(stmt ast.Statement) bool {
248:func (g *Generator) containsPrint(stmt ast.Statement) bool {
316:func (g *Generator) containsTimer(stmt ast.Statement) bool {
387:func (g *Generator) containsStop(stmt ast.Statement) bool {

Let me look at how these scanners handle IfStatement choices to see where I need to add NestedIf traversal:

Grep
{
  "pattern": "\\.Choices",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-B": 2,
  "-A": 8
}
221-		}
222-	case *ast.IfStatement:
223:		for _, choice := range s.Choices {
224-			for _, inner := range choice.Body {
225-				if g.containsPar(inner) {
226-					return true
227-				}
228-			}
229-		}
230-	case *ast.CaseStatement:
231:		for _, choice := range s.Choices {
232-			for _, inner := range choice.Body {
233-				if g.containsPar(inner) {
234-					return true
235-				}
236-			}
237-		}
238-	case *ast.VariantReceive:
239-		for _, c := range s.Cases {
--
289-		}
290-	case *ast.IfStatement:
291:		for _, choice := range s.Choices {
292-			for _, inner := range choice.Body {
293-				if g.containsPrint(inner) {
294-					return true
295-				}
296-			}
297-		}
298-	case *ast.CaseStatement:
299:		for _, choice := range s.Choices {
300-			for _, inner := range choice.Body {
301-				if g.containsPrint(inner) {
302-					return true
303-				}
304-			}
305-		}
306-	case *ast.VariantReceive:
307-		for _, c := range s.Cases {
--
360-		}
361-	case *ast.IfStatement:
362:		for _, choice := range s.Choices {
363-			for _, inner := range choice.Body {
364-				if g.containsTimer(inner) {
365-					return true
366-				}
367-			}
368-		}
369-	case *ast.CaseStatement:
370:		for _, choice := range s.Choices {
371-			for _, inner := range choice.Body {
372-				if g.containsTimer(inner) {
373-					return true
374-				}
375-			}
376-		}
377-	case *ast.VariantReceive:
378-		for _, c := range s.Cases {
--
428-		}
429-	case *ast.IfStatement:
430:		for _, choice := range s.Choices {
431-			for _, inner := range choice.Body {
432-				if g.containsStop(inner) {
433-					return true
434-				}
435-			}
436-		}
437-	case *ast.CaseStatement:
438:		for _, choice := range s.Choices {
439-			for _, inner := range choice.Body {
440-				if g.containsStop(inner) {
441-					return true
442-				}
443-			}
444-		}
445-	case *ast.VariantReceive:
446-		for _, c := range s.Cases {
--
504-		}
505-	case *ast.IfStatement:
506:		for _, choice := range s.Choices {
507-			if g.exprNeedsMath(choice.Condition) {
508-				return true
509-			}
510-			for _, inner := range choice.Body {
511-				if g.containsMostExpr(inner) {
512-					return true
513-				}
514-			}
--
518-			return true
519-		}
520:		for _, choice := range s.Choices {
521-			for _, v := range choice.Values {
522-				if g.exprNeedsMath(v) {
523-					return true
524-				}
525-			}
526-			for _, inner := range choice.Body {
527-				if g.containsMostExpr(inner) {
528-					return true
--
943-		}
944-	case *ast.IfStatement:
945:		for _, choice := range s.Choices {
946-			for _, inner := range choice.Body {
947-				g.collectChanProtocols(inner)
948-			}
949-		}
950-	case *ast.CaseStatement:
951:		for _, choice := range s.Choices {
952-			for _, inner := range choice.Body {
953-				g.collectChanProtocols(inner)
954-			}
955-		}
956-	case *ast.AltBlock:
957-		for _, c := range s.Cases {
958-			for _, inner := range c.Body {
959-				g.collectChanProtocols(inner)
--
999-		}
1000-	case *ast.IfStatement:
1001:		for _, choice := range s.Choices {
1002-			for _, inner := range choice.Body {
1003-				g.collectRecordVars(inner)
1004-			}
1005-		}
1006-	case *ast.CaseStatement:
1007:		for _, choice := range s.Choices {
1008-			for _, inner := range choice.Body {
1009-				g.collectRecordVars(inner)
1010-			}
1011-		}
1012-	case *ast.AltBlock:
1013-		for _, c := range s.Cases {
1014-			for _, inner := range c.Body {
1015-				g.collectRecordVars(inner)
--
1555-	} else {
1556-		// Flatten non-replicated nested IFs into the parent choice list
1557:		choices := g.flattenIfChoices(stmt.Choices)
1558-		g.generateIfChoiceChain(choices, true)
1559-	}
1560-}
1561-
1562-// flattenIfChoices inlines choices from non-replicated nested IFs into a flat list.
1563-// Replicated nested IFs are preserved as-is (they need special loop codegen).
1564-func (g *Generator) flattenIfChoices(choices []ast.IfChoice) []ast.IfChoice {
1565-	var flat []ast.IfChoice
--
1567-		if c.NestedIf != nil && c.NestedIf.Replicator == nil {
1568-			/ Non-replicated nested IF: inline its choices recursively
1569:			flat = append(flat, g.flattenIfChoices(c.NestedIf.Choices)...)
1570-		} else {
1571-			flat = append(flat, c)
1572-		}
1573-	}
1574-	return flat
1575-}
1576-
1577-// generateReplicatedIfLoop emits a for loop that breaks on first matching choice.
--
1605-	}
1606-
1607:	for i, choice := range stmt.Choices {
1608-		g.builder.WriteString(strings.Repeat("\t", g.indent))
1609-		if i == 0 {
1610-			g.write("if ")
1611-		} else {
1612-			g.write("} else if ")
1613-		}
1614-		g.generateExpression(choice.Condition)
1615-		g.write(" {\n")
--
1727-	g.write(" {\n")
1728-
1729:	for _, choice := range stmt.Choices {
1730-		if choice.IsElse {
1731-			g.writeLine("default:")
1732-		} else {
1733-			g.builder.WriteString(strings.Repeat("\t", g.indent))
1734-			g.write("case ")
1735-			for i, val := range choice.Values {
1736-				if i > 0 {
1737-					g.write(", ")

I need to add NestedIf traversal to all these scanner functions. For each place that iterates over IfStatement.Choices, I need to also recurse into choice.NestedIf if present. Let me update them all:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 222,
  "limit": 10
}
   222→	case *ast.IfStatement:
   223→		for _, choice := range s.Choices {
   224→			for _, inner := range choice.Body {
   225→				if g.containsPar(inner) {
   226→					return true
   227→				}
   228→			}
   229→		}
   230→	case *ast.CaseStatement:
   231→		for _, choice := range s.Choices {
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPar(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPar(inner) {
					return true
				}
			}
		}
+
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.NestedIf != nil {
				if g.containsPar(choice.NestedIf) {
					return true
				}
			}
			for _, inner := range choice.Body {
				if g.containsPar(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPar(inner) {
					return true
				}
			}
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 290,
  "limit": 10
}
   290→		for _, inner := range s.Body {
   291→			if g.containsPrint(inner) {
   292→				return true
   293→			}
   294→		}
   295→	case *ast.IfStatement:
   296→		for _, choice := range s.Choices {
   297→			for _, inner := range choice.Body {
   298→				if g.containsPrint(inner) {
   299→					return true
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPrint(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPrint(inner) {
					return true
				}
			}
		}
+
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.NestedIf != nil {
				if g.containsPrint(choice.NestedIf) {
					return true
				}
			}
			for _, inner := range choice.Body {
				if g.containsPrint(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPrint(inner) {
					return true
				}
			}
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 365,
  "limit": 14
}
   365→	case *ast.WhileLoop:
   366→		for _, inner := range s.Body {
   367→			if g.containsTimer(inner) {
   368→				return true
   369→			}
   370→		}
   371→	case *ast.IfStatement:
   372→		for _, choice := range s.Choices {
   373→			for _, inner := range choice.Body {
   374→				if g.containsTimer(inner) {
   375→					return true
   376→				}
   377→			}
   378→		}
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsTimer(inner) {
					return true
				}
			}
		}
+
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.NestedIf != nil {
				if g.containsTimer(choice.NestedIf) {
					return true
				}
			}
			for _, inner := range choice.Body {
				if g.containsTimer(inner) {
					return true
				}
			}
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 438,
  "limit": 14
}
   438→	case *ast.WhileLoop:
   439→		for _, inner := range s.Body {
   440→			if g.containsStop(inner) {
   441→				return true
   442→			}
   443→		}
   444→	case *ast.IfStatement:
   445→		for _, choice := range s.Choices {
   446→			for _, inner := range choice.Body {
   447→				if g.containsStop(inner) {
   448→					return true
   449→				}
   450→			}
   451→		}
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsStop(inner) {
					return true
				}
			}
		}
+
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.NestedIf != nil {
				if g.containsStop(choice.NestedIf) {
					return true
				}
			}
			for _, inner := range choice.Body {
				if g.containsStop(inner) {
					return true
				}
			}
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update containsMostExpr:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 517,
  "limit": 14
}
   517→		if g.exprNeedsMath(s.Condition) {
   518→			return true
   519→		}
   520→		for _, inner := range s.Body {
   521→			if g.containsMostExpr(inner) {
   522→				return true
   523→			}
   524→		}
   525→	case *ast.IfStatement:
   526→		for _, choice := range s.Choices {
   527→			if g.exprNeedsMath(choice.Condition) {
   528→				return true
   529→			}
   530→			for _, inner := range choice.Body {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 525,
  "limit": 15
}
   525→	case *ast.IfStatement:
   526→		for _, choice := range s.Choices {
   527→			if g.exprNeedsMath(choice.Condition) {
   528→				return true
   529→			}
   530→			for _, inner := range choice.Body {
   531→				if g.containsMostExpr(inner) {
   532→					return true
   533→				}
   534→			}
   535→		}
   536→	case *ast.CaseStatement:
   537→		if g.exprNeedsMath(s.Selector) {
   538→			return true
   539→		}
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if g.exprNeedsMath(choice.Condition) {
				return true
			}
			for _, inner := range choice.Body {
				if g.containsMostExpr(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		if g.exprNeedsMath(s.Selector) {
			return true
		}
+
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.NestedIf != nil {
				if g.containsMostExpr(choice.NestedIf) {
					return true
				}
			}
			if g.exprNeedsMath(choice.Condition) {
				return true
			}
			for _, inner := range choice.Body {
				if g.containsMostExpr(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		if g.exprNeedsMath(s.Selector) {
			return true
		}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update collectChanProtocols and collectRecordVars:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 960,
  "limit": 14
}
   960→		}
   961→	case *ast.FuncDecl:
   962→		for _, inner := range s.Body {
   963→			g.collectChanProtocols(inner)
   964→		}
   965→	case *ast.WhileLoop:
   966→		for _, inner := range s.Body {
   967→			g.collectChanProtocols(inner)
   968→		}
   969→	case *ast.IfStatement:
   970→		for _, choice := range s.Choices {
   971→			for _, inner := range choice.Body {
   972→				g.collectChanProtocols(inner)
   973→			}
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectChanProtocols(inner)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectChanProtocols(inner)
			}
		}
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				g.collectChanProtocols(inner)
+
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.NestedIf != nil {
				g.collectChanProtocols(choice.NestedIf)
			}
			for _, inner := range choice.Body {
				g.collectChanProtocols(inner)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectChanProtocols(inner)
			}
		}
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				g.collectChanProtocols(inner)
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1022,
  "limit": 14
}
  1022→			g.collectRecordVars(inner)
  1023→		}
  1024→	case *ast.WhileLoop:
  1025→		for _, inner := range s.Body {
  1026→			g.collectRecordVars(inner)
  1027→		}
  1028→	case *ast.IfStatement:
  1029→		for _, choice := range s.Choices {
  1030→			for _, inner := range choice.Body {
  1031→				g.collectRecordVars(inner)
  1032→			}
  1033→		}
  1034→	case *ast.CaseStatement:
  1035→		for _, choice := range s.Choices {
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectRecordVars(inner)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
+
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.NestedIf != nil {
				g.collectRecordVars(choice.NestedIf)
			}
			for _, inner := range choice.Body {
				g.collectRecordVars(inner)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now let me build and test:

$ Bash
Build to check for compile errors
go build ./... 2>&1

It builds. Let me test with the minimal reproducer first:

$ Bash
Test minimal replicated IF case
go build -o occam2go . && timeout 5 ./occam2go /tmp/test_repif4.occ 2>&1; echo "Exit: $?"
package main

func test() {
	var x int
	for i := 0; i < 0 + 3; i++ {
		if (i > 1) {
			x = i
			break
		}
	}
}

Exit: 0

No more hang. Let me test the more complex cases:

$ Bash
Test replicated IF with sibling branches
# Test replicated IF with TRUE default
timeout 5 ./occam2go /tmp/test_repif2.occ 2>&1; echo "---"; timeout 5 ./occam2go /tmp/test_boolfunc.occ 2>&1; echo "Exit: $?"
package main

func test(n int) {
	var x int
	_ifmatched := false
	for i := 0; i < 0 + n; i++ {
		if (i > 5) {
			x = i
			_ifmatched = true
			break
		}
	}
	if !_ifmatched {
		if true {
			x = 0
		}
	}
}

---
package main

func file.check.num.chars(s []byte, length int) bool {
	var ok bool
	if (((byte(48) > s[0]) || (s[0] > byte(57))) && ((s[0] != byte(43)) && (s[0] != byte(45)))) {
		ok = false
	} else {
		_ifmatched := false
		for i := 1; i < 1 + (length - 1); i++ {
			if ((byte(48) > s[i]) || (s[i] > byte(57))) {
				ok = false
				_ifmatched = true
				break
			}
		}
		if !_ifmatched {
			if (length > 1) {
				ok = true
			} else if (length == 1) {
				ok = ((byte(48) <= s[0]) && (s[0] <= byte(57)))
			} else if true {
				ok = false
			}
		}
	}
	return ok
}

Exit: 0

Excellent! Both work correctly. Now let me test the actual files that were hanging:

$ Bash
Test all course module files
for f in kroc/modules/course/libsrc/*.occ; do echo "=== Testing: $f ==="; timeout 5 ./occam2go -I kroc/modules/course/libsrc "$f" -o /dev/null 2>&1 | head -5; rc=${PIPESTATUS[0]}; if [ $rc -eq 124 ]; then echo "TIMEOUT (likely infinite loop)"; elif [ $rc -ne 0 ]; then echo "Exit code: $rc"; else echo "OK"; fi; echo; done
=== Testing: kroc/modules/course/libsrc/demo_cycles.occ ===
Parse errors:
  line 33: expected type in parameter, got IDENT
  line 33: expected ), got !
  line 33: unexpected token: !
  line 33: unexpected token: )
Exit code: 1

=== Testing: kroc/modules/course/libsrc/demo_nets.occ ===
Parse errors:
  line 46: expected type in parameter, got IDENT
  line 46: expected ), got !
  line 46: unexpected token: !
  line 46: unexpected token: )
Exit code: 1

=== Testing: kroc/modules/course/libsrc/file_in.occ ===
Parse errors:
  line 161: unexpected token in expression: NEWLINE
  line 161: expected ), got INDENT
  line 162: unexpected token: (
  line 162: unexpected token: =
Exit code: 1

=== Testing: kroc/modules/course/libsrc/float_io.occ ===
Parse errors:
  line 47: expected type in parameter, got RESULT
  line 47: expected ), got INT
  line 47: expected IDENT, got RESULT
  line 47: unexpected token: RESULT
Exit code: 1

=== Testing: kroc/modules/course/libsrc/random.occ ===
Parse errors:
  line 46: expected type in parameter, got IDENT
Exit code: 1

=== Testing: kroc/modules/course/libsrc/shared_screen.occ ===
Terminated
TIMEOUT (likely infinite loop)

=== Testing: kroc/modules/course/libsrc/string.occ ===
Parse errors:
  line 133: expected type in parameter, got IDENT
  line 182: expected type in parameter, got IDENT
  line 246: expected type in parameter, got IDENT
  line 267: expected type in parameter, got IDENT
Exit code: 1

=== Testing: kroc/modules/course/libsrc/stringbuf.occ ===
Parse errors:
  line 36: expected FROM, got FOR
  line 36: unexpected token: FOR
  line 36: unexpected token: (
  line 36: unexpected token: (
Exit code: 1

=== Testing: kroc/modules/course/libsrc/utils.occ ===
Parse errors:
  line 249: expected type after VAL, got [
  line 249: unexpected token: ]
  line 343: expected type in parameter, got IDENT
  line 343: expected ), got !
Exit code: 141

Good progress! Fixed 3 of the 4 hanging files (file_in.occ, string.occ, utils.occ now produce parse errors instead of hanging). But shared_screen.occ still hangs. Let me investigate that one:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/shared_screen.occ"
}
     1→--
     2→--      Shared screen
     3→--      Copyright (C) 1997  P.H. Welch
     4→--      Modified 2005 Jonathan Whiting, Fred Barnes
     5→--
     6→--	This library is free software; you can redistribute it and/or
     7→--	modify it under the terms of the GNU Lesser General Public
     8→--	License as published by the Free Software Foundation; either
     9→--	version 2 of the License, or (at your option) any later version.
    10→--
    11→--	This library is distributed in the hope that it will be useful,
    12→--	but WITHOUT ANY WARRANTY; without even the implied warranty of
    13→--	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    14→--	Lesser General Public License for more details.
    15→--
    16→--	You should have received a copy of the GNU Lesser General Public
    17→--	License along with this library; if not, write to the Free Software
    18→--	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
    19→--
    20→
    21→--** Shared screen library.
    22→--
    23→-- To use this library:
    24→--
    25→-- [@code
    26→-- #INCLUDE "shared_screen.module"]
    27→--
    28→-- @module shared_screen
    29→
    30→#INCLUDE "consts.inc"
    31→#INCLUDE "shared_screen.inc"
    32→#USE "course.lib"
    33→
    34→--{{{  PROC fg.col (VAL COLOUR c, CHAN BYTE out!)
    35→--* Set foreground colour.
    36→-- Generate a VT220 control sequence to set the terminal colour.
    37→-- @param c Colour to set; see [@ref COLOURS]
    38→-- @param out Channel to write to
    39→PROC fg.col (VAL COLOUR c, CHAN BYTE out!)
    40→  --{{{  
    41→  VAL COLOUR set.fg IS 30:
    42→  IF
    43→    c = default
    44→      SEQ
    45→        out ! ESCAPE
    46→        out.string ("[0m", 0, out!)
    47→        out ! FLUSH
    48→    c > bold.white
    49→      SKIP
    50→    TRUE
    51→      SEQ
    52→        out ! ESCAPE
    53→        out ! '['
    54→        IF
    55→          c > white
    56→            SEQ
    57→              out ! '1'
    58→              out ! ';'
    59→              out.byte (BYTE ((c - bold.black) + set.fg), 0, out!)
    60→          TRUE
    61→            SEQ
    62→              out ! '0'
    63→              out ! ';'
    64→              out.byte (BYTE (c + set.fg), 0, out!)
    65→        out ! 'm'
    66→        out ! FLUSH
    67→  --}}}
    68→:
    69→--}}}
    70→--{{{  PROC screen.interpret (VAL []SCREEN.NODE node, CHAN BYTE out!)
    71→--* Interpret an array of screen update commands.
    72→-- @param node Commands to interpret; see [@ref SCREEN.NODE]
    73→-- @param out Channel to write to
    74→PROC screen.interpret (VAL []SCREEN.NODE node, CHAN BYTE out!)
    75→  SEQ i = 0 FOR SIZE node
    76→    VAL SCREEN.NODE node IS node[i]:
    77→    CASE node[tag]
    78→      --{{{  coordinates.tag
    79→      coordinates.tag
    80→        cursor.x.y (node[a], node[b], out!)
    81→      --}}}
    82→      --{{{  colour.tag
    83→      colour.tag
    84→        fg.col (COLOUR node[a], out!)
    85→      --}}}
    86→      --{{{  byte.tag
    87→      byte.tag
    88→        out.byte (node[a], 0, out!)
    89→      --}}}
    90→      --{{{  character.tag
    91→      character.tag
    92→        out ! node[a]
    93→      --}}}
    94→      --{{{  left.tag
    95→      left.tag
    96→        cursor.left (node[a], out!)
    97→      --}}}
    98→      --{{{  right.tag
    99→      right.tag
   100→        cursor.right (node[a], out!)
   101→      --}}}
   102→      --{{{  up.tag
   103→      up.tag
   104→        cursor.up (node[a], out!)
   105→      --}}}
   106→      --{{{  down.tag
   107→      down.tag
   108→        cursor.down (node[a], out!)
   109→      --}}}
   110→      --{{{  draw.left.tag
   111→      draw.left.tag
   112→        SEQ i = 0 FOR INT node[b]
   113→          SEQ
   114→            out ! node[a]
   115→            cursor.left (2, out!)
   116→      --}}}
   117→      --{{{  draw.right.tag
   118→      draw.right.tag
   119→        SEQ i = 0 FOR INT node[b]
   120→          out ! node[a]
   121→      --}}}
   122→      --{{{  draw.up.tag
   123→      draw.up.tag
   124→        SEQ i = 0 FOR INT node[b]
   125→          SEQ
   126→            out ! node[a]
   127→            cursor.left (1, out!)
   128→            cursor.up (1, out!)
   129→      --}}}
   130→      --{{{  draw.down.tag
   131→      draw.down.tag
   132→        SEQ i = 0 FOR INT node[b]
   133→          SEQ
   134→            out ! node[a]
   135→            cursor.left (1, out!)
   136→            cursor.down (1, out!)
   137→      --}}}
   138→      --{{{  erase.eol.tag
   139→      erase.eol.tag
   140→        erase.eol (out!)
   141→      --}}}
   142→      --{{{  erase.bol.tag
   143→      erase.bol.tag
   144→        erase.bol (out!)
   145→      --}}}
   146→      --{{{  erase.line.tag
   147→      erase.line.tag
   148→        erase.line (out!)
   149→      --}}}
   150→      --{{{  erase.eos.tag
   151→      erase.eos.tag
   152→        erase.eos (out!)
   153→      --}}}
   154→      --{{{  erase.bos.tag
   155→      erase.bos.tag
   156→        erase.bos (out!)
   157→      --}}}
   158→      --{{{  erase.screen.tag
   159→      erase.screen.tag
   160→        erase.screen (out!)
   161→      --}}}
   162→      --{{{  cursor.visible.tag
   163→      cursor.visible.tag
   164→        SEQ
   165→          cursor.visible (out!)
   166→          out ! FLUSH
   167→      --}}}
   168→      --{{{  cursor.invisible.tag
   169→      cursor.invisible.tag
   170→        SEQ
   171→          cursor.invisible (out!)
   172→          out ! FLUSH
   173→      --}}}
   174→      --{{{  flush.tag
   175→      flush.tag
   176→        out ! FLUSH
   177→      --}}}
   178→:
   179→--}}}
   180→--{{{  PROC screen.driver (CHAN SCREEN.CONTROL in?, CHAN BYTE out!)
   181→--* Shared screen process.
   182→-- Manage a VT220 terminal display.
   183→-- @param in Command input channel (see [@ref SCREEN.CONTROL])
   184→-- @param out Channel to write to
   185→PROC screen.driver (CHAN SCREEN.CONTROL in?, CHAN BYTE out!)
   186→  --{{{  
   187→  BOOL running:
   188→  SEQ
   189→    running := TRUE
   190→    WHILE running
   191→      in ? CASE
   192→        --{{{  atomic
   193→        --{{{  coordinates
   194→        COORDINATES p:
   195→        coordinates; p
   196→          cursor.x.y (p[X], p[Y], out!)
   197→        --}}}
   198→        --{{{  colour
   199→        COLOUR c:
   200→        colour; c
   201→          fg.col (c, out!)
   202→        --}}}
   203→        --{{{  string
   204→        BYTE size:
   205→        [255]BYTE s:
   206→        string; size::s
   207→          out.string ([s FOR (INT size)], 0, out!)
   208→        --}}}
   209→        --{{{  byte
   210→        BYTE b:
   211→        byte; b
   212→          out.byte (b, 0, out!)
   213→        --}}}
   214→        --{{{  character
   215→        BYTE b:
   216→        character; b
   217→          out ! b
   218→        --}}}
   219→        --{{{  integer
   220→        INT n:
   221→        integer; n
   222→          out.int (n, 0, out!)
   223→        --}}}
   224→        --{{{  left
   225→        BYTE b:
   226→        left; b
   227→          cursor.left (b, out!)
   228→        --}}}
   229→        --{{{  right
   230→        BYTE b:
   231→        right; b
   232→          cursor.right (b, out!)
   233→        --}}}
   234→        --{{{  up
   235→        BYTE b:
   236→        up; b
   237→          cursor.up (b, out!)
   238→        --}}}
   239→        --{{{  down
   240→        BYTE b:
   241→        down; b
   242→          cursor.down (b, out!)
   243→        --}}}
   244→        --{{{  draw.left
   245→        BYTE b, n:
   246→        draw.left; b; n
   247→          SEQ i = 0 FOR INT n
   248→            SEQ
   249→              out ! b
   250→              cursor.left (2, out!)
   251→        --}}}
   252→        --{{{  draw.right
   253→        BYTE b, n:
   254→        draw.right; b; n
   255→          SEQ i = 0 FOR INT n
   256→            out ! b
   257→        --}}}
   258→        --{{{  draw.up
   259→        BYTE b, n:
   260→        draw.up; b; n
   261→          SEQ i = 0 FOR INT n
   262→            SEQ
   263→              out ! b
   264→              cursor.left (1, out!)
   265→              cursor.up (1, out!)
   266→        --}}}
   267→        --{{{  draw.down
   268→        BYTE b, n:
   269→        draw.down; b; n
   270→          SEQ i = 0 FOR INT n
   271→            SEQ
   272→              out ! b
   273→              cursor.left (1, out!)
   274→              cursor.down (1, out!)
   275→        --}}}
   276→        --{{{  erase.eol
   277→        erase.eol
   278→          erase.eol (out!)
   279→        --}}}
   280→        --{{{  erase.bol
   281→        erase.bol
   282→          erase.bol (out!)
   283→        --}}}
   284→        --{{{  erase.line
   285→        erase.line
   286→          erase.line (out!)
   287→        --}}}
   288→        --{{{  erase.eos
   289→        erase.eos
   290→          erase.eos (out!)
   291→        --}}}
   292→        --{{{  erase.bos
   293→        erase.bos
   294→          erase.bos (out!)
   295→        --}}}
   296→        --{{{  flush
   297→        flush
   298→          out ! FLUSH
   299→        --}}}
   300→        --}}}
   301→        --{{{  structured
   302→        --{{{  pcs
   303→        COORDINATES.COLOUR pc:
   304→        BYTE size:
   305→        [255]BYTE s:
   306→        pcs; pc; size::s
   307→          SEQ
   308→            cursor.x.y (pc[X], pc[Y], out!)
   309→            fg.col (pc[col], out!)
   310→            out.string ([s FOR (INT size)], 0, out!)
   311→            out ! FLUSH
   312→        --}}}
   313→        --{{{  pci
   314→        COORDINATES.COLOUR pc:
   315→        INT n:
   316→        pci; pc; n
   317→          SEQ
   318→            cursor.x.y (pc[X], pc[Y], out!)
   319→            fg.col (pc[col], out!)
   320→            out.int (n, 0, out!)
   321→            out ! FLUSH
   322→        --}}}
   323→        --{{{  pcsi
   324→        COORDINATES.COLOUR pc:
   325→        BYTE size:
   326→        [255]BYTE s:
   327→        INT n:
   328→        pcsi; pc; size::s; n
   329→          SEQ
   330→            cursor.x.y (pc[X], pc[Y], out!)
   331→            fg.col (pc[col], out!)
   332→            out.string ([s FOR (INT size)], 0, out!)
   333→            out.int (n, 0, out!)
   334→            out ! FLUSH
   335→        --}}}
   336→        --{{{  pcis
   337→        COORDINATES.COLOUR pc:
   338→        BYTE size:
   339→        [255]BYTE s:
   340→        INT n:
   341→        pcis; pc; n; size::s
   342→          SEQ
   343→            cursor.x.y (pc[X], pc[Y], out!)
   344→            fg.col (pc[col], out!)
   345→            out.int (n, 0, out!)
   346→            out.string ([s FOR (INT size)], 0, out!)
   347→            out ! FLUSH
   348→        --}}}
   349→        --{{{  pcb
   350→        COORDINATES.COLOUR pc:
   351→        BYTE b:
   352→        pcb; pc; b
   353→          SEQ
   354→            cursor.x.y (pc[X], pc[Y], out!)
   355→            fg.col (pc[col], out!)
   356→            out.byte (b, 0, out!)
   357→            out ! FLUSH
   358→        --}}}
   359→        --{{{  pcsb
   360→        COORDINATES.COLOUR pc:
   361→        BYTE size:
   362→        [255]BYTE s:
   363→        BYTE b:
   364→        pcsb; pc; size::s; b
   365→          SEQ
   366→            cursor.x.y (pc[X], pc[Y], out!)
   367→            fg.col (pc[col], out!)
   368→            out.string ([s FOR (INT size)], 0, out!)
   369→            out.byte (b, 0, out!)
   370→            out ! FLUSH
   371→        --}}}
   372→        --{{{  pcbs
   373→        COORDINATES.COLOUR pc:
   374→        BYTE size:
   375→        [255]BYTE s:
   376→        BYTE b:
   377→        pcbs; pc; b; size::s
   378→          SEQ
   379→            cursor.x.y (pc[X], pc[Y], out!)
   380→            fg.col (pc[col], out!)
   381→            out.byte (b, 0, out!)
   382→            out.string ([s FOR (INT size)], 0, out!)
   383→            out ! FLUSH
   384→        --}}}
   385→        --{{{  pc.draw.left
   386→        COORDINATES.COLOUR pc:
   387→        BYTE b, n:
   388→        pc.draw.left; pc; b; n
   389→          SEQ
   390→            cursor.x.y (pc[X], pc[Y], out!)
   391→            fg.col (pc[col], out!)
   392→            SEQ i = 0 FOR INT n
   393→              SEQ
   394→                out ! b
   395→                cursor.left (2, out!)
   396→            out ! FLUSH
   397→        --}}}
   398→        --{{{  pc.draw.right
   399→        COORDINATES.COLOUR pc:
   400→        BYTE b, n:
   401→        pc.draw.right; pc; b; n
   402→          SEQ
   403→            cursor.x.y (pc[X], pc[Y], out!)
   404→            fg.col (pc[col], out!)
   405→            SEQ i = 0 FOR INT n
   406→              out ! b
   407→            out ! FLUSH
   408→        --}}}
   409→        --{{{  pc.draw.up
   410→        COORDINATES.COLOUR pc:
   411→        BYTE b, n:
   412→        pc.draw.up; pc; b; n
   413→          SEQ
   414→            cursor.x.y (pc[X], pc[Y], out!)
   415→            fg.col (pc[col], out!)
   416→            SEQ i = 0 FOR INT n
   417→              SEQ
   418→                out ! b
   419→                cursor.left (1, out!)
   420→                cursor.up (1, out!)
   421→            out ! FLUSH
   422→        --}}}
   423→        --{{{  pc.draw.down
   424→        COORDINATES.COLOUR pc:
   425→        BYTE b, n:
   426→        pc.draw.down; pc; b; n
   427→          SEQ
   428→            cursor.x.y (pc[X], pc[Y], out!)
   429→            fg.col (pc[col], out!)
   430→            SEQ i = 0 FOR INT n
   431→              SEQ
   432→                out ! b
   433→                cursor.left (1, out!)
   434→                cursor.down (1, out!)
   435→            out ! FLUSH
   436→        --}}}
   437→        --{{{  c.erase.eol
   438→        COORDINATES p:
   439→        c.erase.eol; p
   440→          SEQ
   441→            cursor.x.y (p[X], p[Y], out!)
   442→            erase.eol (out!)
   443→            out ! FLUSH
   444→        --}}}
   445→        --{{{  c.erase.bol
   446→        COORDINATES p:
   447→        c.erase.bol; p
   448→          SEQ
   449→            cursor.x.y (p[X], p[Y], out!)
   450→            erase.bol (out!)
   451→            out ! FLUSH
   452→        --}}}
   453→        --{{{  c.erase.line
   454→        COORDINATES p:
   455→        c.erase.line; p
   456→          SEQ
   457→            cursor.x.y (p[X], p[Y], out!)
   458→            erase.line (out!)
   459→            out ! FLUSH
   460→        --}}}
   461→        --{{{  c.erase.eos
   462→        COORDINATES p:
   463→        c.erase.eos; p
   464→          SEQ
   465→            cursor.x.y (p[X], p[Y], out!)
   466→            erase.eos (out!)
   467→            out ! FLUSH
   468→        --}}}
   469→        --{{{  c.erase.bos
   470→        COORDINATES p:
   471→        c.erase.bos; p
   472→          SEQ
   473→            cursor.x.y (p[X], p[Y], out!)
   474→            erase.bos (out!)
   475→            out ! FLUSH
   476→        --}}}
   477→        --{{{  erase.screen
   478→        erase.screen
   479→          SEQ
   480→            erase.screen (out!)
   481→            out ! FLUSH
   482→        --}}}
   483→        --{{{  cursor.visible
   484→        cursor.visible
   485→          SEQ
   486→            cursor.visible (out!)
   487→            out ! FLUSH
   488→        --}}}
   489→        --{{{  cursor.invisible
   490→        cursor.invisible
   491→          SEQ
   492→            cursor.invisible (out!)
   493→            out ! FLUSH
   494→        --}}}
   495→        --{{{  interpret
   496→        [255]SCREEN.NODE node:
   497→        BYTE size:
   498→        interpret; size::node
   499→          SEQ
   500→            screen.interpret ([node FOR INT size], out!)
   501→            out ! FLUSH
   502→        --}}}
   503→        --{{{  poison
   504→        poison
   505→          SEQ
   506→            out ! FLUSH
   507→            running := FALSE
   508→        --}}}
   509→        --}}}
   510→  --}}}
   511→:
   512→--}}}
   513→
Thinking

The shared_screen.occ file uses constructs that the parser doesn't support: 1. VAL COLOUR c - custom type names (COLOUR, SCREEN.NODE, etc.) 2. in ? CASE - variant receive pattern 3. size::s - counted array protocol 4. COORDINATES.COLOUR - record types 5. Various custom protocol features

But the issue is it STILL hangs despite the progress guard. The hanging is likely in a different part of the parser, not the IF statement. Let me check what construct causes it.

Looking at the code: - Line 39: PROC fg.col (VAL COLOUR c, CHAN BYTE out!) - COLOUR is not a built-in type

  • The parser would fail at COLOUR since it's not recognized as a type

The file starts with includes and library usage, but the shared_screen.inc file probably defines custom types that the parser doesn't know about. Even if parsing those fails, it shouldn't cause an infinite loop—it should produce errors and terminate. I need to figure out what's actually triggering the hang.

The shared_screen.occ file uses custom types (COLOUR, SCREEN.NODE, etc.) and complex protocol features. Let me check what the include file defines and try to isolate the remaining hang:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/shared_screen.inc"
}
     1→--
     2→--      Shared screen definitions
     3→--      Copyright (C) 1997  P.H. Welch
     4→--      Modified 2005 Jonathan Whiting, Fred Barnes
     5→--
     6→--	This library is free software; you can redistribute it and/or
     7→--	modify it under the terms of the GNU Lesser General Public
     8→--	License as published by the Free Software Foundation; either
     9→--	version 2 of the License, or (at your option) any later version.
    10→--
    11→--	This library is distributed in the hope that it will be useful,
    12→--	but WITHOUT ANY WARRANTY; without even the implied warranty of
    13→--	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    14→--	Lesser General Public License for more details.
    15→--
    16→--	You should have received a copy of the GNU Lesser General Public
    17→--	License along with this library; if not, write to the Free Software
    18→--	Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
    19→--
    20→
    21→--** @module shared_screen
    22→
    23→--{{{  DATA TYPE COLOUR
    24→--* A colour.
    25→DATA TYPE COLOUR IS BYTE:
    26→--*{{{  COLOURS Colour constants
    27→VAL COLOUR default IS 255:        --* Default colour
    28→VAL COLOUR black IS 0:            --* Black
    29→VAL COLOUR red IS 1:              --* Red
    30→VAL COLOUR green IS 2:            --* Green
    31→VAL COLOUR yellow IS 3:           --* Yellow
    32→VAL COLOUR blue IS 4:             --* Blue
    33→VAL COLOUR magenta IS 5:          --* Magenta
    34→VAL COLOUR cyan IS 6:             --* Cyan
    35→VAL COLOUR white IS 7:            --* White
    36→VAL COLOUR bold.black IS 8:       --* Bold black (grey)
    37→VAL COLOUR bold.red IS 9:         --* Bold red
    38→VAL COLOUR bold.green IS 10:      --* Bold green
    39→VAL COLOUR bold.yellow IS 11:     --* Bold yellow
    40→VAL COLOUR bold.blue IS 12:       --* Bold blue
    41→VAL COLOUR bold.magenta IS 13:    --* Bold magenta
    42→VAL COLOUR bold.cyan IS 14:       --* Bold cyan
    43→VAL COLOUR bold.white IS 15:      --* Bold white
    44→--*}}}
    45→--}}}
    46→--{{{  DATA TYPE COORDINATES
    47→--* A pair of coordinates.
    48→DATA TYPE COORDINATES
    49→  RECORD
    50→    BYTE X:                  --* X coordinate
    51→    BYTE Y:                  --* Y coordinate
    52→:
    53→--}}}
    54→--{{{  DATA TYPE COORDINATES.COLOUR
    55→--* A pair of coordinates and a colour.
    56→DATA TYPE COORDINATES.COLOUR
    57→  RECORD
    58→    BYTE X:                  --* X coordinate
    59→    BYTE Y:                  --* Y coordinate
    60→    COLOUR col:              --* Colour
    61→:
    62→--}}}
    63→--{{{  DATA TYPE SCREEN.NODE
    64→--*{{{  SCREEN.COMMAND Command tags for [@ref SCREEN.NODE].
    65→VAL BYTE coordinates.tag IS 0:
    66→VAL BYTE colour.tag IS 1:
    67→VAL BYTE byte.tag IS 2:
    68→VAL BYTE character.tag IS 3:
    69→VAL BYTE left.tag IS 4:
    70→VAL BYTE right.tag IS 5:
    71→VAL BYTE up.tag IS 6:
    72→VAL BYTE down.tag IS 7:
    73→VAL BYTE draw.left.tag IS 8:
    74→VAL BYTE draw.right.tag IS 9:
    75→VAL BYTE draw.up.tag IS 10:
    76→VAL BYTE draw.down.tag IS 11:
    77→VAL BYTE erase.eol.tag IS 12:
    78→VAL BYTE erase.bol.tag IS 13:
    79→VAL BYTE erase.line.tag IS 14:
    80→VAL BYTE erase.eos.tag IS 15:
    81→VAL BYTE erase.bos.tag IS 16:
    82→VAL BYTE erase.screen.tag IS 17:
    83→VAL BYTE cursor.visible.tag IS 18:
    84→VAL BYTE cursor.invisible.tag IS 19:
    85→VAL BYTE flush.tag IS 20:
    86→--*}}}
    87→
    88→--* A batch instruction to the shared screen.
    89→-- This is really a union type; the argument tags have different meanings
    90→-- depending on the command.
    91→DATA TYPE SCREEN.NODE
    92→  RECORD
    93→    --* The command to execute.
    94→    -- See [@ref SCREEN.COMMAND].
    95→    BYTE tag:
    96→    --* First argument
    97→    BYTE a:
    98→    --* Second argument
    99→    BYTE b:
   100→:
   101→--}}}
   102→
   103→--{{{  PROTOCOL SCREEN.CONTROL
   104→-- FIXME OccamDoc here is rather incomplete
   105→--* Control protocol for [@ref screen.driver].
   106→PROTOCOL SCREEN.CONTROL
   107→  CASE
   108→    --{{{  atomic
   109→    coordinates; COORDINATES --* Set coordinates
   110→    colour; COLOUR           --* Set colour
   111→    string; BYTE::[]BYTE     --* Draw a string
   112→    integer; INT             --* Draw an integer
   113→    byte; BYTE               --* Draw a byte
   114→    character; BYTE          --* Draw a character
   115→    left; BYTE               --* Move left
   116→    right; BYTE              --* Move right
   117→    up; BYTE                 --* Move up
   118→    down; BYTE               --* Move down
   119→    draw.left; BYTE; BYTE    --* Move left drawing character
   120→    draw.right; BYTE; BYTE   --* Move right drawing character
   121→    draw.up; BYTE; BYTE      --* Move up drawing character
   122→    draw.down; BYTE; BYTE    --* Move down drawing character
   123→    erase.eol                --* Erase to end of line
   124→    erase.bol                --* Erase to beginning of line
   125→    erase.line               --* Erase whole line
   126→    erase.eos                --* Erase to end of screen
   127→    erase.bos                --* Erase to beginning of screen
   128→    flush                    --* Force a screen update
   129→    --}}}
   130→    --{{{  structured
   131→    pcs; COORDINATES.COLOUR; BYTE::[]BYTE
   132→    pci; COORDINATES.COLOUR; INT
   133→    pcb; COORDINATES.COLOUR; BYTE
   134→    pcsi; COORDINATES.COLOUR; BYTE::[]BYTE; INT
   135→    pcis; COORDINATES.COLOUR; INT; BYTE::[]BYTE
   136→    pcsb; COORDINATES.COLOUR; BYTE::[]BYTE; BYTE
   137→    pcbs; COORDINATES.COLOUR; BYTE; BYTE::[]BYTE
   138→    pc.draw.left; COORDINATES.COLOUR; BYTE; BYTE
   139→    pc.draw.right; COORDINATES.COLOUR; BYTE; BYTE
   140→    pc.draw.up; COORDINATES.COLOUR; BYTE; BYTE
   141→    pc.draw.down; COORDINATES.COLOUR; BYTE; BYTE
   142→    c.erase.eol; COORDINATES      --* Move then erase to end of line
   143→    c.erase.bol; COORDINATES      --* Move then erase to beginning of line
   144→    c.erase.line; COORDINATES     --* Move then erase whole line
   145→    c.erase.eos; COORDINATES      --* Move then erase to end of screen
   146→    c.erase.bos; COORDINATES      --* Move then erase to beginning of screen
   147→    erase.screen             --* Clear the screen
   148→    cursor.visible           --* Make the cursor visible
   149→    cursor.invisible         --* Make the cursor invisible
   150→    --* Interpret a list of commands.
   151→    -- See [@ref SCREEN.NODE].
   152→    interpret; BYTE::[]SCREEN.NODE
   153→    poison                   --* Exit
   154→    --}}}
   155→:
   156→--}}}
   157→
   158→
Thinking

This file uses DATA TYPE declarations which the parser doesn't support. The DATA TYPE COLOUR IS BYTE: is a type alias. The DATA TYPE COORDINATES RECORD ... are record definitions. The PROTOCOL SCREEN.CONTROL CASE ... uses a variant protocol with BYTE::[]BYTE counted array types.

The hang in shared_screen.occ is likely caused by a different parsing issue - probably something related to parsing the unsupported DATA TYPE or PROTOCOL CASE with BYTE::[]BYTE counted arrays, or the CASE node[tag] construct.

Let me try to isolate the specific construct that causes the hang.

Since the includes will fail with unsupported constructs, I should test running it without them to see where the hang actually occurs. The preprocessor would try to include shared_screen.inc and consts.inc from the libsrc directory with the -I path I provided, so those files would be found and processed.

The parser would then encounter DATA TYPE and PROTOCOL declarations it doesn't recognize—DATA TYPE COLOUR IS BYTE would fail since the parser doesn't know DATA as a keyword. Let me create a minimal test case with just the problematic constructs to isolate the issue.

$ Bash
Test nested IF pattern from shared_screen
# Try without includes to narrow down the problem
cat > /tmp/test_screen.occ << 'EOF'
PROC fg.col (VAL BYTE c, CHAN BYTE out!)
  VAL BYTE set.fg IS 30:
  IF
    c = 255
      SEQ
        out ! 27
    c > 15
      SKIP
    TRUE
      SEQ
        out ! 27
        out ! '['
        IF
          c > 7
            SEQ
              out ! '1'
              out ! ';'
          TRUE
            SEQ
              out ! '0'
              out ! ';'
        out ! 'm'
:
EOF
timeout 5 ./occam2go /tmp/test_screen.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func fg.col(c byte, out chan<- byte) {
	set.fg := 30
	if (c == 255) {
		out <- 27
	} else if (c > 15) {
		// SKIP
	} else if true {
		out <- 27
		out <- byte(91)
		if (c > 7) {
			out <- byte(49)
			out <- byte(59)
		} else if true {
			out <- byte(48)
			out <- byte(59)
		}
		out <- byte(109)
	}
}

Exit: 0

That works. The issue is somewhere else in the file. Let me try the actual file directly (without includes) to narrow it down:

$ Bash
Test shared_screen without include paths
# Try without includes - will fail to find files but let's see if parser itself hangs
timeout 5 ./occam2go kroc/modules/course/libsrc/shared_screen.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Still hangs even without includes. So the include file content isn't the trigger - it's something in shared_screen.occ itself. Let me use binary search to find the problematic section:

$ Bash
Test first proc of shared_screen
# Test first half of shared_screen.occ (first proc only, lines 1-69)
head -69 kroc/modules/course/libsrc/shared_screen.occ > /tmp/test_screen_half1.occ
timeout 5 ./occam2go /tmp/test_screen_half1.occ -o /dev/null 2>&1; echo "Exit: $?"
Preprocessor error: line 30: cannot find included file "consts.inc"
Exit: 1
$ Bash
Test shared_screen without include directives
# Remove the include directives
sed '/#INCLUDE\|#USE/d' kroc/modules/course/libsrc/shared_screen.occ > /tmp/test_screen_noinc.occ
timeout 5 ./occam2go /tmp/test_screen_noinc.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124
$ Bash
Test first proc only
# First proc only (without includes)
sed '/#INCLUDE\|#USE/d' kroc/modules/course/libsrc/shared_screen.occ | head -60 > /tmp/test_s1.occ
timeout 5 ./occam2go /tmp/test_s1.occ -o /dev/null 2>&1; echo "Exit: $?"
Parse errors:
  line 35: expected type in parameter, got IDENT
  line 35: expected ), got IDENT
  line 35: unexpected token: !
  line 35: unexpected token: )
  line 37: expected type after VAL, got IDENT
  line 37: unexpected token: IS
  line 37: unexpected token: INT
  line 37: unexpected token: :
Exit: 1

First proc doesn't hang (just parse errors for COLOUR type). The hang is in the second or third proc.

$ Bash
Test first two procs
# Second proc - screen.interpret (lines ~70-178)
sed '/#INCLUDE\|#USE/d' kroc/modules/course/libsrc/shared_screen.occ | head -170 > /tmp/test_s2.occ
timeout 5 ./occam2go /tmp/test_s2.occ -o /dev/null 2>&1 | head -5; echo "Exit: ${PIPESTATUS[0]}"
Parse errors:
  line 35: expected type in parameter, got IDENT
  line 35: expected ), got IDENT
  line 35: unexpected token: !
  line 35: unexpected token: )
Exit: 1
$ Bash
Test third proc only
# Third proc - screen.driver (lines ~180-512)
sed '/#INCLUDE\|#USE/d' kroc/modules/course/libsrc/shared_screen.occ | tail -n +175 > /tmp/test_s3.occ
timeout 5 ./occam2go /tmp/test_s3.occ -o /dev/null 2>&1 | head -5; echo "Exit: ${PIPESTATUS[0]}"
Terminated
Exit: 124

The third proc (screen.driver) causes the hang. This proc uses in ? CASE (variant receive). Let me narrow it further:

$ Bash
Test variant receive pattern
# Simplified version of screen.driver
cat > /tmp/test_varrecv.occ << 'EOF'
PROC screen.driver (CHAN INT in?)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        INT p:
        coordinates; p
          SKIP
        INT c:
        colour; c
          SKIP
:
EOF
timeout 5 ./occam2go /tmp/test_varrecv.occ -o /dev/null 2>&1; echo "Exit: $?"
Parse errors:
  line 7: expected variant tag name, got INT
  line 7: unexpected token: :
  line 8: unexpected token: ;
  line 11: unexpected token: ;
Exit: 1

That gives parse errors without hanging. Let me try the actual file structure more closely:

$ Bash
Test first part of screen.driver
# Narrow down: take lines 180-300 from original file (no includes)
sed '/#INCLUDE\|#USE/d' kroc/modules/course/libsrc/shared_screen.occ | sed -n '175,300p' > /tmp/test_s3a.occ
timeout 5 ./occam2go /tmp/test_s3a.occ -o /dev/null 2>&1 | head -5; echo "Exit: ${PIPESTATUS[0]}"
Terminated
Exit: 124
$ Bash
Test minimal screen.driver variant receive
# Even shorter - just the proc header and WHILE with variant receive
sed '/#INCLUDE\|#USE/d' kroc/modules/course/libsrc/shared_screen.occ | sed -n '175,210p' > /tmp/test_s3b.occ
cat /tmp/test_s3b.occ
echo "---"
timeout 5 ./occam2go /tmp/test_s3b.occ -o /dev/null 2>&1 | head -10; echo "Exit: ${PIPESTATUS[0]}"
--}}}
--{{{  PROC screen.driver (CHAN SCREEN.CONTROL in?, CHAN BYTE out!)
--* Shared screen process.
-- Manage a VT220 terminal display.
-- @param in Command input channel (see [@ref SCREEN.CONTROL])
-- @param out Channel to write to
PROC screen.driver (CHAN SCREEN.CONTROL in?, CHAN BYTE out!)
  --{{{  
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        --{{{  atomic
        --{{{  coordinates
        COORDINATES p:
        coordinates; p
          cursor.x.y (p[X], p[Y], out!)
        --}}}
        --{{{  colour
        COLOUR c:
        colour; c
          fg.col (c, out!)
        --}}}
        --{{{  string
        BYTE size:
        [255]BYTE s:
        string; size::s
          out.string ([s FOR (INT size)], 0, out!)
        --}}}
        --{{{  byte
        BYTE b:
        byte; b
          out.byte (b, 0, out!)
        --}}}
        --{{{  character
---
Terminated
Exit: 124
$ Bash
Minimal variant receive with declaration
# Even more minimal
cat > /tmp/test_varrecv2.occ << 'EOF'
PROC test (CHAN INT in?)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        INT p:
        foo; p
          SKIP
:
EOF
timeout 5 ./occam2go /tmp/test_varrecv2.occ -o /dev/null 2>&1; echo "Exit: $?"
Parse errors:
  line 7: expected variant tag name, got INT
  line 7: unexpected token: :
  line 8: unexpected token: ;
Exit: 1

That doesn't hang. The difference must be the CHAN SCREEN.CONTROL type or something else. Let me try with a type the parser won't understand:

$ Bash
Test with unrecognized channel type
cat > /tmp/test_varrecv3.occ << 'EOF'
PROC screen.driver (CHAN SCREEN.CONTROL in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        INT p:
        foo; p
          SKIP
:
EOF
timeout 5 ./occam2go /tmp/test_varrecv3.occ -o /dev/null 2>&1; echo "Exit: $?"
Parse errors:
  line 7: expected variant tag name, got INT
  line 7: unexpected token: :
  line 8: unexpected token: ;
Exit: 1
$ Bash
Test many variant receive cases
# The actual file has SCREEN.CONTROL as chan type and lots of variant cases
# Let me try with many variant cases
cat > /tmp/test_varrecv4.occ << 'EOF'
PROC screen.driver (CHAN INT in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        INT p:
        coordinates; p
          SKIP
        INT c:
        colour; c
          SKIP
        BYTE size:
        [255]BYTE s:
        mystring; size
          SKIP
        BYTE b:
        mybyte; b
          SKIP
        BYTE b:
        character; b
          out ! b
        INT n:
        integer; n
          SKIP
        BYTE b:
        left; b
          SKIP
        BYTE b:
        right; b
          SKIP
        BYTE b:
        up; b
          SKIP
        BYTE b:
        down; b
          SKIP
:
EOF
timeout 5 ./occam2go /tmp/test_varrecv4.occ -o /dev/null 2>&1 | head -15; echo "Exit: ${PIPESTATUS[0]}"
Parse errors:
  line 7: expected variant tag name, got INT
  line 7: unexpected token: :
  line 8: unexpected token: ;
  line 11: unexpected token: ;
  line 15: unexpected token: ;
  line 18: unexpected token: ;
  line 21: unexpected token: ;
  line 24: unexpected token: ;
  line 27: unexpected token: ;
  line 30: unexpected token: ;
  line 33: unexpected token: ;
  line 36: unexpected token: ;
Exit: 1

That doesn't hang either. The original file has additional complexity. Let me try with the exact original file content but truncated at different points:

$ Bash
Test variant with comma-separated declarations
# Let me add more matching cases from the actual file
# The actual has "draw.left; b; n" with TWO declarations before the tag
cat > /tmp/test_varrecv5.occ << 'EOF'
PROC test (CHAN INT in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        BYTE b:
        mybyte; b
          out ! b
        BYTE b, n:
        draw.left; b; n
          SEQ i = 0 FOR INT n
            SEQ
              out ! b
:
EOF
timeout 5 ./occam2go /tmp/test_varrecv5.occ -o /dev/null 2>&1; echo "Exit: $?"
Parse errors:
  line 7: expected variant tag name, got BYTE
  line 7: unexpected token: :
  line 8: unexpected token: ;
  line 11: unexpected token: ;
  line 11: unexpected token: ;
Exit: 1
$ Bash
Test variant with no-arg tags
# Let me try with erase.eol tag (no args - just tag name matches an identifier)
cat > /tmp/test_varrecv6.occ << 'EOF'
PROC test (CHAN INT in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        erase.eol
          SKIP
        erase.bol
          SKIP
        erase.line
          SKIP
        erase.screen
          SKIP
        cursor.visible
          SKIP
        cursor.invisible
          SKIP
        poison
          SEQ
            running := FALSE
:
EOF
timeout 5 ./occam2go /tmp/test_varrecv6.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func test(in <-chan int, out chan<- byte) {
	var running bool
	running = true
	for running {
		switch _v := (<-in).(type) {
		case _proto__erase.eol:
			// SKIP
		case _proto__erase.bol:
			// SKIP
		case _proto__erase.line:
			// SKIP
		case _proto__erase.screen:
			// SKIP
		case _proto__cursor.visible:
			// SKIP
		case _proto__cursor.invisible:
			// SKIP
		case _proto__poison:
			running = false
		}
	}
}

Exit: 0
Thinking

That works fine. So the issue is the combination of variant cases with declarations and variant cases without declarations. Let me try combining them.

Wait, let me re-examine the actual file more carefully. The in ? CASE block in the actual file has sections grouped by fold comments. Let me reproduce it more precisely.

Actually, let me look more carefully. The WHILE loop body is in ? CASE with many variant cases. Some of them have local declarations (like INT p: before coordinates; p), and some don't (like erase.eol). The first group I tested (with INT declarations before tags) gives parse errors but doesn't hang.

Now I'll test the exact pattern from the actual file—mixing variant cases with and without declarations to see if that's what triggers the hang.

$ Bash
Test mixed variant cases
# Mix: variant cases with and without declarations
cat > /tmp/test_varrecv7.occ << 'EOF'
PROC test (CHAN INT in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        INT p:
        coordinates; p
          SKIP
        erase.eol
          SKIP
        poison
          running := FALSE
:
EOF
timeout 5 ./occam2go /tmp/test_varrecv7.occ -o /dev/null 2>&1; echo "Exit: $?"
Parse errors:
  line 7: expected variant tag name, got INT
  line 7: unexpected token: :
  line 8: unexpected token: ;
Exit: 1

Doesn't hang. Let me go back to the actual file. The issue might be that after parse errors from unsupported constructs, the parser gets stuck somewhere. Let me try extracting the precise structure of the shared_screen.occ third proc, with all the fold markers:

$ Bash
Test with fold markers in variant cases
# Try the exact lines from the original file but just a portion
# Lines 181-210 of the original (after removing includes = 178-207)
cat > /tmp/test_exact.occ << 'EOF'
PROC screen.driver (CHAN INT in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        --{{{  atomic
        --{{{  coordinates
        COORDINATES p:
        coordinates; p
          SKIP
        --}}}
        --{{{  colour
        COLOUR c:
        colour; c
          SKIP
        --}}}
        --{{{  erase.eol
        erase.eol
          SKIP
        --}}}
        --{{{  flush
        flush
          out ! 255
        --}}}
        --}}}
        --{{{  structured
        --{{{  poison
        poison
          SEQ
            out ! 255
            running := FALSE
        --}}}
        --}}}
:
EOF
timeout 5 ./occam2go /tmp/test_exact.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Hangs! Now let me narrow down:

$ Bash
Test variant without unsupported types
# Remove the unsupported COORDINATES/COLOUR types
cat > /tmp/test_exact2.occ << 'EOF'
PROC screen.driver (CHAN INT in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        erase.eol
          SKIP
        flush
          out ! 255
        poison
          SEQ
            out ! 255
            running := FALSE
:
EOF
timeout 5 ./occam2go /tmp/test_exact2.occ -o /dev/null 2>&1; echo "Exit: $?"
package main

func screen.driver(in <-chan int, out chan<- byte) {
	var running bool
	running = true
	for running {
		switch _v := (<-in).(type) {
		case _proto__erase.eol:
			// SKIP
		case _proto__flush:
			out <- 255
		case _proto__poison:
			out <- 255
			running = false
		}
	}
}

Exit: 0

Doesn't hang. The hang is triggered by the COORDINATES p: / COLOUR c: lines, which are typed declarations using unknown types. Let me add those back:

$ Bash
Test with unknown type in variant case
# Add one unknown-type declaration
cat > /tmp/test_exact3.occ << 'EOF'
PROC screen.driver (CHAN INT in?, CHAN BYTE out!)
  BOOL running:
  SEQ
    running := TRUE
    WHILE running
      in ? CASE
        COORDINATES p:
        coordinates; p
          SKIP
        erase.eol
          SKIP
        poison
          running := FALSE
:
EOF
timeout 5 ./occam2go /tmp/test_exact3.occ -o /dev/null 2>&1; echo "Exit: $?"
Exit: 124

Hangs. Now let me understand what happens. COORDINATES is not a keyword, so the lexer treats it as an IDENT. Let me check what the parser does when it encounters COORDINATES p: inside a variant receive CASE block. Let me look at parseVariantReceive:

Grep
{
  "pattern": "func.*parseVariantReceive|parseAltCase",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content"
}
1193:func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {
1281:func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndex ast.Expression, token lexer.Token) *ast.VariantReceive {
1482:	block.Cases = p.parseAltCases()
1487:func (p *Parser) parseAltCases() []ast.AltCase {
1521:		altCase := p.parseAltCase()
1530:func (p *Parser) parseAltCase() *ast.AltCase {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1193,
  "limit": 90
}
  1193→func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {
  1194→	stmt := &ast.VariantReceive{
  1195→		Token:   token,
  1196→		Channel: channel,
  1197→	}
  1198→
  1199→	// Skip to next line
  1200→	for p.peekTokenIs(lexer.NEWLINE) {
  1201→		p.nextToken()
  1202→	}
  1203→
  1204→	// Expect INDENT
  1205→	if !p.peekTokenIs(lexer.INDENT) {
  1206→		p.addError("expected indented block after ? CASE")
  1207→		return stmt
  1208→	}
  1209→	p.nextToken() // consume INDENT
  1210→	startLevel := p.indentLevel
  1211→	p.nextToken() // move into block
  1212→
  1213→	// Parse variant cases (similar to parseCaseStatement pattern)
  1214→	for !p.curTokenIs(lexer.EOF) {
  1215→		// Skip newlines
  1216→		for p.curTokenIs(lexer.NEWLINE) {
  1217→			p.nextToken()
  1218→		}
  1219→
  1220→		// Handle DEDENT tokens
  1221→		for p.curTokenIs(lexer.DEDENT) {
  1222→			if p.indentLevel < startLevel {
  1223→				return stmt
  1224→			}
  1225→			p.nextToken()
  1226→		}
  1227→
  1228→		// Skip any more newlines after DEDENT
  1229→		for p.curTokenIs(lexer.NEWLINE) {
  1230→			p.nextToken()
  1231→		}
  1232→
  1233→		if p.curTokenIs(lexer.EOF) {
  1234→			break
  1235→		}
  1236→
  1237→		if p.indentLevel < startLevel {
  1238→			break
  1239→		}
  1240→
  1241→		// Parse a variant case: tag [; var]* \n INDENT body
  1242→		vc := ast.VariantCase{}
  1243→
  1244→		if !p.curTokenIs(lexer.IDENT) {
  1245→			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
  1246→			return stmt
  1247→		}
  1248→		vc.Tag = p.curToken.Literal
  1249→
  1250→		// Parse optional variables after semicolons: tag ; x ; y
  1251→		for p.peekTokenIs(lexer.SEMICOLON) {
  1252→			p.nextToken() // move to ;
  1253→			if !p.expectPeek(lexer.IDENT) {
  1254→				return stmt
  1255→			}
  1256→			vc.Variables = append(vc.Variables, p.curToken.Literal)
  1257→		}
  1258→
  1259→		// Skip newlines and expect INDENT for body
  1260→		for p.peekTokenIs(lexer.NEWLINE) {
  1261→			p.nextToken()
  1262→		}
  1263→
  1264→		if p.peekTokenIs(lexer.INDENT) {
  1265→			p.nextToken() // consume INDENT
  1266→			p.nextToken() // move to body
  1267→			vc.Body = p.parseStatement()
  1268→
  1269→			// Advance past the last token of the statement if needed
  1270→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1271→				p.nextToken()
  1272→			}
  1273→		}
  1274→
  1275→		stmt.Cases = append(stmt.Cases, vc)
  1276→	}
  1277→
  1278→	return stmt
  1279→}
  1280→
  1281→func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndex ast.Expression, token lexer.Token) *ast.VariantReceive {
  1282→	stmt := &ast.VariantReceive{
Thinking

Now I see the issue! At line 1244, the parser checks if !p.curTokenIs(lexer.IDENT). When it encounters COORDINATES (an IDENT token), it treats it as a variant tag name, not as a type declaration. So:

  1. COORDINATES is treated as a variant tag (since it's an IDENT)
  2. Parser checks for semicolons after tag - p is after COORDINATES, and peek is p (IDENT), not SEMICOLON
  3. Goes to "Skip newlines and expect INDENT for body" - peek is p (IDENT), not NEWLINE

  4. Expects INDENT for body but finds p (IDENT) instead, so no body gets parsed

  5. The variant case with tag="COORDINATES" gets added to cases, but the cursor never advances past COORDINATES because the body parsing fails
  6. Back at the loop start, curToken is still COORDINATES, creating an infinite loop

Let me actually trace through the token sequence more carefully. When the parser reads vc.Tag = p.curToken.Literal, it captures the literal value but doesn't advance. The semicolon check then peeks at the next token after COORDINATES, which would be p. Since that's not a semicolon, the loop exits and we try to parse the body, but p isn't INDENT either, so nothing happens and we're stuck. I see the issue now—after appending the variant, the cursor never advances past COORDINATES, so the loop immediately tries to parse it again as a new variant case, creating an infinite loop where no progress is made through the token stream.

The fix is to add a progress guard in the variant receive parsing loop, similar to what I implemented for parseIfStatement, or more directly, ensure the cursor advances after reading the tag and optional variables even if no body is found. I should also check if parseVariantReceiveWithIndex has the same problem.

Found the second infinite loop. In parseVariantReceive() (line 1214), the same problem: when COORDINATES (an unknown type treated as an IDENT) is encountered, it's read as a tag name (line 1248) but the cursor is never advanced past it. The main loop keeps re-parsing the same token.

Let me fix this by adding a progress guard:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1213,
  "limit": 67
}
  1213→	// Parse variant cases (similar to parseCaseStatement pattern)
  1214→	for !p.curTokenIs(lexer.EOF) {
  1215→		// Skip newlines
  1216→		for p.curTokenIs(lexer.NEWLINE) {
  1217→			p.nextToken()
  1218→		}
  1219→
  1220→		// Handle DEDENT tokens
  1221→		for p.curTokenIs(lexer.DEDENT) {
  1222→			if p.indentLevel < startLevel {
  1223→				return stmt
  1224→			}
  1225→			p.nextToken()
  1226→		}
  1227→
  1228→		// Skip any more newlines after DEDENT
  1229→		for p.curTokenIs(lexer.NEWLINE) {
  1230→			p.nextToken()
  1231→		}
  1232→
  1233→		if p.curTokenIs(lexer.EOF) {
  1234→			break
  1235→		}
  1236→
  1237→		if p.indentLevel < startLevel {
  1238→			break
  1239→		}
  1240→
  1241→		// Parse a variant case: tag [; var]* \n INDENT body
  1242→		vc := ast.VariantCase{}
  1243→
  1244→		if !p.curTokenIs(lexer.IDENT) {
  1245→			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
  1246→			return stmt
  1247→		}
  1248→		vc.Tag = p.curToken.Literal
  1249→
  1250→		// Parse optional variables after semicolons: tag ; x ; y
  1251→		for p.peekTokenIs(lexer.SEMICOLON) {
  1252→			p.nextToken() // move to ;
  1253→			if !p.expectPeek(lexer.IDENT) {
  1254→				return stmt
  1255→			}
  1256→			vc.Variables = append(vc.Variables, p.curToken.Literal)
  1257→		}
  1258→
  1259→		// Skip newlines and expect INDENT for body
  1260→		for p.peekTokenIs(lexer.NEWLINE) {
  1261→			p.nextToken()
  1262→		}
  1263→
  1264→		if p.peekTokenIs(lexer.INDENT) {
  1265→			p.nextToken() // consume INDENT
  1266→			p.nextToken() // move to body
  1267→			vc.Body = p.parseStatement()
  1268→
  1269→			// Advance past the last token of the statement if needed
  1270→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1271→				p.nextToken()
  1272→			}
  1273→		}
  1274→
  1275→		stmt.Cases = append(stmt.Cases, vc)
  1276→	}
  1277→
  1278→	return stmt
  1279→}
✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	// Parse variant cases (similar to parseCaseStatement pattern)
	for !p.curTokenIs(lexer.EOF) {
		// Skip newlines
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		// Handle DEDENT tokens
		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		// Skip any more newlines after DEDENT
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		// Parse a variant case: tag [; var]* \n INDENT body
		vc := ast.VariantCase{}

		if !p.curTokenIs(lexer.IDENT) {
			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
			return stmt
		}
		vc.Tag = p.curToken.Literal

		// Parse optional variables after semicolons: tag ; x ; y
		for p.peekTokenIs(lexer.SEMICOLON) {
			p.nextToken() // move to ;
			if !p.expectPeek(lexer.IDENT) {
				return stmt
			}
			vc.Variables = append(vc.Variables, p.curToken.Literal)
		}

		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			vc.Body = p.parseStatement()

			// Advance past the last token of the statement if needed
			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
				p.nextToken()
			}
		}

		stmt.Cases = append(stmt.Cases, vc)
	}

	return stmt
}
+
	// Parse variant cases (similar to parseCaseStatement pattern)
	for !p.curTokenIs(lexer.EOF) {
		// Skip newlines
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		// Handle DEDENT tokens
		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		// Skip any more newlines after DEDENT
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		// Safety guard: record position before parsing to detect no-progress
		prevToken := p.curToken
		prevPeek := p.peekToken

		// Parse a variant case: tag [; var]* \n INDENT body
		vc := ast.VariantCase{}

		if !p.curTokenIs(lexer.IDENT) {
			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
			p.nextToken() // skip unrecognized token to avoid infinite loop
			continue
		}
		vc.Tag = p.curToken.Literal

		// Parse optional variables after semicolons: tag ; x ; y
		for p.peekTokenIs(lexer.SEMICOLON) {
			p.nextToken() // move to ;
			if !p.expectPeek(lexer.IDENT) {
				return stmt
			}
			vc.Variables = append(vc.Variables, p.curToken.Literal)
		}

		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			vc.Body = p.parseStatement()

			// Advance past the last token of the statement if needed
			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
				p.nextToken()
			}
		}

		stmt.Cases = append(stmt.Cases, vc)

		// No-progress guard: if we haven't moved, break to prevent infinite loop
		if p.curToken == prevToken && p.peekToken == prevPeek {
			p.nextToken() // force progress
			if p.curToken == prevToken {
				break
			}
		}
	}

	return stmt
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now let me also check the second variant receive function parseVariantReceiveWithIndex:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1289,
  "limit": 80
}
  1289→	}
  1290→
  1291→	return stmt
  1292→}
  1293→
  1294→func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndex ast.Expression, token lexer.Token) *ast.VariantReceive {
  1295→	stmt := &ast.VariantReceive{
  1296→		Token:        token,
  1297→		Channel:      channel,
  1298→		ChannelIndex: channelIndex,
  1299→	}
  1300→
  1301→	// Skip to next line
  1302→	for p.peekTokenIs(lexer.NEWLINE) {
  1303→		p.nextToken()
  1304→	}
  1305→
  1306→	// Expect INDENT
  1307→	if !p.peekTokenIs(lexer.INDENT) {
  1308→		p.addError("expected indented block after ? CASE")
  1309→		return stmt
  1310→	}
  1311→	p.nextToken() // consume INDENT
  1312→	startLevel := p.indentLevel
  1313→	p.nextToken() // move into block
  1314→
  1315→	for !p.curTokenIs(lexer.EOF) {
  1316→		for p.curTokenIs(lexer.NEWLINE) {
  1317→			p.nextToken()
  1318→		}
  1319→
  1320→		for p.curTokenIs(lexer.DEDENT) {
  1321→			if p.indentLevel < startLevel {
  1322→				return stmt
  1323→			}
  1324→			p.nextToken()
  1325→		}
  1326→
  1327→		for p.curTokenIs(lexer.NEWLINE) {
  1328→			p.nextToken()
  1329→		}
  1330→
  1331→		if p.curTokenIs(lexer.EOF) {
  1332→			break
  1333→		}
  1334→
  1335→		if p.indentLevel < startLevel {
  1336→			break
  1337→		}
  1338→
  1339→		vc := ast.VariantCase{}
  1340→
  1341→		if !p.curTokenIs(lexer.IDENT) {
  1342→			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
  1343→			return stmt
  1344→		}
  1345→		vc.Tag = p.curToken.Literal
  1346→
  1347→		for p.peekTokenIs(lexer.SEMICOLON) {
  1348→			p.nextToken() // move to ;
  1349→			if !p.expectPeek(lexer.IDENT) {
  1350→				return stmt
  1351→			}
  1352→			vc.Variables = append(vc.Variables, p.curToken.Literal)
  1353→		}
  1354→
  1355→		for p.peekTokenIs(lexer.NEWLINE) {
  1356→			p.nextToken()
  1357→		}
  1358→
  1359→		if p.peekTokenIs(lexer.INDENT) {
  1360→			p.nextToken() // consume INDENT
  1361→			p.nextToken() // move to body
  1362→			vc.Body = p.parseStatement()
  1363→
  1364→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1365→				p.nextToken()
  1366→			}
  1367→		}
  1368→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1368,
  "limit": 10
}
  1368→
  1369→		stmt.Cases = append(stmt.Cases, vc)
  1370→	}
  1371→
  1372→	return stmt
  1373→}
  1374→
  1375→func (p *Parser) parseSeqBlock() *ast.SeqBlock {
  1376→	block := &ast.SeqBlock{Token: p.curToken}
  1377→

Same pattern - needs the same fix:

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	for !p.curTokenIs(lexer.EOF) {
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		vc := ast.VariantCase{}

		if !p.curTokenIs(lexer.IDENT) {
			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
			return stmt
		}
		vc.Tag = p.curToken.Literal

		for p.peekTokenIs(lexer.SEMICOLON) {
			p.nextToken() // move to ;
			if !p.expectPeek(lexer.IDENT) {
				return stmt
			}
			vc.Variables = append(vc.Variables, p.curToken.Literal)
		}

		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			vc.Body = p.parseStatement()

			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
				p.nextToken()
			}
		}

		stmt.Cases = append(stmt.Cases, vc)
	}

	return stmt
}
+
	for !p.curTokenIs(lexer.EOF) {
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		// Safety guard: record position before parsing to detect no-progress
		prevToken := p.curToken
		prevPeek := p.peekToken

		vc := ast.VariantCase{}

		if !p.curTokenIs(lexer.IDENT) {
			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
			p.nextToken() // skip unrecognized token to avoid infinite loop
			continue
		}
		vc.Tag = p.curToken.Literal

		for p.peekTokenIs(lexer.SEMICOLON) {
			p.nextToken() // move to ;
			if !p.expectPeek(lexer.IDENT) {
				return stmt
			}
			vc.Variables = append(vc.Variables, p.curToken.Literal)
		}

		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			vc.Body = p.parseStatement()

			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
				p.nextToken()
			}
		}

		stmt.Cases = append(stmt.Cases, vc)

		// No-progress guard: if we haven't moved, break to prevent infinite loop
		if p.curToken == prevToken && p.peekToken == prevPeek {
			p.nextToken() // force progress
			if p.curToken == prevToken {
				break
			}
		}
	}

	return stmt
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now let me also add progress guards to the parseCaseStatement loop which has the same pattern:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 2310,
  "limit": 70
}
  2310→
  2311→			// Skip newlines and expect INDENT for body
  2312→			for p.peekTokenIs(lexer.NEWLINE) {
  2313→				p.nextToken()
  2314→			}
  2315→
  2316→			if p.peekTokenIs(lexer.INDENT) {
  2317→				p.nextToken() // consume INDENT
  2318→				p.nextToken() // move to body
  2319→				choice.Body = p.parseBodyStatements()
  2320→			}
  2321→		}
  2322→
  2323→		stmt.Choices = append(stmt.Choices, choice)
  2324→
  2325→		// No-progress guard: if we haven't moved, break to prevent infinite loop
  2326→		if p.curToken == prevToken && p.peekToken == prevPeek {
  2327→			break
  2328→		}
  2329→	}
  2330→
  2331→	return stmt
  2332→}
  2333→
  2334→func (p *Parser) parseCaseStatement() *ast.CaseStatement {
  2335→	stmt := &ast.CaseStatement{Token: p.curToken}
  2336→
  2337→	// Parse selector expression on the same line
  2338→	p.nextToken()
  2339→	stmt.Selector = p.parseExpression(LOWEST)
  2340→
  2341→	// Skip to next line
  2342→	for p.peekTokenIs(lexer.NEWLINE) {
  2343→		p.nextToken()
  2344→	}
  2345→
  2346→	// Expect INDENT
  2347→	if !p.peekTokenIs(lexer.INDENT) {
  2348→		p.addError("expected indented block after CASE")
  2349→		return stmt
  2350→	}
  2351→	p.nextToken() // consume INDENT
  2352→	startLevel := p.indentLevel
  2353→	p.nextToken() // move into block
  2354→
  2355→	// Parse case choices
  2356→	for !p.curTokenIs(lexer.EOF) {
  2357→		// Skip newlines
  2358→		for p.curTokenIs(lexer.NEWLINE) {
  2359→			p.nextToken()
  2360→		}
  2361→
  2362→		// Handle DEDENT tokens
  2363→		for p.curTokenIs(lexer.DEDENT) {
  2364→			if p.indentLevel < startLevel {
  2365→				return stmt
  2366→			}
  2367→			p.nextToken()
  2368→		}
  2369→
  2370→		// Skip any more newlines after DEDENT
  2371→		for p.curTokenIs(lexer.NEWLINE) {
  2372→			p.nextToken()
  2373→		}
  2374→
  2375→		if p.curTokenIs(lexer.EOF) {
  2376→			break
  2377→		}
  2378→
  2379→		if p.indentLevel < startLevel {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 2379,
  "limit": 60
}
  2379→		if p.indentLevel < startLevel {
  2380→			break
  2381→		}
  2382→
  2383→		choice := ast.CaseChoice{}
  2384→
  2385→		if p.curTokenIs(lexer.ELSE) {
  2386→			choice.IsElse = true
  2387→		} else {
  2388→			// Parse value expression
  2389→			choice.Values = append(choice.Values, p.parseExpression(LOWEST))
  2390→		}
  2391→
  2392→		// Skip newlines and expect INDENT for body
  2393→		for p.peekTokenIs(lexer.NEWLINE) {
  2394→			p.nextToken()
  2395→		}
  2396→
  2397→		if p.peekTokenIs(lexer.INDENT) {
  2398→			p.nextToken() // consume INDENT
  2399→			p.nextToken() // move to body
  2400→			choice.Body = p.parseBodyStatements()
  2401→		}
  2402→
  2403→		stmt.Choices = append(stmt.Choices, choice)
  2404→	}
  2405→
  2406→	return stmt
  2407→}
  2408→
  2409→// Expression parsing using Pratt parsing
  2410→
  2411→func (p *Parser) parseExpression(precedence int) ast.Expression {
  2412→	var left ast.Expression
  2413→
  2414→	switch p.curToken.Type {
  2415→	case lexer.IDENT:
  2416→		if p.peekTokenIs(lexer.LPAREN) {
  2417→			left = p.parseFuncCallExpr()
  2418→		} else {
  2419→			left = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
  2420→		}
  2421→	case lexer.INT:
  2422→		base := 10
  2423→		literal := p.curToken.Literal
  2424→		if strings.HasPrefix(literal, "0x") || strings.HasPrefix(literal, "0X") {
  2425→			base = 16
  2426→			literal = literal[2:]
  2427→		}
  2428→		val, err := strconv.ParseInt(literal, base, 64)
  2429→		if err != nil {
  2430→			p.addError(fmt.Sprintf("could not parse %q as integer", p.curToken.Literal))
  2431→			return nil
  2432→		}
  2433→		left = &ast.IntegerLiteral{Token: p.curToken, Value: val}
  2434→	case lexer.TRUE:
  2435→		left = &ast.BooleanLiteral{Token: p.curToken, Value: true}
  2436→	case lexer.FALSE:
  2437→		left = &ast.BooleanLiteral{Token: p.curToken, Value: false}
  2438→	case lexer.STRING:

The parseCaseStatement loop (line 2356) also lacks a progress guard. Let me add one:

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	// Parse case choices
	for !p.curTokenIs(lexer.EOF) {
		// Skip newlines
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		// Handle DEDENT tokens
		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		// Skip any more newlines after DEDENT
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		choice := ast.CaseChoice{}

		if p.curTokenIs(lexer.ELSE) {
			choice.IsElse = true
		} else {
			// Parse value expression
			choice.Values = append(choice.Values, p.parseExpression(LOWEST))
		}

		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			choice.Body = p.parseBodyStatements()
		}

		stmt.Choices = append(stmt.Choices, choice)
	}

	return stmt
}
+
	// Parse case choices
	for !p.curTokenIs(lexer.EOF) {
		// Skip newlines
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		// Handle DEDENT tokens
		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return stmt
			}
			p.nextToken()
		}

		// Skip any more newlines after DEDENT
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		// Safety guard: record position before parsing to detect no-progress
		prevToken := p.curToken
		prevPeek := p.peekToken

		choice := ast.CaseChoice{}

		if p.curTokenIs(lexer.ELSE) {
			choice.IsElse = true
		} else {
			// Parse value expression
			choice.Values = append(choice.Values, p.parseExpression(LOWEST))
		}

		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			choice.Body = p.parseBodyStatements()
		}

		stmt.Choices = append(stmt.Choices, choice)

		// No-progress guard: if we haven't moved, break to prevent infinite loop
		if p.curToken == prevToken && p.peekToken == prevPeek {
			break
		}
	}

	return stmt
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Let me also check the ALT cases parsing loop:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1487,
  "limit": 60
}
  1487→		rep.Step = p.parseExpression(LOWEST)
  1488→	}
  1489→
  1490→	return rep
  1491→}
  1492→
  1493→func (p *Parser) parseAltBlock() *ast.AltBlock {
  1494→	block := &ast.AltBlock{Token: p.curToken}
  1495→
  1496→	// Skip to next line
  1497→	for p.peekTokenIs(lexer.NEWLINE) {
  1498→		p.nextToken()
  1499→	}
  1500→
  1501→	// Expect INDENT
  1502→	if !p.peekTokenIs(lexer.INDENT) {
  1503→		p.addError("expected indented block after ALT")
  1504→		return block
  1505→	}
  1506→	p.nextToken() // consume INDENT
  1507→
  1508→	block.Cases = p.parseAltCases()
  1509→
  1510→	return block
  1511→}
  1512→
  1513→func (p *Parser) parseAltCases() []ast.AltCase {
  1514→	var cases []ast.AltCase
  1515→	startLevel := p.indentLevel
  1516→
  1517→	p.nextToken() // move past INDENT
  1518→
  1519→	for !p.curTokenIs(lexer.EOF) {
  1520→		// Skip newlines
  1521→		for p.curTokenIs(lexer.NEWLINE) {
  1522→			p.nextToken()
  1523→		}
  1524→
  1525→		// Handle DEDENT tokens
  1526→		for p.curTokenIs(lexer.DEDENT) {
  1527→			if p.indentLevel < startLevel {
  1528→				return cases
  1529→			}
  1530→			p.nextToken()
  1531→		}
  1532→
  1533→		// Skip any more newlines after DEDENT
  1534→		for p.curTokenIs(lexer.NEWLINE) {
  1535→			p.nextToken()
  1536→		}
  1537→
  1538→		if p.curTokenIs(lexer.EOF) {
  1539→			break
  1540→		}
  1541→
  1542→		if p.indentLevel < startLevel {
  1543→			break
  1544→		}
  1545→
  1546→		// Parse an ALT case: [guard &] channel ? var
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1546,
  "limit": 60
}
  1546→		// Parse an ALT case: [guard &] channel ? var
  1547→		altCase := p.parseAltCase()
  1548→		if altCase != nil {
  1549→			cases = append(cases, *altCase)
  1550→		}
  1551→	}
  1552→
  1553→	return cases
  1554→}
  1555→
  1556→func (p *Parser) parseAltCase() *ast.AltCase {
  1557→	altCase := &ast.AltCase{}
  1558→
  1559→	// Check for guard: expression & channel ? var
  1560→	// For now, we expect: channel ? var (no guard support yet)
  1561→	// or: guard & channel ? var
  1562→
  1563→	// First token should be identifier (channel name or guard start)
  1564→	if !p.curTokenIs(lexer.IDENT) && !p.curTokenIs(lexer.TRUE) && !p.curTokenIs(lexer.FALSE) {
  1565→		p.addError(fmt.Sprintf("expected channel name or guard in ALT case, got %s", p.curToken.Type))
  1566→		return nil
  1567→	}
  1568→
  1569→	// Look ahead to determine if this is a guard or channel
  1570→	// If next token is & then we have a guard
  1571→	// If next token is ? then it's a channel/timer receive
  1572→	if p.peekTokenIs(lexer.RECEIVE) {
  1573→		name := p.curToken.Literal
  1574→		if p.timerNames[name] {
  1575→			// Timer case: tim ? AFTER deadline
  1576→			altCase.IsTimer = true
  1577→			altCase.Timer = name
  1578→			p.nextToken() // move to ?
  1579→			if !p.expectPeek(lexer.AFTER) {
  1580→				return nil
  1581→			}
  1582→			p.nextToken() // move past AFTER
  1583→			altCase.Deadline = p.parseExpression(LOWEST)
  1584→		} else {
  1585→			// Simple case: channel ? var
  1586→			altCase.Channel = name
  1587→			p.nextToken() // move to ?
  1588→			if !p.expectPeek(lexer.IDENT) {
  1589→				return nil
  1590→			}
  1591→			altCase.Variable = p.curToken.Literal
  1592→		}
  1593→	} else if p.peekTokenIs(lexer.LBRACKET) {
  1594→		// Indexed channel case: cs[i] ? var
  1595→		name := p.curToken.Literal
  1596→		altCase.Channel = name
  1597→		p.nextToken() // move to [
  1598→		p.nextToken() // move past [
  1599→		altCase.ChannelIndex = p.parseExpression(LOWEST)
  1600→		if !p.expectPeek(lexer.RBRACKET) {
  1601→			return nil
  1602→		}
  1603→		if !p.expectPeek(lexer.RECEIVE) {
  1604→			return nil
  1605→		}

The parseAltCases loop also doesn't have a progress guard, but it calls parseAltCase which returns nil and the loop would break. But let me add one for safety:

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
		// Parse an ALT case: [guard &] channel ? var
		altCase := p.parseAltCase()
		if altCase != nil {
			cases = append(cases, *altCase)
		}
	}

	return cases
}
+
		// Safety guard: record position before parsing to detect no-progress
		prevToken := p.curToken
		prevPeek := p.peekToken

		// Parse an ALT case: [guard &] channel ? var
		altCase := p.parseAltCase()
		if altCase != nil {
			cases = append(cases, *altCase)
		}

		// No-progress guard: if we haven't moved, break to prevent infinite loop
		if p.curToken == prevToken && p.peekToken == prevPeek {
			break
		}
	}

	return cases
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now let me build and test everything:

$ Bash
Build with all changes
go build -o occam2go . 2>&1
$ Bash
Run all existing tests
go test ./... 2>&1
?   	github.com/codeassociates/occam2go	[no test files]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	10.147s
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	0.002s
ok  	github.com/codeassociates/occam2go/preproc	(cached)

All tests pass. Now let me test the course module files:

$ Bash
Test all course module files with fixes
for f in kroc/modules/course/libsrc/*.occ; do echo "=== Testing: $(basename $f) ==="; timeout 5 ./occam2go -I kroc/modules/course/libsrc "$f" -o /dev/null 2>&1 | head -3; rc=${PIPESTATUS[0]}; if [ $rc -eq 124 ]; then echo "TIMEOUT (likely infinite loop)"; elif [ $rc -ne 0 ]; then echo "Exit code: $rc"; else echo "OK"; fi; echo; done
=== Testing: demo_cycles.occ ===
Parse errors:
  line 33: expected type in parameter, got IDENT
  line 33: expected ), got !
Exit code: 1

=== Testing: demo_nets.occ ===
Parse errors:
  line 46: expected type in parameter, got IDENT
  line 46: expected ), got !
Exit code: 1

=== Testing: file_in.occ ===
Parse errors:
  line 161: unexpected token in expression: NEWLINE
  line 161: expected ), got INDENT
Exit code: 1

=== Testing: float_io.occ ===
Parse errors:
  line 47: expected type in parameter, got RESULT
  line 47: expected ), got INT
Exit code: 141

=== Testing: random.occ ===
Parse errors:
  line 46: expected type in parameter, got IDENT
Exit code: 1

=== Testing: shared_screen.occ ===
Parse errors:
  line 110: unexpected token: IS
  line 110: expected IDENT, got :
Exit code: 141

=== Testing: string.occ ===
Parse errors:
  line 133: expected type in parameter, got IDENT
  line 182: expected type in parameter, got IDENT
Exit code: 1

=== Testing: stringbuf.occ ===
Parse errors:
  line 36: expected FROM, got FOR
  line 36: unexpected token: FOR
Exit code: 1

=== Testing: utils.occ ===
Parse errors:
  line 249: expected type after VAL, got [
  line 249: unexpected token: ]
Exit code: 141

No more timeouts! All files now terminate with parse errors (expected since many occam features used by these files aren't supported yet) but none hang. Let me also verify the nested IF codegen works end-to-end by writing proper tests:

$ Bash
End-to-end test: nested replicated IF with default
# Test nested IF end-to-end: replicated IF followed by TRUE default
cat > /tmp/test_nested_if_e2e.occ << 'EOF'
PROC main ()
  [5]INT arr:
  INT result:
  SEQ
    SEQ i = 0 FOR 5
      arr[i] := i * 10
    IF
      IF i = 0 FOR 5
        arr[i] > 25
          result := arr[i]
      TRUE
        result := -1
    write.int (result, 0)
    write.newline ()
:
EOF
timeout 5 ./occam2go /tmp/test_nested_if_e2e.occ -o /tmp/test_nested_if_e2e.go 2>&1 && go run /tmp/test_nested_if_e2e.go 2>&1; echo "Exit: $?"
package main

func main() {
	arr := make([]int, 5)
	var result int
	for i := 0; i < 0 + 5; i++ {
		arr[i] = (i * 10)
	}
	_ifmatched := false
	for i := 0; i < 0 + 5; i++ {
		if (arr[i] > 25) {
			result = arr[i]
			_ifmatched = true
			break
		}
	}
	if !_ifmatched {
		if true {
			result = - 1
		}
	}
	write.int(result, 0)
	write.newline()
}

stat /tmp/test_nested_if_e2e.go: no such file or directory
Exit: 1

The -o /dev/null output mode vs stdout is mixed up. Let me fix the test:

$ Bash
E2E test with proper output file
timeout 5 ./occam2go /tmp/test_nested_if_e2e.occ -o /tmp/test_nested_if_e2e.go 2>&1
echo "---"
cat /tmp/test_nested_if_e2e.go
echo "---"
go run /tmp/test_nested_if_e2e.go 2>&1
echo "Exit: $?"
package main

func main() {
	arr := make([]int, 5)
	var result int
	for i := 0; i < 0 + 5; i++ {
		arr[i] = (i * 10)
	}
	_ifmatched := false
	for i := 0; i < 0 + 5; i++ {
		if (arr[i] > 25) {
			result = arr[i]
			_ifmatched = true
			break
		}
	}
	if !_ifmatched {
		if true {
			result = - 1
		}
	}
	write.int(result, 0)
	write.newline()
}

---
cat: /tmp/test_nested_if_e2e.go: No such file or directory
---
stat /tmp/test_nested_if_e2e.go: no such file or directory
Exit: 1
Thinking

The output is going to stdout instead of a file because of errors from write.int/write.newline which are built-in procs that need the codegen to output them. Let me use a proper e2e test instead of manual testing.

Looking at the output, the Go code uses write.int and write.newline which are Go function calls with dots in the name - that won't compile. I need to use fmt.Println or similar built-in procs that the transpiler supports.

Actually, looking at CLAUDE.md: "built-in print procedures" - the transpiler supports write.int, write.newline etc.

The -o flag isn't creating the file as expected, just printing to stdout. Let me redirect the output to a file instead.

$ Bash
Check if generated Go code compiles
timeout 5 ./occam2go /tmp/test_nested_if_e2e.occ > /tmp/test_nested_if_e2e.go 2>&1
go build -o /dev/null /tmp/test_nested_if_e2e.go 2>&1
echo "Exit: $?"
# command-line-arguments
/tmp/test_nested_if_e2e.go:22:2: undefined: write
/tmp/test_nested_if_e2e.go:23:2: undefined: write
Exit: 1

The write.int/write.newline aren't being resolved - let me use fmt.Println directly or just test through the existing e2e test framework. Let me write a proper e2e test:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go",
  "offset": 1,
  "limit": 50
}
     1→package codegen
     2→
     3→import "testing"
     4→
     5→func TestE2E_IfBasic(t *testing.T) {
     6→	// Test basic IF: first branch is true
     7→	occam := `SEQ
     8→  INT x, y:
     9→  x := 5
    10→  y := 0
    11→  IF
    12→    x > 0
    13→      y := 1
    14→    x = 0
    15→      y := 2
    16→  print.int(y)
    17→`
    18→	output := transpileCompileRun(t, occam)
    19→	expected := "1\n"
    20→	if output != expected {
    21→		t.Errorf("expected %q, got %q", expected, output)
    22→	}
    23→}
    24→
    25→func TestE2E_IfSecondBranch(t *testing.T) {
    26→	// Test IF where second branch matches
    27→	occam := `SEQ
    28→  INT x, y:
    29→  x := 0
    30→  y := 0
    31→  IF
    32→    x > 0
    33→      y := 1
    34→    x = 0
    35→      y := 2
    36→  print.int(y)
    37→`
    38→	output := transpileCompileRun(t, occam)
    39→	expected := "2\n"
    40→	if output != expected {
    41→		t.Errorf("expected %q, got %q", expected, output)
    42→	}
    43→}
    44→
    45→func TestE2E_IfThreeBranches(t *testing.T) {
    46→	// Test IF with three choices where the last matches
    47→	occam := `SEQ
    48→  INT x, y:
    49→  x := 0
    50→  y := 0
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go",
  "offset": 200,
  "limit": 100
}
   200→	if output != expected {
   201→		t.Errorf("expected %q, got %q", expected, output)
   202→	}
   203→}
   204→
   205→func TestE2E_MultiStatementIfBody(t *testing.T) {
   206→	occam := `SEQ
   207→  INT x:
   208→  x := 5
   209→  IF
   210→    x > 0
   211→      INT y:
   212→      y := x + 10
   213→      print.int(y)
   214→    TRUE
   215→      SKIP
   216→`
   217→	output := transpileCompileRun(t, occam)
   218→	expected := "15\n"
   219→	if output != expected {
   220→		t.Errorf("expected %q, got %q", expected, output)
   221→	}
   222→}
   223→
   224→func TestE2E_MultiStatementCaseBody(t *testing.T) {
   225→	occam := `SEQ
   226→  INT x:
   227→  x := 2
   228→  CASE x
   229→    1
   230→      print.int(10)
   231→    2
   232→      INT y:
   233→      y := x * 100
   234→      print.int(y)
   235→    ELSE
   236→      print.int(0)
   237→`
   238→	output := transpileCompileRun(t, occam)
   239→	expected := "200\n"
   240→	if output != expected {
   241→		t.Errorf("expected %q, got %q", expected, output)
   242→	}
   243→}
   244→
   245→func TestE2E_MultiStatementWhileBody(t *testing.T) {
   246→	occam := `SEQ
   247→  INT x:
   248→  x := 0
   249→  WHILE x < 3
   250→    INT step:
   251→    step := 1
   252→    x := x + step
   253→  print.int(x)
   254→`
   255→	output := transpileCompileRun(t, occam)
   256→	expected := "3\n"
   257→	if output != expected {
   258→		t.Errorf("expected %q, got %q", expected, output)
   259→	}
   260→}
   261→
   262→func TestE2E_ChannelDirAtCallSite(t *testing.T) {
   263→	occam := `PROC worker(CHAN OF INT in?, CHAN OF INT out!)
   264→  INT x:
   265→  in ? x
   266→  out ! x + 1
   267→:
   268→SEQ
   269→  CHAN OF INT a:
   270→  CHAN OF INT b:
   271→  PAR
   272→    worker(a?, b!)
   273→    SEQ
   274→      a ! 10
   275→      INT result:
   276→      b ? result
   277→      print.int(result)
   278→`
   279→	output := transpileCompileRun(t, occam)
   280→	expected := "11\n"
   281→	if output != expected {
   282→		t.Errorf("expected %q, got %q", expected, output)
   283→	}
   284→}
   285→

Now let me add e2e tests for nested replicated IF:

✏️ Edit e2e_control_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go
func TestE2E_ChannelDirAtCallSite(t *testing.T) {
+
func TestE2E_NestedReplicatedIfWithDefault(t *testing.T) {
	// Replicated IF as a choice within outer IF, with TRUE default
	occam := `SEQ
  [5]INT arr:
  INT result:
  SEQ i = 0 FOR 5
    arr[i] := i * 10
  IF
    IF i = 0 FOR 5
      arr[i] > 25
        result := arr[i]
    TRUE
      result := -1
  print.int(result)
`
	output := transpileCompileRun(t, occam)
	expected := "30\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_NestedReplicatedIfNoMatch(t *testing.T) {
	// Replicated IF where no choice matches, falls through to TRUE
	occam := `SEQ
  [3]INT arr:
  INT result:
  SEQ i = 0 FOR 3
    arr[i] := i
  IF
    IF i = 0 FOR 3
      arr[i] > 100
        result := arr[i]
    TRUE
      result := -1
  print.int(result)
`
	output := transpileCompileRun(t, occam)
	expected := "-1\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_NestedReplicatedIfWithPrecedingChoice(t *testing.T) {
	// Normal choice before replicated IF, then default
	occam := `SEQ
  [3]INT arr:
  INT result:
  SEQ i = 0 FOR 3
    arr[i] := i
  INT x:
  x := 99
  IF
    x > 100
      result := x
    IF i = 0 FOR 3
      arr[i] = 2
        result := arr[i]
    TRUE
      result := -1
  print.int(result)
`
	output := transpileCompileRun(t, occam)
	expected := "2\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_NestedNonReplicatedIf(t *testing.T) {
	// Non-replicated nested IF (choices inlined into parent)
	occam := `SEQ
  INT x:
  INT result:
  x := 5
  IF
    IF
      x > 10
        result := 1
      x > 3
        result := 2
    TRUE
      result := 0
  print.int(result)
`
	output := transpileCompileRun(t, occam)
	expected := "2\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_ChannelDirAtCallSite(t *testing.T) {
The file /home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go has been updated successfully.
$ Bash
Run new nested IF e2e tests
go test ./codegen -run "TestE2E_Nested" -v 2>&1
=== RUN   TestE2E_NestedReplicatedIfWithDefault
--- PASS: TestE2E_NestedReplicatedIfWithDefault (0.08s)
=== RUN   TestE2E_NestedReplicatedIfNoMatch
--- PASS: TestE2E_NestedReplicatedIfNoMatch (0.08s)
=== RUN   TestE2E_NestedReplicatedIfWithPrecedingChoice
--- PASS: TestE2E_NestedReplicatedIfWithPrecedingChoice (0.08s)
=== RUN   TestE2E_NestedNonReplicatedIf
--- PASS: TestE2E_NestedNonReplicatedIf (0.08s)
=== RUN   TestE2E_NestedProcClosure
--- PASS: TestE2E_NestedProcClosure (0.08s)
=== RUN   TestE2E_NestedProcWithParams
--- PASS: TestE2E_NestedProcWithParams (0.08s)
=== RUN   TestE2E_NestedFunctionIS
--- PASS: TestE2E_NestedFunctionIS (0.08s)
=== RUN   TestE2E_NestedFunctionValof
--- PASS: TestE2E_NestedFunctionValof (0.08s)
PASS
ok  	github.com/codeassociates/occam2go/codegen	0.653s

All 4 new tests pass. Let me run the full test suite to make sure nothing is broken:

$ Bash
Run full test suite
go test ./... 2>&1
?   	github.com/codeassociates/occam2go	[no test files]
?   	github.com/codeassociates/occam2go/ast	[no test files]
ok  	github.com/codeassociates/occam2go/codegen	10.447s
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	(cached)
ok  	github.com/codeassociates/occam2go/preproc	(cached)

All tests pass. Let me clean up the temp files and write a summary:

$ Bash
Clean up temp test files
rm -f /tmp/test_minimal*.occ /tmp/test_repif*.occ /tmp/test_boolfunc.occ /tmp/test_fold.occ /tmp/test_screen*.occ /tmp/test_s*.occ /tmp/test_exact*.occ /tmp/test_varrecv*.occ /tmp/test_nested_if_e2e.occ /tmp/test_nested_if_e2e.go

Here's a summary of what was found and fixed:

Root Causes

Two distinct infinite loop bugs were identified:

1. Nested/Replicated IF within IF (parser.go:parseIfStatement)

When an IF keyword appeared as a choice inside another IF block (e.g., IF i = 0 FOR n used as a replicated choice), parseExpression(LOWEST) was called on the IF token. Since IF isn't a valid expression prefix, parseExpression returned nil without advancing the cursor. The main loop had no progress guard, so it re-parsed the same IF token forever.

Fix: Detect curToken == IF in the choices loop and recursively call parseIfStatement() to properly parse it as a nested IF. Store the result in a new NestedIf *IfStatement field on IfChoice. Also added a progress guard as a safety net.

2. Unknown types in variant receive (parser.go:parseVariantReceive)

When a variant ? CASE block contained declarations with unknown types (e.g., COORDINATES p: where COORDINATES is parsed as an IDENT), the parser treated it as a variant tag name but never advanced past it. The next iteration of the loop saw the same IDENT token and looped forever.

Fix: Added progress guards to both parseVariantReceive and parseVariantReceiveWithIndex. When no progress is detected, the parser forces advancement or breaks. Also changed the "unexpected tag" error path to skip the token with continue instead of returning immediately.

Changes Made

File Change
ast/ast.go Added NestedIf *IfStatement to IfChoice
parser/parser.go Detect nested IF in parseIfStatement, add progress guards to parseIfStatement, parseCaseStatement, parseVariantReceive, parseVariantReceiveWithIndex, parseAltCases
codegen/codegen.go Refactored generateIfStatement into flattenIfChoices (inlines non-replicated nested IFs), generateReplicatedIfLoop, and generateIfChoiceChain (handles mixed choices with _ifmatched flag). Updated 6 scanner functions to traverse NestedIf.
codegen/e2e_control_test.go Added 4 new e2e tests: replicated IF with default, no-match fallthrough, preceding normal choice, non-replicated nested IF

Thank you. Please make a PR for these changes.

$ Bash
Show working tree status
git status
On branch main
Your branch is up to date with 'origin/main'.

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   ast/ast.go
	modified:   codegen/codegen.go
	modified:   codegen/e2e_control_test.go
	modified:   parser/parser.go

no changes added to commit (use "git add" and/or "git commit -a")
$ Bash
Show staged and unstaged changes
git diff
diff --git a/ast/ast.go b/ast/ast.go
index 370a898..3363ffd 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -205,6 +205,7 @@ type IfStatement struct {
 type IfChoice struct {
 	Condition Expression
 	Body      []Statement
+	NestedIf  *IfStatement // non-nil when this choice is a nested/replicated IF
 }
 
 func (i *IfStatement) statementNode()       {}
diff --git a/codegen/codegen.go b/codegen/codegen.go
index b56ba9b..0e8c8a3 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -221,6 +221,11 @@ func (g *Generator) containsPar(stmt ast.Statement) bool {
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
+			if choice.NestedIf != nil {
+				if g.containsPar(choice.NestedIf) {
+					return true
+				}
+			}
 			for _, inner := range choice.Body {
 				if g.containsPar(inner) {
 					return true
@@ -289,6 +294,11 @@ func (g *Generator) containsPrint(stmt ast.Statement) bool {
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
+			if choice.NestedIf != nil {
+				if g.containsPrint(choice.NestedIf) {
+					return true
+				}
+			}
 			for _, inner := range choice.Body {
 				if g.containsPrint(inner) {
 					return true
@@ -360,6 +370,11 @@ func (g *Generator) containsTimer(stmt ast.Statement) bool {
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
+			if choice.NestedIf != nil {
+				if g.containsTimer(choice.NestedIf) {
+					return true
+				}
+			}
 			for _, inner := range choice.Body {
 				if g.containsTimer(inner) {
 					return true
@@ -428,6 +443,11 @@ func (g *Generator) containsStop(stmt ast.Statement) bool {
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
+			if choice.NestedIf != nil {
+				if g.containsStop(choice.NestedIf) {
+					return true
+				}
+			}
 			for _, inner := range choice.Body {
 				if g.containsStop(inner) {
 					return true
@@ -504,6 +524,11 @@ func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
+			if choice.NestedIf != nil {
+				if g.containsMostExpr(choice.NestedIf) {
+					return true
+				}
+			}
 			if g.exprNeedsMath(choice.Condition) {
 				return true
 			}
@@ -943,6 +968,9 @@ func (g *Generator) collectChanProtocols(stmt ast.Statement) {
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
+			if choice.NestedIf != nil {
+				g.collectChanProtocols(choice.NestedIf)
+			}
 			for _, inner := range choice.Body {
 				g.collectChanProtocols(inner)
 			}
@@ -999,6 +1027,9 @@ func (g *Generator) collectRecordVars(stmt ast.Statement) {
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
+			if choice.NestedIf != nil {
+				g.collectRecordVars(choice.NestedIf)
+			}
 			for _, inner := range choice.Body {
 				g.collectRecordVars(inner)
 			}
@@ -1551,35 +1582,104 @@ func (g *Generator) generateWhileLoop(loop *ast.WhileLoop) {
 func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
 	if stmt.Replicator != nil {
 		// Replicated IF: IF i = start FOR count → for loop with break on first match
-		v := stmt.Replicator.Variable
-		if stmt.Replicator.Step != nil {
-			counter := "_repl_" + v
-			g.builder.WriteString(strings.Repeat("\t", g.indent))
-			g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
-			g.generateExpression(stmt.Replicator.Count)
-			g.write(fmt.Sprintf("; %s++ {\n", counter))
-			g.indent++
-			g.builder.WriteString(strings.Repeat("\t", g.indent))
-			g.write(fmt.Sprintf("%s := ", v))
-			g.generateExpression(stmt.Replicator.Start)
-			g.write(fmt.Sprintf(" + %s * ", counter))
-			g.generateExpression(stmt.Replicator.Step)
-			g.write("\n")
+		g.generateReplicatedIfLoop(stmt, false)
+	} else {
+		// Flatten non-replicated nested IFs into the parent choice list
+		choices := g.flattenIfChoices(stmt.Choices)
+		g.generateIfChoiceChain(choices, true)
+	}
+}
+
+// flattenIfChoices inlines choices from non-replicated nested IFs into a flat list.
+// Replicated nested IFs are preserved as-is (they need special loop codegen).
+func (g *Generator) flattenIfChoices(choices []ast.IfChoice) []ast.IfChoice {
+	var flat []ast.IfChoice
+	for _, c := range choices {
+		if c.NestedIf != nil && c.NestedIf.Replicator == nil {
+			// Non-replicated nested IF: inline its choices recursively
+			flat = append(flat, g.flattenIfChoices(c.NestedIf.Choices)...)
 		} else {
-			g.builder.WriteString(strings.Repeat("\t", g.indent))
-			g.write(fmt.Sprintf("for %s := ", v))
-			g.generateExpression(stmt.Replicator.Start)
-			g.write(fmt.Sprintf("; %s < ", v))
-			g.generateExpression(stmt.Replicator.Start)
-			g.write(" + ")
-			g.generateExpression(stmt.Replicator.Count)
-			g.write(fmt.Sprintf("; %s++ {\n", v))
-			g.indent++
+			flat = append(flat, c)
 		}
+	}
+	return flat
+}
 
-		for i, choice := range stmt.Choices {
+// generateReplicatedIfLoop emits a for loop that breaks on first matching choice.
+// When withinFlag is true, it sets _ifmatched = true before breaking.
+func (g *Generator) generateReplicatedIfLoop(stmt *ast.IfStatement, withinFlag bool) {
+	repl := stmt.Replicator
+	v := repl.Variable
+	if repl.Step != nil {
+		counter := "_repl_" + v
+		g.builder.WriteString(strings.Repeat("\t", g.indent))
+		g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
+		g.generateExpression(repl.Count)
+		g.write(fmt.Sprintf("; %s++ {\n", counter))
+		g.indent++
+		g.builder.WriteString(strings.Repeat("\t", g.indent))
+		g.write(fmt.Sprintf("%s := ", v))
+		g.generateExpression(repl.Start)
+		g.write(fmt.Sprintf(" + %s * ", counter))
+		g.generateExpression(repl.Step)
+		g.write("\n")
+	} else {
+		g.builder.WriteString(strings.Repeat("\t", g.indent))
+		g.write(fmt.Sprintf("for %s := ", v))
+		g.generateExpression(repl.Start)
+		g.write(fmt.Sprintf("; %s < ", v))
+		g.generateExpression(repl.Start)
+		g.write(" + ")
+		g.generateExpression(repl.Count)
+		g.write(fmt.Sprintf("; %s++ {\n", v))
+		g.indent++
+	}
+
+	for i, choice := range stmt.Choices {
+		g.builder.WriteString(strings.Repeat("\t", g.indent))
+		if i == 0 {
+			g.write("if ")
+		} else {
+			g.write("} else if ")
+		}
+		g.generateExpression(choice.Condition)
+		g.write(" {\n")
+		g.indent++
+
+		for _, s := range choice.Body {
+			g.generateStatement(s)
+		}
+		if withinFlag {
+			g.writeLine("_ifmatched = true")
+		}
+		g.writeLine("break")
+
+		g.indent--
+	}
+	g.writeLine("}")
+
+	g.indent--
+	g.writeLine("}")
+}
+
+// generateIfChoiceChain emits a chain of if/else-if for the given choices.
+// When a replicated nested IF is encountered, it splits the chain and uses
+// a _ifmatched flag to determine whether remaining choices should be tried.
+func (g *Generator) generateIfChoiceChain(choices []ast.IfChoice, isFirst bool) {
+	// Find first replicated nested IF
+	replIdx := -1
+	for i, c := range choices {
+		if c.NestedIf != nil && c.NestedIf.Replicator != nil {
+			replIdx = i
+			break
+		}
+	}
+
+	if replIdx == -1 {
+		// No replicated nested IFs — simple if/else-if chain
+		for i, choice := range choices {
 			g.builder.WriteString(strings.Repeat("\t", g.indent))
-			if i == 0 {
+			if i == 0 && isFirst {
 				g.write("if ")
 			} else {
 				g.write("} else if ")
@@ -1591,18 +1691,25 @@ func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
 			for _, s := range choice.Body {
 				g.generateStatement(s)
 			}
-			g.writeLine("break")
 
 			g.indent--
 		}
-		g.writeLine("}")
+		if len(choices) > 0 {
+			g.writeLine("}")
+		}
+		return
+	}
 
-		g.indent--
-		g.writeLine("}")
-	} else {
-		for i, choice := range stmt.Choices {
+	// Split at the replicated nested IF
+	before := choices[:replIdx]
+	replChoice := choices[replIdx]
+	after := choices[replIdx+1:]
+
+	// Emit choices before the replicated IF as a normal if-else chain
+	if len(before) > 0 {
+		for i, choice := range before {
 			g.builder.WriteString(strings.Repeat("\t", g.indent))
-			if i == 0 {
+			if i == 0 && isFirst {
 				g.write("if ")
 			} else {
 				g.write("} else if ")
@@ -1610,13 +1717,36 @@ func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
 			g.generateExpression(choice.Condition)
 			g.write(" {\n")
 			g.indent++
-
 			for _, s := range choice.Body {
 				g.generateStatement(s)
 			}
-
 			g.indent--
 		}
+		// Open else block for the replicated IF + remaining choices
+		g.builder.WriteString(strings.Repeat("\t", g.indent))
+		g.write("} else {\n")
+		g.indent++
+	}
+
+	// Emit the replicated nested IF with a flag
+	needFlag := len(after) > 0
+	if needFlag {
+		g.writeLine("_ifmatched := false")
+	}
+	g.generateReplicatedIfLoop(replChoice.NestedIf, needFlag)
+
+	// Emit remaining choices inside if !_ifmatched (recursive for multiple)
+	if len(after) > 0 {
+		g.builder.WriteString(strings.Repeat("\t", g.indent))
+		g.write("if !_ifmatched {\n")
+		g.indent++
+		g.generateIfChoiceChain(after, true) // recursive for remaining
+		g.indent--
+		g.writeLine("}")
+	}
+
+	if len(before) > 0 {
+		g.indent--
 		g.writeLine("}")
 	}
 }
diff --git a/codegen/e2e_control_test.go b/codegen/e2e_control_test.go
index ccedf8a..e07f032 100644
--- a/codegen/e2e_control_test.go
+++ b/codegen/e2e_control_test.go
@@ -259,6 +259,99 @@ func TestE2E_MultiStatementWhileBody(t *testing.T) {
 	}
 }
 
+func TestE2E_NestedReplicatedIfWithDefault(t *testing.T) {
+	// Replicated IF as a choice within outer IF, with TRUE default
+	occam := `SEQ
+  [5]INT arr:
+  INT result:
+  SEQ i = 0 FOR 5
+    arr[i] := i * 10
+  IF
+    IF i = 0 FOR 5
+      arr[i] > 25
+        result := arr[i]
+    TRUE
+      result := -1
+  print.int(result)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "30\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
+func TestE2E_NestedReplicatedIfNoMatch(t *testing.T) {
+	// Replicated IF where no choice matches, falls through to TRUE
+	occam := `SEQ
+  [3]INT arr:
+  INT result:
+  SEQ i = 0 FOR 3
+    arr[i] := i
+  IF
+    IF i = 0 FOR 3
+      arr[i] > 100
+        result := arr[i]
+    TRUE
+      result := -1
+  print.int(result)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "-1\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
+func TestE2E_NestedReplicatedIfWithPrecedingChoice(t *testing.T) {
+	// Normal choice before replicated IF, then default
+	occam := `SEQ
+  [3]INT arr:
+  INT result:
+  SEQ i = 0 FOR 3
+    arr[i] := i
+  INT x:
+  x := 99
+  IF
+    x > 100
+      result := x
+    IF i = 0 FOR 3
+      arr[i] = 2
+        result := arr[i]
+    TRUE
+      result := -1
+  print.int(result)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "2\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
+func TestE2E_NestedNonReplicatedIf(t *testing.T) {
+	// Non-replicated nested IF (choices inlined into parent)
+	occam := `SEQ
+  INT x:
+  INT result:
+  x := 5
+  IF
+    IF
+      x > 10
+        result := 1
+      x > 3
+        result := 2
+    TRUE
+      result := 0
+  print.int(result)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "2\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
 func TestE2E_ChannelDirAtCallSite(t *testing.T) {
 	occam := `PROC worker(CHAN OF INT in?, CHAN OF INT out!)
   INT x:
diff --git a/parser/parser.go b/parser/parser.go
index 3340c70..accf746 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1238,12 +1238,17 @@ func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.Var
 			break
 		}
 
+		// Safety guard: record position before parsing to detect no-progress
+		prevToken := p.curToken
+		prevPeek := p.peekToken
+
 		// Parse a variant case: tag [; var]* \n INDENT body
 		vc := ast.VariantCase{}
 
 		if !p.curTokenIs(lexer.IDENT) {
 			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
-			return stmt
+			p.nextToken() // skip unrecognized token to avoid infinite loop
+			continue
 		}
 		vc.Tag = p.curToken.Literal
 
@@ -1273,6 +1278,14 @@ func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.Var
 		}
 
 		stmt.Cases = append(stmt.Cases, vc)
+
+		// No-progress guard: if we haven't moved, break to prevent infinite loop
+		if p.curToken == prevToken && p.peekToken == prevPeek {
+			p.nextToken() // force progress
+			if p.curToken == prevToken {
+				break
+			}
+		}
 	}
 
 	return stmt
@@ -1323,11 +1336,16 @@ func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndex ast.E
 			break
 		}
 
+		// Safety guard: record position before parsing to detect no-progress
+		prevToken := p.curToken
+		prevPeek := p.peekToken
+
 		vc := ast.VariantCase{}
 
 		if !p.curTokenIs(lexer.IDENT) {
 			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
-			return stmt
+			p.nextToken() // skip unrecognized token to avoid infinite loop
+			continue
 		}
 		vc.Tag = p.curToken.Literal
 
@@ -1354,6 +1372,14 @@ func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndex ast.E
 		}
 
 		stmt.Cases = append(stmt.Cases, vc)
+
+		// No-progress guard: if we haven't moved, break to prevent infinite loop
+		if p.curToken == prevToken && p.peekToken == prevPeek {
+			p.nextToken() // force progress
+			if p.curToken == prevToken {
+				break
+			}
+		}
 	}
 
 	return stmt
@@ -1517,11 +1543,20 @@ func (p *Parser) parseAltCases() []ast.AltCase {
 			break
 		}
 
+		// Safety guard: record position before parsing to detect no-progress
+		prevToken := p.curToken
+		prevPeek := p.peekToken
+
 		// Parse an ALT case: [guard &] channel ? var
 		altCase := p.parseAltCase()
 		if altCase != nil {
 			cases = append(cases, *altCase)
 		}
+
+		// No-progress guard: if we haven't moved, break to prevent infinite loop
+		if p.curToken == prevToken && p.peekToken == prevPeek {
+			break
+		}
 	}
 
 	return cases
@@ -2269,21 +2304,37 @@ func (p *Parser) parseIfStatement() *ast.IfStatement {
 			break
 		}
 
+		// Safety guard: record position before parsing to detect no-progress
+		prevToken := p.curToken
+		prevPeek := p.peekToken
+
 		choice := ast.IfChoice{}
-		choice.Condition = p.parseExpression(LOWEST)
 
-		// Skip newlines and expect INDENT for body
-		for p.peekTokenIs(lexer.NEWLINE) {
-			p.nextToken()
-		}
+		// Nested IF (plain or replicated) used as a choice within this IF
+		if p.curTokenIs(lexer.IF) {
+			nestedIf := p.parseIfStatement()
+			choice.NestedIf = nestedIf
+		} else {
+			choice.Condition = p.parseExpression(LOWEST)
 
-		if p.peekTokenIs(lexer.INDENT) {
-			p.nextToken() // consume INDENT
-			p.nextToken() // move to body
-			choice.Body = p.parseBodyStatements()
+			// Skip newlines and expect INDENT for body
+			for p.peekTokenIs(lexer.NEWLINE) {
+				p.nextToken()
+			}
+
+			if p.peekTokenIs(lexer.INDENT) {
+				p.nextToken() // consume INDENT
+				p.nextToken() // move to body
+				choice.Body = p.parseBodyStatements()
+			}
 		}
 
 		stmt.Choices = append(stmt.Choices, choice)
+
+		// No-progress guard: if we haven't moved, break to prevent infinite loop
+		if p.curToken == prevToken && p.peekToken == prevPeek {
+			break
+		}
 	}
 
 	return stmt
@@ -2338,6 +2389,10 @@ func (p *Parser) parseCaseStatement() *ast.CaseStatement {
 			break
 		}
 
+		// Safety guard: record position before parsing to detect no-progress
+		prevToken := p.curToken
+		prevPeek := p.peekToken
+
 		choice := ast.CaseChoice{}
 
 		if p.curTokenIs(lexer.ELSE) {
@@ -2359,6 +2414,11 @@ func (p *Parser) parseCaseStatement() *ast.CaseStatement {
 		}
 
 		stmt.Choices = append(stmt.Choices, choice)
+
+		// No-progress guard: if we haven't moved, break to prevent infinite loop
+		if p.curToken == prevToken && p.peekToken == prevPeek {
+			break
+		}
 	}
 
 	return stmt
$ Bash
Show recent commit messages
git log --oneline -5
bb7ac78 Merge pull request #24 from codeassociates/ci/github-actions
3394b50 Add GitHub Actions CI workflow
857246b Merge pull request #23 from codeassociates/fix/multi-statement-bodies
b088adc Support multi-statement bodies in IF/CASE/WHILE/ALT and call-site channel direction
5cbfc5a Merge pull request #22 from codeassociates/docs/consolidate-todo-implemented
$ Bash
Create feature branch
git checkout -b fix/nested-if-infinite-loop
Switched to a new branch 'fix/nested-if-infinite-loop'
$ Bash
Stage changed files
git add ast/ast.go codegen/codegen.go codegen/e2e_control_test.go parser/parser.go
$ Bash
Commit changes
git commit -m "$(cat <<'EOF'
Fix infinite loops in parser and support nested IF constructs

The parser would enter infinite loops (consuming memory until OOM) when
encountering: (1) nested/replicated IF within an IF block, and (2)
unknown type declarations inside variant receive CASE blocks. Both
caused by parsing loops that failed to advance the token position on
unrecognized constructs.

Fix nested IF by detecting IF tokens as choices in parseIfStatement and
recursively parsing them, storing in a new IfChoice.NestedIf field.
Codegen flattens non-replicated nested IFs and emits replicated ones
as loops with a _ifmatched flag. Add progress guards to all parser
loops (IF, CASE, ALT, variant receive) as a safety net.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
4 files changed, 329 insertions(+), 45 deletions(-)
$ Bash
Push branch to remote
git push -u origin fix/nested-if-infinite-loop 2>&1
remote: 
remote: Create a pull request for 'fix/nested-if-infinite-loop' on GitHub by visiting:        
remote:      https://github.com/codeassociates/occam2go/pull/new/fix/nested-if-infinite-loop        
remote: 
To associate-1.github.com:codeassociates/occam2go.git
 * [new branch]      fix/nested-if-infinite-loop -> fix/nested-if-infinite-loop
branch 'fix/nested-if-infinite-loop' set up to track 'origin/fix/nested-if-infinite-loop'.
$ Bash
Create pull request
gh pr create --title "Fix infinite loops in parser and support nested IF" --body "$(cat <<'EOF'
## Summary

- **Fix OOM-inducing infinite loops** in the parser that occurred when transpiling real-world occam code (e.g., KRoC course module). Two root causes: (1) nested/replicated `IF` within `IF` blocks caused `parseExpression` to fail without advancing the cursor, and (2) unknown type declarations in variant `? CASE` blocks caused the same stall pattern.
- **Support nested IF constructs** — both plain nested `IF` (choices inlined into parent chain) and replicated `IF i = 0 FOR n` (emitted as a loop with `_ifmatched` flag). This is a common occam idiom for searching arrays.
- **Add progress guards** to all parser loops (`IF`, `CASE`, `ALT`, variant receive) as a safety net against future infinite loop regressions.

## Test plan

- [x] All existing tests pass (`go test ./...`)
- [x] 4 new e2e tests: replicated IF with default, no-match fallthrough, preceding normal choice, non-replicated nested IF
- [x] All KRoC course module `.occ` files terminate without hanging (previously 4 of 9 caused OOM)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
https://github.com/codeassociates/occam2go/pull/25

PR created: https://github.com/codeassociates/occam2go/pull/25

Thank you. If the PR has been approved and passed CI checks please merge.

$ Bash
Check PR approval and CI status
gh pr view 25 --json state,reviewDecision,statusCheckRollup 2>&1
{"reviewDecision":"APPROVED","state":"OPEN","statusCheckRollup":[{"__typename":"CheckRun","completedAt":"2026-02-19T19:02:21Z","conclusion":"SUCCESS","detailsUrl":"https://github.com/codeassociates/occam2go/actions/runs/22195857863/job/64195835398","name":"test","startedAt":"2026-02-19T19:01:46Z","status":"COMPLETED","workflowName":"CI"}]}

PR is approved and CI passed. Merging now.

$ Bash
Merge PR #25
gh pr merge 25 --merge 2>&1
$ Bash
Confirm PR is merged
gh pr view 25 --json state --jq .state 2>&1
MERGED

PR #25 has been merged.

Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to.

/exit exit