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.
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.
{
"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.
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
? 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.
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.
{
"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→{
"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→Let me look at what constructs these files use that might cause the infinite loop. I notice:
string.occ has #USE "utils.tce" - which the preprocessor would try to processfile_in.occ has #INCLUDE "consts.inc" and #PRAGMA DEFINED sBOOL FUNCTION, VALOF/RESULT patternsfile_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.
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: $?"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: $?"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: 0package 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: 0package main
func test(in <-chan byte) {
var ch byte
ch = <-in
for (ch != byte(10)) {
ch = <-in
}
}
Exit: 0Those work. Let me try the #USE directive and multi-function files, and also try BOOL, INT FUNCTION (multi-result).
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: $?"
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: $?"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 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: 0package 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: 0Those 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.
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: $?"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:
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".
{
"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→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: 0OK, 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:
timeout 5 ./occam2go -I kroc/modules/course/libsrc kroc/modules/course/libsrc/file_in.occ -o /dev/null 2>&1; echo "Exit: $?"
Hangs with the include path. Let me check if the preprocessor is the issue by running without the include path:
timeout 5 ./occam2go kroc/modules/course/libsrc/file_in.occ -o /dev/null 2>&1; echo "Exit: $?"
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):
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: 0That 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:
# 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: $?"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.
# 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: $?"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:
# 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: $?"# 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: $?"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:
# 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: $?"# 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: $?"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:
{
"pattern": "func.*parseIfStatement",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content"
}{
"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→ }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
parseIfStatement() is called for the outer IFp.peekTokenIs(lexer.IDENT) - NO, the peek token is NEWLINE (after outer IF)Current token is IF (the inner replicated IF)
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:
{
"pattern": "func.*parseExpression",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content"
}{
"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),{
"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→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:
{
"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 tokenNow 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:
{
"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:
{
"pattern": "func.*parseBodyStatements",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content"
}{
"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→ }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:
{
"pattern": "func.*generateIfStatement",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content"
}{
"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:
{
"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 statementtype 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.
{
"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} // 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:
{
"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))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:
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:
{
"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:
{
"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:
{
"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 { 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.
{
"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 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.
{
"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→ } 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.
{
"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→ } 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.
{
"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 {{
"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→ } 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.
{
"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→ } 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.
{
"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 { 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.
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# 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: 0for 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:
{
"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→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
COLOUR since it's not recognized as a typeThe 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:
{
"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→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.
# 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: 0That works. The issue is somewhere else in the file. Let me try the actual file directly (without includes) to narrow it down:
# 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: $?"
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:
# 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: $?"
# 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: $?"
# 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.
# 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
# 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]}"The third proc (screen.driver) causes the hang. This proc uses in ? CASE (variant receive). Let me narrow it further:
# 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:
# 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]}"# 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# 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:
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
# 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:
# 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
# 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: 0That 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.
# 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:
# 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: $?"# 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: 0Doesn'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:
# 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: $?"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:
{
"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 {{
"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{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:
COORDINATES is treated as a variant tag (since it's an IDENT)p is after COORDINATES, and peek is p (IDENT), not SEMICOLONGoes to "Skip newlines and expect INDENT for body" - peek is p (IDENT), not NEWLINE
Expects INDENT for body but finds p (IDENT) instead, so no body gets parsed
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:
{
"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→} // 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.
{
"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→{
"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→ 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:
{
"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 {{
"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: // 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.
{
"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{
"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:
// 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.
? 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)
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:
# 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: 1timeout 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: 1The 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.
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:
{
"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{
"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→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.
=== 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
? 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)
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:
Two distinct infinite loop bugs were identified:
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.
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.
| 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 |
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")
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 stmtbb7ac78 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
git add ast/ast.go codegen/codegen.go codegen/e2e_control_test.go parser/parser.go
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(-)
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'.
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 )"
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"}]}