I'd like you to resume work on a large feature: support for transpiling programs that use the occam "course" library module, for example this program: ./kroc/demos/complexity/concurrent_hello_1.occ . Previously you implemented various features that were prerequisites for this: support for #IMPORT and several language features that had the goal to be able to compile the course module's source code. The module's code can be found at ./kroc/modules/course . One thing I'm not sure about is whether we need to be able to run occbuild (a Python tool from the kroc project). Perhaps that isn't necessary if the transpiler simply follows the #INCLUDE statements recursively.
The user wants me to resume work on supporting the occam "course" library module for transpilation. Let me start by understanding the current state of things - what's in the codebase, what the course module looks like, and what the demo program needs.
Let me explore the relevant files to understand the situation.
{
"subagent_type": "Explore",
"prompt": "I need to understand the occam \"course\" library module and a demo program that uses it. Please:\n\n1. Read the file ./kroc/demos/complexity/concurrent_hello_1.occ\n2. List all files in ./kroc/modules/course/\n3. Read the main course module source files (look for .occ files in ./kroc/modules/course/)\n4. Check if there's a SConscript or build file in ./kroc/modules/course/\n5. Read TODO.md in the project root for context on what's planned\n6. Check if there's any existing #IMPORT or #USE handling in the codebase (search for \"IMPORT\" and \"USE\" in the preproc/, parser/, and main.go files)\n\nBe very thorough - read all relevant .occ files in the course module directory."
}[
{
"type": "text",
"text": "Perfect! Now I have all the information I need. Let me compile the comprehensive report:\n\n## Summary Report: occam \"course\" Library Module and Demo Program\n\n### 1. Demo Program: concurrent_hello_1.occ\n\n**Location:** `/home/david/projects/code-associates/occam2go/kroc/demos/complexity/concurrent_hello_1.occ`\n\n**Purpose:** Demonstrates a simple concurrent architecture with multiple writer processes sending data to a single output collector process via channels.\n\n**Architecture:**\n- **outer/3** — Main procedure with three channels (keyboard input, screen output, error output)\n- **writer.1()** / **writer.2()** — Two parallel writer processes; each sends a single integer ID down its channel\n- **output.collector/2** — Server process that receives from both writers via ALT (channel alternation) and prints the received values to the output channel\n\n**Key Pattern:** \n- Uses `#INCLUDE \"course.module\"` to include the course module library\n- Calls `out.int()` and `out.string()` from the course library to print values\n- Demonstrates ALT with two channel inputs and buffered output printing\n\n**Outputs:**\n```\nstarting the writers...*n\n1\n2 (or any order due to parallel execution)\nwriters have finished.*n\n```\n\n---\n\n### 2. Course Module Directory Structure\n\n**Location:** `/home/david/projects/code-associates/occam2go/kroc/modules/course/`\n\n**Directory Layout:**\n```\ncourse/\n├── README # Overview documentation\n├── SConscript # Build script (imports libsrc, answers, examples)\n├── libsrc/ # Main library sources\n│ ├── consts.inc # ASCII constants (NULL, BELL, ESCAPE, etc.)\n│ ├── utils.occ # Output/input/screen utilities\n│ ├── string.occ # String manipulation functions (#USE \"utils.tce\")\n│ ├── random.occ # Pseudorandom number generator\n│ ├── shared_screen.inc # Shared screen definitions (DATA TYPE syntax)\n│ ├── shared_screen.occ # Shared screen implementation\n│ ├── float_io.occ # REAL32/REAL64 conversion utilities\n│ ├── file_in.occ # File input procedures\n│ ├── demo_cycles.occ # Example cycle processes\n│ ├── demo_nets.occ # Example network patterns\n│ ├── stringbuf.occ # String buffer operations (#USE \"float_io\")\n│ └── SConscript # Builds course.lib and shared_screen.lib\n├── exercises/ # Exercise stub programs (with course.module #INCLUDE)\n├── examples/ # Example programs demonstrating the library\n├── answers/ # Model answers to exercises\n└── etc/ # Additional reference files\n```\n\n---\n\n### 3. Main Course Module Library Files (libsrc/*.occ)\n\n#### **consts.inc** (Lines 1-56)\nASCII character constants and utility values:\n```occam\nVAL BYTE NULL IS 0:\nVAL BYTE BELL IS 7: -- terminal bell\nVAL BYTE BACK IS 8: -- backspace\nVAL BYTE ESCAPE IS 27: -- escape key\nVAL BYTE DELETE IS 127: -- delete key\nVAL BYTE FLUSH IS 255: -- flush output buffer\nVAL INT string.less IS -1: -- compare.string return values\nVAL INT string.same IS 0:\nVAL INT string.more IS 1:\n```\n\n#### **utils.occ** (Lines 1-1132)\nComprehensive I/O and screen utilities:\n\n**Output Functions:**\n- `out.repeat/3` — Repeat a character n times to a channel\n- `out.ch/3` — Output a character with right-justified field width\n- `out.string/3` — Output a string with field width\n- `out.byte/3` — Output a byte in decimal\n- `out.int/3` — Output an integer in decimal (handles MOSTNEG INT special case)\n- `out.hex/3` — Output an integer in hexadecimal\n- `out.bool/3` — Output boolean as \"TRUE\"/\"FALSE\"\n- `out.yes.no/3` — Output boolean as \"yes\"/\"no\"\n- `flush/1` — Flush output channel\n\n**Input Functions:**\n- `in.skip/2` — Skip whitespace and return first non-space character\n- `in.digit/3` — Read a single digit with error feedback (beeped on invalid)\n- `in.string/5` — Interactive string input with backspace/delete support\n- `in.bool/3` — Interactive boolean input (y/Y or n/N)\n- `in.byte/4` — Read a byte value (0-255) with validation\n- `in.int/4` — Read an integer with overflow protection (using checked arithmetic PLUS/MINUS/TIMES)\n- Helper functions: `in.byte.chars`, `in.num.chars`, `convert.byte.chars`, `convert.num.chars`, `check.num.chars`\n\n**Interactive I/O with Prompts:**\n- `ask.string/6`, `ask.bool/4`, `ask.byte/5`, `ask.int/5` — All prompt versions\n\n**Screen Control (VT220 protocol):**\n- `cursor.x.y/3` — Position cursor\n- `cursor.up/2`, `cursor.down/2`, `cursor.left/2`, `cursor.right/2` — Move cursor\n- `erase.eol/1`, `erase.bol/1`, `erase.line/1` — Erase portions of line\n- `erase.eos/1`, `erase.bos/1`, `erase.screen/1` — Erase screen areas\n- `cursor.visible/1`, `cursor.invisible/1` — Toggle cursor visibility\n- `pos.int/5`, `pos.yes.no/6` — Composite: move cursor and print\n\n**Deprecated:**\n- `out.number/3` — Synonym for out.int\n- `goto.x.y/3` — Synonym for cursor.x.y\n\n#### **string.occ** (Lines 1-303)\nString manipulation with #USE \"utils.tce\" dependency:\n- `make.string/2` — Null-pad a byte array\n- `copy.string/2` — Copy and pad/truncate strings\n- `equal.string/2` — String equality test\n- `compare.string/2` — Lexicographic string comparison\n- `starts.with.string/2` — Prefix check\n- `ends.with.string/2` — Suffix check\n- `byte.in.string/2` — Character search\n- `string.to.int/1` — Convert string to integer (two-result: success, value)\n- `write.string/3` — Output NULL-padded string with field width\n\n#### **random.occ** (Lines 1-87)\nPseudorandom number generator:\n- `INT, INT FUNCTION random (VAL INT upto, seed)` — Two-result function\n- Returns random value in [0, upto) and new seed\n- 32-bit platforms: Park & Miller \"minimal standard\" (period 2^31 - 1)\n- 16-bit platforms: LFSR with period (2^16) - 1\n- Uses checked arithmetic: `TIMES`, `MINUS`, `PLUS` operators\n- Conditional compilation: `#IF (TARGET.BITS.PER.WORD = 32)`\n\n#### **shared_screen.inc** (Partial, Lines 1-100)\nAdvanced data types (not yet fully supported in occam2go):\n- `DATA TYPE COLOUR IS BYTE:` — Type alias (requires DATA TYPE support)\n- `DATA TYPE COORDINATES RECORD { BYTE X, Y }` — Composite record\n- `DATA TYPE COORDINATES.COLOUR RECORD { BYTE X, Y; COLOUR col }`\n- `DATA TYPE SCREEN.NODE RECORD { BYTE tag, a, b }` — Command batch instruction\n- Color constants: `default`, `black`, `red`, `green`, etc. (0-15 values)\n- Command tags for screen operations (0-20)\n\n#### **float_io.occ** (First 80 lines shown)\nREAL32/REAL64 floating-point conversion:\n- `PROC course.REAL32TOSTRING/5` — Format REAL32 as decimal string\n- Supports three formatting modes: free format, fixed-point, exponential\n- Handles special values: Inf, -Inf, NaN\n- **Not yet implemented** in occam2go (uses unsupported features like RETYPES, CAUSEERROR)\n\n#### **stringbuf.occ** (First 100 lines shown)\nString buffer operations with `#USE \"float_io\"` dependency:\n- `PROC buffer.set.string/3` — Add string to buffer\n- `PROC buffer.set.int/3` — Add formatted integer to buffer\n- Nested `INT FUNCTION e10/1` — Power of 10 calculator\n\n#### **demo_cycles.occ**\nExample process patterns documented in the module:\n- `id/2` — Copy input to output\n- `succ/2` — Add 1 to each input (using PLUS checked arithmetic)\n\n---\n\n### 4. SConscript and Module File Generation\n\n**Location:** `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/SConscript`\n\n**Purpose:** KRoC build script specifying library composition.\n\n**Content:**\n```python\ncourse_lib_srcs = Split('''\n utils.occ\n string.occ\n demo_cycles.occ\n demo_nets.occ\n file_in.occ\n float_io.occ\n random.occ\n ''')\n\ncourse_lib = local.OccamLibrary(\n 'course.lib',\n course_lib_srcs,\n INCPATH='.',\n OCCBUILDFLAGS='--include consts.inc')\n\nshared_screen_lib_srcs = Split('''\n shared_screen.occ\n ''')\n\nsharedscreen_lib = local.OccamLibrary(\n 'shared_screen.lib',\n shared_screen_lib_objs,\n INCPATH='.',\n OCCBUILDFLAGS='--need course --include shared_screen.inc')\n```\n\n**Module Generation via modgen:**\n\nThe `modgen` package in occam2go parses this SConscript to generate a `.module` file:\n\n```go\n// Key types\ntype Library struct {\n Name string // e.g., \"course.lib\"\n Sources []string // [\"utils.occ\", \"string.occ\", ...]\n Includes []string // [\"consts.inc\"]\n Needs []string // dependencies\n}\n\n// GenerateModule creates: course.module\n#IF NOT (DEFINED (COURSE.MODULE))\n#DEFINE COURSE.MODULE\n#INCLUDE \"consts.inc\"\n#INCLUDE \"utils.occ\"\n#INCLUDE \"string.occ\"\n...\n#ENDIF\n```\n\n**Result:** A single `course.module` file with include guards that textually includes all library components.\n\n---\n\n### 5. #INCLUDE, #USE, and #IMPORT Handling\n\n**Current Status in occam2go:**\n\n**Preprocessor (preproc/preproc.go, Lines 1-200+)**\n\nImplements:\n- **#INCLUDE \"file\"** — File inclusion with search paths and circular include detection\n- **#IF/#ELSE/#ENDIF** — Conditional compilation\n- **#DEFINE symbol** — Symbol definition\n- **#COMMENT, #PRAGMA, #USE** — Ignored (replaced with blank lines for line number preservation)\n\n```go\ncase \"COMMENT\", \"PRAGMA\", \"USE\":\n out.WriteString(\"\") // no-op, blank line\n\ncase \"INCLUDE\":\n if isActive(condStack) {\n included, err := pp.resolveAndInclude(rest, baseDir)\n // Textually includes file content\n }\n```\n\n**#USE Directive:**\n- Currently treated as a no-op (blank line)\n- Used in libsrc/string.occ: `#USE \"utils.tce\"` (references compiled module)\n- Used in answers/q2.occ: `#USE \"q2-1\"` (references answer fragments)\n- **Not currently implemented** for dynamic linking in occam2go\n\n**#IMPORT Directive:**\n- No occurrences found in the codebase\n- Not implemented\n\n---\n\n### 6. Key Implementation Details\n\n**Preprocessor Symbols:**\n- `#IF NOT (DEFINED (COURSE.MODULE))` — Include guard pattern\n- `#DEFINE COURSE.MODULE` — Guard definition\n- `TARGET.BITS.PER.WORD = 64` — Predefined symbol\n\n**Include Paths (-I flag):**\nThe transpiler accepts `-I includepath` arguments:\n```bash\n./occam2go -I examples -o include_demo.go examples/include_demo.occ\n```\n\n**Checked Arithmetic in utils.occ:**\nUses PLUS, MINUS, TIMES operators for modular arithmetic:\n```occam\nnew.seed := test PLUS period\nint.result := new.seed \\ upto \nx := (10 TIMES number) PLUS ((INT s[i]) - (INT '0'))\n```\n\n**Data Types Not Yet Implemented:**\n- `DATA TYPE X IS TYPE:` — Simple type alias (needed for shared_screen.inc)\n- `BYTE::[]BYTE` — Counted array protocol\n- `RESULT` param qualifier — Output-only parameters\n- `RETYPES` — Type punning\n- `CAUSEERROR()` — Error signaling\n\n---\n\n### 7. File Structure Summary\n\n**All .occ Files in libsrc:**\n1. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/utils.occ` (1132 lines) — Core I/O\n2. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/string.occ` (303 lines) — String ops\n3. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/random.occ` (87 lines) — PRNG\n4. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/consts.inc` (56 lines) — Constants\n5. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_cycles.occ` — Process patterns\n6. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_nets.occ` — Network patterns\n7. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/float_io.occ` — Float conversion\n8. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/file_in.occ` — File input\n9. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/stringbuf.occ` — Buffer ops\n10. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/shared_screen.inc` — Screen defs\n11. `/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/shared_screen.occ` — Screen impl\n\n---\n\n### 8. Integration with concurrent_hello_1.occ\n\nThe demo program uses:\n- **#INCLUDE \"course.module\"** — Pulls in all utilities (via textual preprocessing)\n- **out.int/3** — Outputs integer value (defined in utils.occ)\n- **out.string/3** — Outputs string with field width (defined in utils.occ)\n- **ALT** — Channel alternation (core occam construct)\n- **PAR** — Parallel composition (goroutines in occam2go)\n\nThe transpiler converts:\n```occam\nout.int(data1, 0, out!)\nout.string(\"*n\", 0, out!)\n```\n\nTo:\n```go\noutInt(data1, 0, out)\noutString(\"\\n\", 0, out) // where \"\\n\" is unescaped from \"*n\"\n```"
},
{
"type": "text",
"text": "agentId: a8a8f52 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 63254\ntool_uses: 35\nduration_ms: 83099</usage>"
}
]{
"subagent_type": "Explore",
"prompt": "Read the following files thoroughly:\n1. /home/david/projects/code-associates/occam2go/TODO.md\n2. /home/david/projects/code-associates/occam2go/preproc/preproc.go - full file\n3. /home/david/projects/code-associates/occam2go/main.go - full file\n4. Check if there's a course.module file anywhere in the project (glob for *.module)\n5. Read /home/david/projects/code-associates/occam2go/kroc/demos/complexity/concurrent_hello_1.occ\n6. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/utils.occ (full file)\n7. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_cycles.occ\n8. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_nets.occ\n9. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/random.occ\n\nBe very thorough - read the complete files."
}[
{
"type": "text",
"text": "Perfect! I have now thoroughly read all the requested files. Let me compile my findings into a comprehensive report.\n\n## Comprehensive Code Analysis Report\n\n### 1. **TODO.md - Implementation Status**\n\nThe file documents the comprehensive implementation status of the occam2go transpiler. Key sections include:\n\n**Fully Implemented Features:**\n- Core constructs: SEQ, PAR, IF, WHILE, CASE, ALT (with guards and timers), SKIP, STOP\n- Data types: INT, BYTE, BOOL, REAL32, REAL64\n- Arrays and channels (including channel arrays with `[n]CHAN OF TYPE`)\n- Channel direction modifiers (receive-only `?`, send-only `!`)\n- Procedures and functions (including multi-result functions)\n- Nested procedures/functions (as Go closures)\n- Protocols (simple, sequential, and variant types)\n- Records with field access via bracket syntax\n- Preprocessor directives: `#IF/#ELSE/#ENDIF`, `#DEFINE`, `#INCLUDE` with search paths\n- Checked arithmetic operators: `PLUS`, `MINUS`, `TIMES` (modular/wrapping)\n- `MOSTNEG`/`MOSTPOS` constants for type min/max values\n- Array slices with `[arr FROM n FOR m]` syntax\n- Multi-assignment including indexed targets\n- The `gen-module` subcommand for generating `.module` files from KRoC SConscript files\n\n**Not Yet Implemented (Notable gaps):**\n- `DATA TYPE X IS TYPE:` — Simple type alias syntax\n- `DATA TYPE X RECORD` — Alternative record syntax\n- Counted array protocol (`BYTE::[]BYTE`)\n- `RESULT` param qualifier for output-only parameters\n- `PRI ALT` / `PRI PAR` — Priority variants\n- `PLACED PAR` — Hardware-specific process placement\n- `PORT OF` — Hardware port mapping\n- `RETYPES` — Type punning/reinterpret cast\n- `CAUSEERROR()` — Error-raising primitive\n- Transputer intrinsics\n- `VAL []BYTE` string constants\n- `#PRAGMA DEFINED`\n\n### 2. **preproc/preproc.go - Preprocessor Implementation**\n\nThis file implements a comprehensive textual preprocessor with:\n\n**Key Components:**\n- **Options pattern**: `WithIncludePaths()` and `WithDefines()` for configuration\n- **Include resolution**: Searches in order: relative to current file, then include paths\n- **Circular include detection**: Maintains `processing` map to prevent infinite loops\n- **Conditional compilation**: Supports `#IF`, `#ELSE`, `#ENDIF` with nested levels\n- **Expression evaluation**: `evalExpr()` method supports:\n - Boolean literals (`TRUE`/`FALSE`)\n - `DEFINED(SYMBOL)` checks\n - `NOT` operators\n - Equality checks: `(SYMBOL = value)`\n - Parenthesized expressions\n - Bare symbol (treated as DEFINED)\n- **Predefined symbols**: `TARGET.BITS.PER.WORD = \"64\"` built-in\n- **Line number preservation**: Writes blank lines for directives to maintain line count\n- **Directive handling**:\n - `#DEFINE`: Stores in defines map\n - `#COMMENT`, `#PRAGMA`, `#USE`: Silently ignored (converted to blank lines)\n - `#INCLUDE`: Recursively processes included files\n - Unknown directives: Pass through if active\n\n### 3. **main.go - CLI Entry Point**\n\nThe main program orchestrates the transpilation pipeline:\n\n**Two main commands:**\n\n1. **Default transpilation** (`./occam2go [options] input.occ`):\n - Flags:\n - `-o output.go`: Specify output file (default: stdout)\n - `-I path`: Include search paths (repeatable)\n - `-D SYMBOL` or `-D SYMBOL=value`: Predefined symbols (repeatable)\n - `--version`: Print version (0.1.0)\n - Pipeline: Preprocess → Lex → Parse → Generate\n - Error handling: Reports preprocessor warnings, parse errors\n\n2. **Module generation** (`./occam2go gen-module [options] SConscript`):\n - Flags:\n - `-o output`: Output file (default: stdout)\n - `-name GUARD`: Module guard name (default: derived from library name)\n - Parses Python-based SConscript files\n - Derives guard name format: `course.lib` → `COURSE.MODULE`\n\n### 4. **Glob Results - .module Files**\n\nFound 52 .module files across the project:\n- Main example: `/home/david/projects/code-associates/occam2go/examples/mathlib.module`\n- Most in `kroc/tvm/arduino/` directory tree with various subsystems:\n - Architecture-specific (m328p, m644p, m1280)\n - Community modules (gatoruav, prettys)\n - Platform-specific (Arduino variants)\n\n**Important finding**: No `course.module` file exists in the project root, but it's referenced via `#INCLUDE \"course.module\"` in the example files.\n\n### 5. **concurrent_hello_1.occ - Example Program**\n\nA demonstration of concurrent programming patterns in occam:\n- **Architecture**: Two writer processes, one output collector process\n- **Key constructs**:\n - Channel declarations: `CHAN INT w1.to.oc:`, `CHAN INT w2.to.oc:`\n - VAL abbreviations: `VAL INT id IS 1 :`\n - Nested procedures with `PROC` definitions inside outer procedures\n - Replicators: `SEQ i = 0 FOR number.of.writers`\n - ALT construct for non-deterministic choice from multiple channels\n - Parallel execution: `PAR` block with multiple processes\n - Calls to course.module utilities: `out.int()`, `out.string()`\n\n- **Pattern**: Client-server architecture where writers are clients sending to a collector server\n\n### 6. **utils.occ - Utility Library (1132 lines)**\n\nComprehensive I/O library from P.H. Welch (1997), part of course module:\n\n**Output procedures:**\n- `out.repeat()` - Write character n times\n- `out.ch()` - Write character with field width\n- `out.string()` - Write string with field width\n- `out.byte()` - Decimal byte output\n- `out.int()` - Decimal integer output with field width\n- `out.hex()` - Hexadecimal integer output\n- `out.bool()` - Boolean as TRUE/FALSE\n- `out.yes.no()` - Boolean as yes/no\n- `flush()` - Flush output\n\n**Input procedures:**\n- `in.skip()` - Skip whitespace\n- `in.digit()` - Read single digit\n- `in.string()` - Read string with editing\n- `in.bool()` - Read y/n or Y/N\n- `in.byte()` - Read decimal byte\n- `in.int()` - Read decimal integer with overflow checking\n\n**Helper functions:**\n- `convert.byte.chars()` - Parse byte from string\n- `check.num.chars()` - Validate integer format\n- `convert.num.chars()` - Parse integer with overflow detection (multi-result function)\n\n**Interactive input procedures (with prompts):**\n- `ask.string()`, `ask.bool()`, `ask.byte()`, `ask.int()`\n\n**Screen control (VT220 protocol):**\n- Cursor positioning: `cursor.x.y()`, `cursor.up()`, `cursor.down()`, `cursor.left()`, `cursor.right()`\n- Cursor visibility: `cursor.visible()`, `cursor.invisible()`\n- Screen/line erasing: `erase.eol()`, `erase.bol()`, `erase.line()`, `erase.eos()`, `erase.bos()`, `erase.screen()`\n- Combined operations: `pos.int()`, `pos.yes.no()`\n\n**Notable implementation details:**\n- Uses `#PRAGMA DEFINED` hints to suppress definedness warnings (line 170, 530, 751)\n- Handles special character constants like `FLUSH`, `BELL`, `BACK`, `DELETE`, `ESCAPE`, `NULL`\n- Field width right-justification with space padding\n- Multi-result function for integer conversion with overflow checking\n\n### 7. **demo_cycles.occ - Demonstration Cycles Library (99 lines)**\n\nCore CSP stream processing patterns from P.H. Welch (1984):\n\n**Basic cycle processes:**\n- `id()` - Identity/copy: Pass input stream to output\n- `succ()` - Increment: Add 1 to each number in stream\n- `plus()` - Addition: Combine two streams (parallel reads)\n- `delta()` - Duplicate/fan-out: Copy single stream to two outputs\n- `prefix()` - Prepend: Output fixed value, then pass through input\n- `tail()` - Skip first: Discard one value, then pass through\n\n**Implementation notes:**\n- Uses `WHILE TRUE` for infinite processes\n- Employs `PLUS` operator for modular arithmetic\n- Comments indicate overflow is ignored\n\n### 8. **demo_nets.occ - Demonstration Networks Library (103 lines)**\n\nHigher-level network compositions building on demo_cycles (P.H. Welch, 1984):\n\n**Network processes:**\n- `numbers()` - Generate integer stream 0, 1, 2, 3, ...\n- `integrate()` - Cumulative sum\n- `pairs()` - Transform A,B,C,... to A+B, B+C, ...\n- `squares()` - Generate square numbers 1, 4, 9, 16, ...\n- `fibonacci()` - Generate Fibonacci sequence\n- `times()` - Output stream of current time in microseconds\n\n**Key features:**\n- Demonstrates dataflow networks using PAR with multiple processes\n- Uses TIMER construct for time reading\n- Builds complex behaviors from simple, reusable cycles\n- Channel variable declarations for inter-process communication\n\n### 9. **random.occ - Random Number Generator (87 lines)**\n\nPseudorandom number generator (David Morse, 1985):\n\n**Implementation:**\n- Multi-result function: `INT, INT FUNCTION random (VAL INT upto, seed)`\n- Returns: (random_number, new_seed)\n- Platform-dependent:\n - **32-bit**: Park-Miller \"minimal standard\" algorithm\n - Constants: magic=16807, period=2147483647\n - Full period: 2^31 - 1 (2,147,483,647)\n - Uses checked arithmetic operators: `TIMES`, `MINUS`, `PLUS`\n - **16-bit**: LFSR (Linear Feedback Shift Register)\n - Period: (2^16) - 1\n - Uses bitwise operators: `>>`, `><` (XOR), `<<`, `\\/` (OR)\n- Conditional compilation: `#IF (TARGET.BITS.PER.WORD = 32)`\n- Range: Returns value between 0 and (upto - 1) inclusive\n\n**Algorithm notes (32-bit):**\n- Takes advantage of quotient/remainder decomposition to avoid overflow\n- Conditional adjustment: if test <= 0, adds period to result\n- Result is modulo operation: `new.seed \\ upto`\n\n---\n\n## Summary\n\nThis codebase implements a comprehensive occam-to-Go transpiler with a well-architected pipeline. The key files show:\n\n1. A **mature preprocessor** supporting conditional compilation and file inclusion\n2. A **complete CLI** with support for include paths and predefined symbols\n3. **Rich example libraries** demonstrating CSP patterns (cycles, networks)\n4. **Extensive I/O utilities** for console interaction and screen control\n5. **Platform-aware code** with conditional compilation for different word sizes\n\nThe project successfully transpiles occam's concurrent CSP model to Go's goroutines and channels, with support for most language features except for some advanced constructs like PRI variants and PLACED PAR."
},
{
"type": "text",
"text": "agentId: af279f0 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 47429\ntool_uses: 9\nduration_ms: 40181</usage>"
}
]Now I have a comprehensive understanding of the situation. Let me think about what's needed to transpile the concurrent_hello_1.occ demo program.
The demo uses #INCLUDE "course.module" - but there's no course.module file generated yet. The modgen package can generate it from the SConscript.
Let me think about the approach:
First, we need a course.module file. We can either:
a. Generate it using the gen-module command
b. Or have the transpiler handle #USE "course" / #INCLUDE "course.module" by finding the right files
Once the course.module file is in place, the preprocessor's #INCLUDE will pull in all the library source files recursively.
From the other library files, string.occ has a #USE directive that we're currently treating as a no-op, random.occ uses conditional compilation based on word size and multi-result functions, and the demo files rely on standard constructs plus TIMER usage. The real question is whether we can actually generate the course.module file and successfully transpile the demo with all these includes working together.
I need to understand how #USE directives work in KRoC—they reference compiled bytecode files, but since we're doing textual inclusion, the module file would need to include utils.occ before string.occ so those functions are already in scope. Let me trace through what the generated module structure would look like and whether the transpiler can handle the full dependency chain. I need to tackle the missing language features, but first let me check if the gen-module command can already generate a course.module file from the SConscript so I can see what we're working with.
Now I have a good understanding. Let me try the practical approach: generate a course.module file and attempt transpilation to see what breaks.
./occam2go gen-module kroc/modules/course/libsrc/SConscript 2>&1
#IF NOT (DEFINED (COURSE.MODULE)) #DEFINE COURSE.MODULE #INCLUDE "consts.inc" #INCLUDE "utils.occ" #INCLUDE "string.occ" #INCLUDE "demo_cycles.occ" #INCLUDE "demo_nets.occ" #INCLUDE "file_in.occ" #INCLUDE "float_io.occ" #INCLUDE "random.occ" #ENDIF
./occam2go gen-module -o kroc/modules/course/libsrc/course.module kroc/modules/course/libsrc/SConscript 2>&1
./occam2go -I kroc/modules/course/libsrc kroc/demos/complexity/concurrent_hello_1.occ 2>&1 | head -200
./occam2go -I kroc/modules/course/libsrc kroc/demos/complexity/concurrent_hello_1.occ 2>&1 | head -300
That last command seems to crash the OS, presumably due to running out of memory. I suspect that the library transpilation has some unbounded scenario, for example it is not detecting circular includes. Can you see if you can diagnose this problem?
1→// Package preproc implements a textual preprocessor for occam source files.
2→// It handles #IF/#ELSE/#ENDIF conditional compilation, #DEFINE symbols,
3→// #INCLUDE file inclusion, and ignores #COMMENT/#PRAGMA/#USE directives.
4→// The output is a single expanded string suitable for feeding into the lexer.
5→package preproc
6→
7→import (
8→ "fmt"
9→ "os"
10→ "path/filepath"
11→ "strings"
12→)
13→
14→// Option configures a Preprocessor.
15→type Option func(*Preprocessor)
16→
17→// WithIncludePaths sets the search paths for #INCLUDE resolution.
18→func WithIncludePaths(paths []string) Option {
19→ return func(pp *Preprocessor) {
20→ pp.includePaths = paths
21→ }
22→}
23→
24→// WithDefines sets predefined symbols.
25→func WithDefines(defs map[string]string) Option {
26→ return func(pp *Preprocessor) {
27→ for k, v := range defs {
28→ pp.defines[k] = v
29→ }
30→ }
31→}
32→
33→// Preprocessor performs textual preprocessing of occam source.
34→type Preprocessor struct {
35→ defines map[string]string
36→ includePaths []string
37→ errors []string
38→ processing map[string]bool // absolute paths currently being processed (circular include detection)
39→}
40→
41→// New creates a new Preprocessor with the given options.
42→func New(opts ...Option) *Preprocessor {
43→ pp := &Preprocessor{
44→ defines: map[string]string{},
45→ processing: map[string]bool{},
46→ }
47→ // Predefined symbols
48→ pp.defines["TARGET.BITS.PER.WORD"] = "64"
49→
50→ for _, opt := range opts {
51→ opt(pp)
52→ }
53→ return pp
54→}
55→
56→// Errors returns any errors accumulated during processing.
57→func (pp *Preprocessor) Errors() []string {
58→ return pp.errors
59→}
60→
61→// ProcessFile reads and processes a file, resolving #INCLUDE directives.
62→func (pp *Preprocessor) ProcessFile(filename string) (string, error) {
63→ absPath, err := filepath.Abs(filename)
64→ if err != nil {
65→ return "", fmt.Errorf("cannot resolve path %q: %w", filename, err)
66→ }
67→
68→ if pp.processing[absPath] {
69→ return "", fmt.Errorf("circular include detected: %s", filename)
70→ }
71→ pp.processing[absPath] = true
72→ defer delete(pp.processing, absPath)
73→
74→ data, err := os.ReadFile(absPath)
75→ if err != nil {
76→ return "", fmt.Errorf("cannot read %q: %w", filename, err)
77→ }
78→
79→ return pp.processSource(string(data), filepath.Dir(absPath))
80→}
81→
82→// ProcessSource processes occam source text with no file context.
83→// #INCLUDE directives will only resolve against includePaths.
84→func (pp *Preprocessor) ProcessSource(source string) (string, error) {
85→ return pp.processSource(source, "")
86→}
87→
88→// processSource performs line-by-line preprocessing.
89→// baseDir is the directory of the current file (for relative #INCLUDE resolution).
90→func (pp *Preprocessor) processSource(source string, baseDir string) (string, error) {
91→ lines := strings.Split(source, "\n")
92→ var out strings.Builder
93→ var condStack []condState
94→
95→ for i, line := range lines {
96→ if i > 0 {
97→ out.WriteByte('\n')
98→ }
99→
100→ trimmed := strings.TrimSpace(line)
101→
102→ if strings.HasPrefix(trimmed, "#") {
103→ directive, rest := parseDirectiveLine(trimmed)
104→
105→ switch directive {
106→ case "DEFINE":
107→ if isActive(condStack) {
108→ sym := strings.TrimSpace(rest)
109→ if sym != "" {
110→ pp.defines[sym] = ""
111→ }
112→ }
113→ out.WriteString("") // blank line preserves line numbers
114→
115→ case "IF":
116→ val := pp.evalExpr(rest)
117→ condStack = append(condStack, condState{active: val, seenTrue: val})
118→ out.WriteString("")
119→
120→ case "ELSE":
121→ if len(condStack) == 0 {
122→ pp.errors = append(pp.errors, fmt.Sprintf("line %d: #ELSE without matching #IF", i+1))
123→ } else {
124→ top := &condStack[len(condStack)-1]
125→ if top.seenTrue {
126→ top.active = false
127→ } else {
128→ top.active = true
129→ top.seenTrue = true
130→ }
131→ }
132→ out.WriteString("")
133→
134→ case "ENDIF":
135→ if len(condStack) == 0 {
136→ pp.errors = append(pp.errors, fmt.Sprintf("line %d: #ENDIF without matching #IF", i+1))
137→ } else {
138→ condStack = condStack[:len(condStack)-1]
139→ }
140→ out.WriteString("")
141→
142→ case "INCLUDE":
143→ if isActive(condStack) {
144→ included, err := pp.resolveAndInclude(rest, baseDir)
145→ if err != nil {
146→ return "", fmt.Errorf("line %d: %w", i+1, err)
147→ }
148→ out.WriteString(included)
149→ } else {
150→ out.WriteString("")
151→ }
152→
153→ case "COMMENT", "PRAGMA", "USE":
154→ out.WriteString("") // no-op, blank line
155→
156→ default:
157→ // Unknown directive — pass through if active
158→ if isActive(condStack) {
159→ out.WriteString(line)
160→ } else {
161→ out.WriteString("")
162→ }
163→ }
164→ } else {
165→ if isActive(condStack) {
166→ out.WriteString(line)
167→ } else {
168→ out.WriteString("") // blank line preserves line numbers
169→ }
170→ }
171→ }
172→
173→ if len(condStack) > 0 {
174→ pp.errors = append(pp.errors, fmt.Sprintf("unterminated #IF (missing %d #ENDIF)", len(condStack)))
175→ }
176→
177→ return out.String(), nil
178→}
179→
180→// condState tracks one level of #IF/#ELSE nesting.
181→type condState struct {
182→ active bool // currently emitting lines?
183→ seenTrue bool // has any branch been true?
184→}
185→
186→// isActive returns true if all condition stack levels are active.
187→func isActive(stack []condState) bool {
188→ for _, s := range stack {
189→ if !s.active {
190→ return false
191→ }
192→ }
193→ return true
194→}
195→
196→// parseDirectiveLine splits "#DIRECTIVE rest" into (directive, rest).
197→func parseDirectiveLine(trimmed string) (string, string) {
198→ // trimmed starts with "#"
199→ s := trimmed[1:] // skip '#'
200→ s = strings.TrimSpace(s)
201→
202→ idx := strings.IndexAny(s, " \t")
203→ if idx == -1 {
204→ return strings.ToUpper(s), ""
205→ }
206→ return strings.ToUpper(s[:idx]), strings.TrimSpace(s[idx+1:])
207→}
208→
209→// resolveAndInclude resolves an #INCLUDE filename and processes the included file.
210→func (pp *Preprocessor) resolveAndInclude(rest string, baseDir string) (string, error) {
211→ filename := stripQuotes(rest)
212→ if filename == "" {
213→ return "", fmt.Errorf("#INCLUDE with empty filename")
214→ }
215→
216→ // Try to find the file
217→ resolved := pp.resolveIncludePath(filename, baseDir)
218→ if resolved == "" {
219→ return "", fmt.Errorf("cannot find included file %q", filename)
220→ }
221→
222→ return pp.ProcessFile(resolved)
223→}
224→
225→// resolveIncludePath searches for a file: first relative to baseDir, then in includePaths.
226→func (pp *Preprocessor) resolveIncludePath(filename string, baseDir string) string {
227→ // First: relative to current file's directory
228→ if baseDir != "" {
229→ candidate := filepath.Join(baseDir, filename)
230→ if _, err := os.Stat(candidate); err == nil {
231→ return candidate
232→ }
233→ }
234→
235→ // Then: each include path
236→ for _, dir := range pp.includePaths {
237→ candidate := filepath.Join(dir, filename)
238→ if _, err := os.Stat(candidate); err == nil {
239→ return candidate
240→ }
241→ }
242→
243→ return ""
244→}
245→
246→// stripQuotes removes surrounding double quotes from a string.
247→func stripQuotes(s string) string {
248→ s = strings.TrimSpace(s)
249→ if len(s) >= 2 && s[0] == '"' && s[len(s)-1] == '"' {
250→ return s[1 : len(s)-1]
251→ }
252→ return s
253→}
254→
255→// evalExpr evaluates a preprocessor conditional expression.
256→// Supports: TRUE, FALSE, DEFINED (SYMBOL), NOT (expr), (SYMBOL = value)
257→func (pp *Preprocessor) evalExpr(expr string) bool {
258→ expr = strings.TrimSpace(expr)
259→ if expr == "" {
260→ return false
261→ }
262→
263→ // TRUE / FALSE
264→ if expr == "TRUE" {
265→ return true
266→ }
267→ if expr == "FALSE" {
268→ return false
269→ }
270→
271→ // NOT (expr) or NOT DEFINED (...)
272→ if strings.HasPrefix(expr, "NOT ") || strings.HasPrefix(expr, "NOT(") {
273→ inner := strings.TrimPrefix(expr, "NOT")
274→ inner = strings.TrimSpace(inner)
275→ return !pp.evalExpr(inner)
276→ }
277→
278→ // DEFINED (SYMBOL)
279→ if strings.HasPrefix(expr, "DEFINED") {
280→ inner := strings.TrimPrefix(expr, "DEFINED")
281→ inner = strings.TrimSpace(inner)
282→ sym := stripParens(inner)
283→ _, ok := pp.defines[sym]
284→ return ok
285→ }
286→
287→ // Parenthesized expression
288→ if strings.HasPrefix(expr, "(") && strings.HasSuffix(expr, ")") {
289→ inner := expr[1 : len(expr)-1]
290→ inner = strings.TrimSpace(inner)
291→
292→ // Check for equality: SYMBOL = value
293→ if eqIdx := strings.Index(inner, "="); eqIdx >= 0 {
294→ lhs := strings.TrimSpace(inner[:eqIdx])
295→ rhs := strings.TrimSpace(inner[eqIdx+1:])
296→ lhsVal, ok := pp.defines[lhs]
297→ if !ok {
298→ return false
299→ }
300→ return lhsVal == rhs
301→ }
302→
303→ // Otherwise recurse
304→ return pp.evalExpr(inner)
305→ }
306→
307→ // Bare symbol — treat as DEFINED
308→ _, ok := pp.defines[expr]
309→ return ok
310→}
311→
312→// stripParens removes surrounding parentheses and whitespace.
313→func stripParens(s string) string {
314→ s = strings.TrimSpace(s)
315→ if len(s) >= 2 && s[0] == '(' && s[len(s)-1] == ')' {
316→ return strings.TrimSpace(s[1 : len(s)-1])
317→ }
318→ return s
319→}
320→Now let me understand the issue. The circular include detection uses pp.processing which tracks files currently being processed. But it uses defer delete(pp.processing, absPath) — so once a file is done processing, it's removed from the map. This means the same file can be included multiple times, just not recursively.
But wait — the course.module has include guards:
#IF NOT (DEFINED (COURSE.MODULE))
#DEFINE COURSE.MODULE
#INCLUDE "consts.inc"
#INCLUDE "utils.occ"
#INCLUDE "string.occ"
...
#ENDIF
So the first time course.module is included, it defines COURSE.MODULE and includes all files. If it's included again, the #IF NOT (DEFINED (COURSE.MODULE)) would be false and nothing would happen.
But the issue could be something else. When the demo program includes course.module, the preprocessor finds it via the -I kroc/modules/course/libsrc include path, then course.module includes files like utils.occ and string.occ from the same directory.
Now I'm wondering if some of these included files have their own #INCLUDE directives that could create a cycle or cause issues. Looking at what I know: string.occ has a #USE directive which is a no-op, and utils.occ includes consts.inc which should work fine. But maybe one of these files is including course.module again, creating a circular dependency? Or perhaps the preprocessor is generating such a large output that it's causing the memory issue. Let me check the file sizes to see if that could be the culprit.
utils.occ: 1132 lines
string.occ: 303 lines
demo_cycles.occ: ~100 lines
demo_nets.occ: ~100 lines
random.occ: 87 lines
consts.inc: 56 lines
That's only around 2000-3000 lines total, which shouldn't trigger an OOM error. The real problem might be in the lexer or parser instead. Actually, I'm noticing that consts.inc gets included both by course.module and by utils.occ, and it doesn't have include guards itself. The circular include detection in the preprocessor might not be catching this properly.
The preprocessor uses defer delete to remove files from the processing set after they're done, which means consts.inc could get included again later even though it's already been processed. That would cause double-inclusion, which is messy but shouldn't cause OOM on its own.
Let me trace through all the files in libsrc to see if there's some kind of exponential blowup happening from the way they're including each other.
The circular include detection uses defer delete — so a file is removed from the processing set after it finishes, allowing it to be included again by a different file. Let me check if there's an exponential blowup in the include graph.
{
"pattern": "#INCLUDE",
"path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc",
"output_mode": "content"
}kroc/modules/course/libsrc/consts.inc:33:-- #INCLUDE "course.module"] kroc/modules/course/libsrc/demo_nets.occ:25:-- #INCLUDE "course.module"] kroc/modules/course/libsrc/string.occ:23:#INCLUDE "consts.inc" kroc/modules/course/libsrc/shared_screen.occ:26:-- #INCLUDE "shared_screen.module"] kroc/modules/course/libsrc/shared_screen.occ:30:#INCLUDE "consts.inc" kroc/modules/course/libsrc/shared_screen.occ:31:#INCLUDE "shared_screen.inc" kroc/modules/course/libsrc/demo_cycles.occ:25:-- #INCLUDE "course.module"] kroc/modules/course/libsrc/file_in.occ:22:#INCLUDE "consts.inc" kroc/modules/course/libsrc/utils.occ:24:#INCLUDE "consts.inc"
{
"pattern": "#INCLUDE",
"path": "/home/david/projects/code-associates/occam2go/kroc/demos/complexity/concurrent_hello_1.occ",
"output_mode": "content"
}OK so the include graph is:
concurrent_hello_1.occ includes: - course.module
course.module includes: - consts.inc - utils.occ (which includes consts.inc) - string.occ (which includes consts.inc) - demo_cycles.occ (no includes) - demo_nets.occ (no includes) - file_in.occ (which includes consts.inc) - float_io.occ (need to check) - random.occ (need to check)
So there's no circular inclusion here — it's a DAG.
The -- comments with #INCLUDE "course.module" are just documentation, not actual directives, so the preprocessor should handle this fine. The problem is likely downstream in the lexer or parser. Let me test the preprocessor output directly to see if it's producing the expected result or if the issue is further down the pipeline.
Actually, I'm wondering if there's an infinite loop in the lexer or parser when processing certain constructs. Looking at what might cause problems: #PRAGMA DEFINED gets treated as a no-op, comments should be handled normally, but some unrecognized constructs could trigger a loop. Let me trace through the include logic more carefully — when a file like consts.inc gets included multiple times, the preprocessor recursively processes it and appends the result to the output, which could potentially cause issues if there's circular inclusion or repeated processing.
Let me check the actual output size by running just the preprocessor, and then examine what float_io.occ and file_in.occ are pulling in.
The include graph looks like a DAG (no cycles) — the -- #INCLUDE "course.module" entries are just comments. Let me check if the issue is in the preprocessor output size, or if it's in the lexer/parser. Let me try isolating the preprocessor step:
{
"pattern": "#INCLUDE",
"path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/float_io.occ",
"output_mode": "content"
}{
"pattern": "#INCLUDE",
"path": "/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/random.occ",
"output_mode": "content"
}Good — no further includes. The include graph is finite with no cycles. The problem must be downstream. Let me write a quick test to isolate the preprocessing step vs lexing/parsing:
cat > /tmp/test_preproc.go << 'EOF'
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
fmt.Printf("Preprocessed output: %d lines, %d bytes\n", len(lines), len(result))
// Print first 20 non-blank lines
count := 0
for i, line := range lines {
if strings.TrimSpace(line) != "" {
fmt.Printf(" line %d: %s\n", i+1, line)
count++
if count >= 20 {
break
}
}
}
}
EOF
go run /tmp/test_preproc.go 2>&1Preprocessed output: 4567 lines, 80352 bytes
line 1: -- ==================================================================================
line 2: -- WHAT CONCURRENT_HELLO DOES
line 3: --
line 4: -- This program has two 'writer' processes and one 'output.collector' process. The writers each have a bespoke channel for communication with
line 5: -- output.collector. Each writer sends a single digit (int) down it's channel to output.collector. Output.collector receives data from each
line 6: -- writer, and prints that digit to the terminal. Everything terminates after each writer has passed 1 message to output.collector.
line 7: --
line 8: -- In this architecture (and those that follow in other programms) the writers act as clients to output.collector, which is a server prividing
line 9: -- a print-to-the-terminal service.
line 10: --
line 11: -- This program should be compiled with "kroc -d -di concurrent_hello_1.occ"
line 12: -- and then run with ./concurrent_hello_1
line 13: -- ==================================================================================
line 16: --{{{ this module contains utilities for printint useful types (like strings)
line 17: -- (like strings) to a channel (which can be linked to the terminal screen)
line 20: --
line 21: -- Constants
line 22: -- Copyright (C) 1984 P.H. Welch
line 23: --
line 24: -- This library is free software; you can redistribute it and/orThe preprocessor works fine — 4567 lines, 80KB. Not a problem. The issue is in the lexer or parser. Let me test the lexer:
cat > /tmp/test_lexer.go << 'EOF'
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Preprocessed: %d lines\n", len(strings.Split(result, "\n")))
l := lexer.New(result)
count := 0
for {
tok := l.NextToken()
count++
if tok.Type == lexer.EOF {
break
}
if count > 500000 {
fmt.Printf("ABORT: too many tokens (>500000), last token: %+v\n", tok)
os.Exit(1)
}
}
fmt.Printf("Lexed: %d tokens\n", count)
}
EOF
go run /tmp/test_lexer.go 2>&1cat > /tmp/test_parser.go << 'EOF'
package main
import (
"fmt"
"os"
"strings"
"runtime"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
// Limit memory to 512MB to avoid OOM killing the system
var m runtime.MemStats
runtime.ReadMemStats(&m)
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
fmt.Printf("Preprocessed: %d lines\n", len(strings.Split(result, "\n")))
l := lexer.New(result)
p := parser.New(l)
fmt.Println("Starting parse...")
program := p.ParseProgram()
if len(p.Errors()) > 0 {
fmt.Printf("Parse errors (%d):\n", len(p.Errors()))
for i, e := range p.Errors() {
fmt.Printf(" %d: %s\n", i+1, e)
if i >= 20 {
fmt.Println(" ... (truncated)")
break
}
}
}
fmt.Printf("Parsed: %d top-level statements\n", len(program.Statements))
runtime.ReadMemStats(&m)
fmt.Printf("Memory: alloc=%dMB, sys=%dMB\n", m.Alloc/1024/1024, m.Sys/1024/1024)
}
EOF
# Set memory limit and timeout
ulimit -v 1048576 2>/dev/null # 1GB virtual memory limit
timeout 10 go run /tmp/test_parser.go 2>&1Exit code 1
Preprocessed: 4567 lines
Starting parse...
runtime: out of memory: cannot allocate 71303168-byte block (271777792 in use)
fatal error: out of memory
goroutine 1 gp=0x3f97ddef41e0 m=9 mp=0x3f97de001808 [running]:
runtime.throw({0x4f975b?, 0x2054?})
/usr/local/go/src/runtime/panic.go:1229 +0x48 fp=0x3f97de0122c8 sp=0x3f97de012298 pc=0x47b708
runtime.(*mcache).allocLarge(0x4e55a0?, 0x40a8000, 0x0)
/usr/local/go/src/runtime/mcache.go:259 +0x18d fp=0x3f97de012318 sp=0x3f97de0122c8 pc=0x42156d
runtime.mallocgcLarge(0x4d6820?, 0x4e55a0, 0x1)
/usr/local/go/src/runtime/malloc.go:1755 +0x79 fp=0x3f97de012370 sp=0x3f97de012318 pc=0x41e699
runtime.mallocgc(0x40a8000, 0x4e55a0, 0x1)
/usr/local/go/src/runtime/malloc.go:1208 +0xd3 fp=0x3f97de0123a0 sp=0x3f97de012370 pc=0x47a213
runtime.growslice(0x3f97e5432000, 0x1?, 0x4f80b5?, 0x2?, 0x4e55a0)
/usr/local/go/src/runtime/slice.go:273 +0x507 fp=0x3f97de012410 sp=0x3f97de0123a0 pc=0x47d447
github.com/codeassociates/occam2go/parser.(*Parser).parseIfStatement(0x3f97de026140)
/home/david/projects/code-associates/occam2go/parser/parser.go:2236 +0x53d fp=0x3f97de0124d0 sp=0x3f97de012410 pc=0x4c56bd
github.com/codeassociates/occam2go/parser.(*Parser).parseStatement(0x3f97de026140)
/home/david/projects/code-associates/occam2go/parser/parser.go:198 +0x20e fp=0x3f97de012588 sp=0x3f97de0124d0 pc=0x4ba54e
github.com/codeassociates/occam2go/parser.(*Parser).parseProcDecl(0x3f97de026140)
/home/david/projects/code-associates/occam2go/parser/parser.go:1758 +0x34f fp=0x3f97de012630 sp=0x3f97de012588 pc=0x4c2b0f
github.com/codeassociates/occam2go/parser.(*Parser).parseStatement(0x3f97de026140)
/home/david/projects/code-associates/occam2go/parser/parser.go:194 +0x256 fp=0x3f97de0126e8 sp=0x3f97de012630 pc=0x4ba596
github.com/codeassociates/occam2go/parser.(*Parser).ParseProgram(0x3f97de026140)
/home/david/projects/code-associates/occam2go/parser/parser.go:147 +0x65 fp=0x3f97de012750 sp=0x3f97de0126e8 pc=0x4ba265
main.main()
/tmp/test_parser.go:33 +0x2de fp=0x3f97de013f48 sp=0x3f97de012750 pc=0x4c9afe
runtime.main()
/usr/local/go/src/runtime/proc.go:290 +0x2d5 fp=0x3f97de013fe0 sp=0x3f97de013f48 pc=0x44b095
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de013fe8 sp=0x3f97de013fe0 pc=0x481661
goroutine 2 gp=0x3f97ddef4780 m=nil [force gc (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97ddf92fa8 sp=0x3f97ddf92f88 pc=0x47b82e
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:468
runtime.forcegchelper()
/usr/local/go/src/runtime/proc.go:375 +0xb3 fp=0x3f97ddf92fe0 sp=0x3f97ddf92fa8 pc=0x44b3b3
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97ddf92fe8 sp=0x3f97ddf92fe0 pc=0x481661
created by runtime.init.7 in goroutine 1
/usr/local/go/src/runtime/proc.go:363 +0x1a
goroutine 3 gp=0x3f97ddef4d20 m=nil [GC sweep wait]:
runtime.gopark(0x1?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97ddf93788 sp=0x3f97ddf93768 pc=0x47b82e
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:468
runtime.bgsweep(0x3f97ddfb2000)
/usr/local/go/src/runtime/mgcsweep.go:324 +0x151 fp=0x3f97ddf937c8 sp=0x3f97ddf93788 pc=0x435a71
runtime.gcenable.gowrap1()
/usr/local/go/src/runtime/mgc.go:214 +0x17 fp=0x3f97ddf937e0 sp=0x3f97ddf937c8 pc=0x427017
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97ddf937e8 sp=0x3f97ddf937e0 pc=0x481661
created by runtime.gcenable in goroutine 1
/usr/local/go/src/runtime/mgc.go:214 +0x66
goroutine 4 gp=0x3f97ddef4f00 m=nil [GC scavenge wait]:
runtime.gopark(0x54381?, 0x3b9aca00?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97ddf93f78 sp=0x3f97ddf93f58 pc=0x47b82e
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:468
runtime.(*scavengerState).park(0x5d8f80)
/usr/local/go/src/runtime/mgcscavenge.go:425 +0x49 fp=0x3f97ddf93fa8 sp=0x3f97ddf93f78 pc=0x4334e9
runtime.bgscavenge(0x3f97ddfb2000)
/usr/local/go/src/runtime/mgcscavenge.go:658 +0x59 fp=0x3f97ddf93fc8 sp=0x3f97ddf93fa8 pc=0x433a79
runtime.gcenable.gowrap2()
/usr/local/go/src/runtime/mgc.go:215 +0x17 fp=0x3f97ddf93fe0 sp=0x3f97ddf93fc8 pc=0x426fd7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97ddf93fe8 sp=0x3f97ddf93fe0 pc=0x481661
created by runtime.gcenable in goroutine 1
/usr/local/go/src/runtime/mgc.go:215 +0xa5
goroutine 17 gp=0x3f97de0043c0 m=nil [GOMAXPROCS updater (idle)]:
runtime.gopark(0x0?, 0x0?, 0x0?, 0x0?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97ddf8e788 sp=0x3f97ddf8e768 pc=0x47b82e
runtime.goparkunlock(...)
/usr/local/go/src/runtime/proc.go:468
runtime.updateMaxProcsGoroutine()
/usr/local/go/src/runtime/proc.go:7095 +0xe7 fp=0x3f97ddf8e7e0 sp=0x3f97ddf8e788 pc=0x4597a7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97ddf8e7e8 sp=0x3f97ddf8e7e0 pc=0x481661
created by runtime.defaultGOMAX
... [39382 characters truncated] ...
o/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de39dfe8 sp=0x3f97de39dfe0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
goroutine 40 gp=0x3f97de2fc780 m=nil [GC worker (idle)]:
runtime.gopark(0x3f07144d34?, 0x3?, 0x8f?, 0x2?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97de39e740 sp=0x3f97de39e720 pc=0x47b82e
runtime.gcBgMarkWorker(0x3f97de01a070)
/usr/local/go/src/runtime/mgc.go:1791 +0xeb fp=0x3f97de39e7c8 sp=0x3f97de39e740 pc=0x4298eb
runtime.gcBgMarkStartWorkers.gowrap1()
/usr/local/go/src/runtime/mgc.go:1695 +0x17 fp=0x3f97de39e7e0 sp=0x3f97de39e7c8 pc=0x4297d7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de39e7e8 sp=0x3f97de39e7e0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
goroutine 41 gp=0x3f97de2fc960 m=nil [GC worker (idle)]:
runtime.gopark(0x3f071b9bac?, 0x1?, 0x29?, 0x1?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97de39ef40 sp=0x3f97de39ef20 pc=0x47b82e
runtime.gcBgMarkWorker(0x3f97de01a070)
/usr/local/go/src/runtime/mgc.go:1791 +0xeb fp=0x3f97de39efc8 sp=0x3f97de39ef40 pc=0x4298eb
runtime.gcBgMarkStartWorkers.gowrap1()
/usr/local/go/src/runtime/mgc.go:1695 +0x17 fp=0x3f97de39efe0 sp=0x3f97de39efc8 pc=0x4297d7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de39efe8 sp=0x3f97de39efe0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
goroutine 42 gp=0x3f97de2fcb40 m=nil [GC worker (idle)]:
runtime.gopark(0x5f9a00?, 0x1?, 0x63?, 0xa3?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97de39f740 sp=0x3f97de39f720 pc=0x47b82e
runtime.gcBgMarkWorker(0x3f97de01a070)
/usr/local/go/src/runtime/mgc.go:1791 +0xeb fp=0x3f97de39f7c8 sp=0x3f97de39f740 pc=0x4298eb
runtime.gcBgMarkStartWorkers.gowrap1()
/usr/local/go/src/runtime/mgc.go:1695 +0x17 fp=0x3f97de39f7e0 sp=0x3f97de39f7c8 pc=0x4297d7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de39f7e8 sp=0x3f97de39f7e0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
goroutine 43 gp=0x3f97de2fcd20 m=nil [GC worker (idle)]:
runtime.gopark(0x5f9a00?, 0x1?, 0x13?, 0x69?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97de3a7f40 sp=0x3f97de3a7f20 pc=0x47b82e
runtime.gcBgMarkWorker(0x3f97de01a070)
/usr/local/go/src/runtime/mgc.go:1791 +0xeb fp=0x3f97de3a7fc8 sp=0x3f97de3a7f40 pc=0x4298eb
runtime.gcBgMarkStartWorkers.gowrap1()
/usr/local/go/src/runtime/mgc.go:1695 +0x17 fp=0x3f97de3a7fe0 sp=0x3f97de3a7fc8 pc=0x4297d7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de3a7fe8 sp=0x3f97de3a7fe0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
goroutine 44 gp=0x3f97de2fcf00 m=nil [GC worker (idle)]:
runtime.gopark(0x5f9a00?, 0x1?, 0xc8?, 0x26?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97de3a8740 sp=0x3f97de3a8720 pc=0x47b82e
runtime.gcBgMarkWorker(0x3f97de01a070)
/usr/local/go/src/runtime/mgc.go:1791 +0xeb fp=0x3f97de3a87c8 sp=0x3f97de3a8740 pc=0x4298eb
runtime.gcBgMarkStartWorkers.gowrap1()
/usr/local/go/src/runtime/mgc.go:1695 +0x17 fp=0x3f97de3a87e0 sp=0x3f97de3a87c8 pc=0x4297d7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de3a87e8 sp=0x3f97de3a87e0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
goroutine 45 gp=0x3f97de2fd0e0 m=nil [GC worker (idle)]:
runtime.gopark(0x3f07249f5f?, 0x1?, 0x81?, 0xc0?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97de3a8f40 sp=0x3f97de3a8f20 pc=0x47b82e
runtime.gcBgMarkWorker(0x3f97de01a070)
/usr/local/go/src/runtime/mgc.go:1791 +0xeb fp=0x3f97de3a8fc8 sp=0x3f97de3a8f40 pc=0x4298eb
runtime.gcBgMarkStartWorkers.gowrap1()
/usr/local/go/src/runtime/mgc.go:1695 +0x17 fp=0x3f97de3a8fe0 sp=0x3f97de3a8fc8 pc=0x4297d7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de3a8fe8 sp=0x3f97de3a8fe0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
goroutine 46 gp=0x3f97de2fd2c0 m=nil [GC worker (idle)]:
runtime.gopark(0x3f071432b0?, 0x1?, 0xaa?, 0x13?, 0x0?)
/usr/local/go/src/runtime/proc.go:462 +0xce fp=0x3f97de3a9740 sp=0x3f97de3a9720 pc=0x47b82e
runtime.gcBgMarkWorker(0x3f97de01a070)
/usr/local/go/src/runtime/mgc.go:1791 +0xeb fp=0x3f97de3a97c8 sp=0x3f97de3a9740 pc=0x4298eb
runtime.gcBgMarkStartWorkers.gowrap1()
/usr/local/go/src/runtime/mgc.go:1695 +0x17 fp=0x3f97de3a97e0 sp=0x3f97de3a97c8 pc=0x4297d7
runtime.goexit({})
/usr/local/go/src/runtime/asm_amd64.s:1771 +0x1 fp=0x3f97de3a97e8 sp=0x3f97de3a97e0 pc=0x481661
created by runtime.gcBgMarkStartWorkers in goroutine 1
/usr/local/go/src/runtime/mgc.go:1695 +0x105
exit status 2The OOM is in parseIfStatement at line 2236 of parser.go. The stack trace shows:
parser.(*Parser).parseIfStatement(0x3f97de026140)
parser.go:2236 +0x53d
parser.(*Parser).parseStatement(0x3f97de026140)
parser.go:198 +0x20e
parser.(*Parser).parseProcDecl(0x3f97de026140)
parser.go:1758 +0x34f
parser.(*Parser).parseStatement(0x3f97de026140)
parser.go:194 +0x256
parser.(*Parser).ParseProgram(0x3f97de026140)
parser.go:147 +0x65
The issue is a growslice call at line 2236, which means parseIfStatement is appending to a slice in an infinite loop without ever terminating. I need to examine that code section to find where the unbounded growth is happening.
Found it. The OOM is in parseIfStatement at parser.go:2236 — it's growing a slice unboundedly. Let me look at that code.
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 2200,
"limit": 100
} 2200→ }
2201→ p.nextToken()
2202→ }
2203→
2204→ // Skip any more newlines after DEDENT
2205→ for p.curTokenIs(lexer.NEWLINE) {
2206→ p.nextToken()
2207→ }
2208→
2209→ if p.curTokenIs(lexer.EOF) {
2210→ break
2211→ }
2212→
2213→ if p.indentLevel < startLevel {
2214→ break
2215→ }
2216→
2217→ choice := ast.IfChoice{}
2218→ choice.Condition = p.parseExpression(LOWEST)
2219→
2220→ // Skip newlines and expect INDENT for body
2221→ for p.peekTokenIs(lexer.NEWLINE) {
2222→ p.nextToken()
2223→ }
2224→
2225→ if p.peekTokenIs(lexer.INDENT) {
2226→ p.nextToken() // consume INDENT
2227→ p.nextToken() // move to body
2228→ choice.Body = p.parseStatement()
2229→
2230→ // Advance past the last token of the statement if needed
2231→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
2232→ p.nextToken()
2233→ }
2234→ }
2235→
2236→ stmt.Choices = append(stmt.Choices, choice)
2237→ }
2238→
2239→ return stmt
2240→}
2241→
2242→func (p *Parser) parseCaseStatement() *ast.CaseStatement {
2243→ stmt := &ast.CaseStatement{Token: p.curToken}
2244→
2245→ // Parse selector expression on the same line
2246→ p.nextToken()
2247→ stmt.Selector = p.parseExpression(LOWEST)
2248→
2249→ // Skip to next line
2250→ for p.peekTokenIs(lexer.NEWLINE) {
2251→ p.nextToken()
2252→ }
2253→
2254→ // Expect INDENT
2255→ if !p.peekTokenIs(lexer.INDENT) {
2256→ p.addError("expected indented block after CASE")
2257→ return stmt
2258→ }
2259→ p.nextToken() // consume INDENT
2260→ startLevel := p.indentLevel
2261→ p.nextToken() // move into block
2262→
2263→ // Parse case choices
2264→ for !p.curTokenIs(lexer.EOF) {
2265→ // Skip newlines
2266→ for p.curTokenIs(lexer.NEWLINE) {
2267→ p.nextToken()
2268→ }
2269→
2270→ // Handle DEDENT tokens
2271→ for p.curTokenIs(lexer.DEDENT) {
2272→ if p.indentLevel < startLevel {
2273→ return stmt
2274→ }
2275→ p.nextToken()
2276→ }
2277→
2278→ // Skip any more newlines after DEDENT
2279→ for p.curTokenIs(lexer.NEWLINE) {
2280→ p.nextToken()
2281→ }
2282→
2283→ if p.curTokenIs(lexer.EOF) {
2284→ break
2285→ }
2286→
2287→ if p.indentLevel < startLevel {
2288→ break
2289→ }
2290→
2291→ choice := ast.CaseChoice{}
2292→
2293→ if p.curTokenIs(lexer.ELSE) {
2294→ choice.IsElse = true
2295→ } else {
2296→ // Parse value expression
2297→ choice.Values = append(choice.Values, p.parseExpression(LOWEST))
2298→ }
2299→{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 2160,
"limit": 50
} 2160→
2161→func (p *Parser) parseIfStatement() *ast.IfStatement {
2162→ stmt := &ast.IfStatement{Token: p.curToken}
2163→
2164→ // Check for replicator: IF i = start FOR count
2165→ if p.peekTokenIs(lexer.IDENT) {
2166→ p.nextToken() // move to identifier
2167→ if p.peekTokenIs(lexer.EQ) {
2168→ stmt.Replicator = p.parseReplicator()
2169→ } else {
2170→ p.addError("unexpected identifier after IF")
2171→ return stmt
2172→ }
2173→ }
2174→
2175→ // Skip to next line
2176→ for p.peekTokenIs(lexer.NEWLINE) {
2177→ p.nextToken()
2178→ }
2179→
2180→ // Expect INDENT
2181→ if !p.peekTokenIs(lexer.INDENT) {
2182→ p.addError("expected indented block after IF")
2183→ return stmt
2184→ }
2185→ p.nextToken() // consume INDENT
2186→ startLevel := p.indentLevel
2187→ p.nextToken() // move into block
2188→
2189→ // Parse if choices (condition -> body pairs)
2190→ for !p.curTokenIs(lexer.EOF) {
2191→ // Skip newlines
2192→ for p.curTokenIs(lexer.NEWLINE) {
2193→ p.nextToken()
2194→ }
2195→
2196→ // Handle DEDENT tokens
2197→ for p.curTokenIs(lexer.DEDENT) {
2198→ if p.indentLevel < startLevel {
2199→ return stmt
2200→ }
2201→ p.nextToken()
2202→ }
2203→
2204→ // Skip any more newlines after DEDENT
2205→ for p.curTokenIs(lexer.NEWLINE) {
2206→ p.nextToken()
2207→ }
2208→
2209→ if p.curTokenIs(lexer.EOF) {Let me trace through what happens when the parser encounters a construct that it doesn't understand in the condition of an IF statement.
The issue is at line 2218: choice.Condition = p.parseExpression(LOWEST). If parseExpression fails to advance the token position (returns an expression but doesn't consume any tokens, or the expression parser encounters a token it doesn't understand), the loop will spin forever, appending empty/failed choices.
The key question is: what construct in the course module triggers this? Let me look at what's in utils.occ's IF statements.
Actually, let me think more carefully. The loop is:
If parseExpression at step 6 encounters a token it can't parse, it might return without advancing. Then at step 7, the peekTokenIs(INDENT) check might fail (no INDENT found), so choice.Body stays nil. Then at step 8 it appends. Then back to step 1 - the same token is still current (since nothing advanced), and the cycle repeats indefinitely with the same unparseable token.
The real issue is that parseExpression hits an unrecognized token and bails out without consuming it, trapping the IF parser in an infinite loop. I need to find what specific construct in the course library triggers this—probably something in utils.occ like a bare TRUE guard condition that the expression parser doesn't handle yet.
Rather than keep guessing, I should add debugging output to pinpoint exactly where the parser gets stuck.
The infinite loop is clear: in the for loop starting at line 2190, if parseExpression(LOWEST) on line 2218 fails to consume any tokens, the loop runs forever appending empty choices. Let me find exactly what construct triggers this:
cat > /tmp/test_debug_parser.go << 'EOF'
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
// Try progressively larger chunks to find where parsing blows up
for _, nLines := range []int{50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200, 1500, 2000, 2500, 3000, 3500, 4000, 4567} {
if nLines > len(lines) {
nLines = len(lines)
}
subset := strings.Join(lines[:nLines], "\n")
l := lexer.New(subset)
p := parser.New(l)
program := p.ParseProgram()
errs := p.Errors()
status := "OK"
if len(errs) > 0 {
status = fmt.Sprintf("ERRORS(%d): %s", len(errs), errs[0])
}
fmt.Printf("Lines %4d: %d stmts, %s\n", nLines, len(program.Statements), status)
// If we hit too many errors, this is probably where the problem starts
if len(errs) > 5 {
fmt.Println("\nFirst 10 errors:")
for i, e := range errs {
if i >= 10 { break }
fmt.Printf(" %s\n", e)
}
// Show context around the error line
for _, e := range errs[:1] {
// Try to extract line number
var lineNum int
fmt.Sscanf(e, "line %d:", &lineNum)
if lineNum > 0 && lineNum <= len(lines) {
fmt.Printf("\nContext around line %d:\n", lineNum)
start := lineNum - 3
if start < 0 { start = 0 }
end := lineNum + 5
if end > len(lines) { end = len(lines) }
for i := start; i < end; i++ {
marker := " "
if i+1 == lineNum { marker = ">" }
fmt.Printf(" %s %4d: %s\n", marker, i+1, lines[i])
}
}
}
break
}
}
}
EOF
timeout 30 go run /tmp/test_debug_parser.go 2>&1Exit code 124 Lines 50: 0 stmts, OK Lines 100: 10 stmts, OK Lines 200: 23 stmts, ERRORS(4): line 188: expected ), got ! Lines 50: 0 stmts, OK Lines 100: 10 stmts, OK Lines 200: 23 stmts, ERRORS(4): line 188: expected ), got !
The program timed out at 300 lines. Let me try narrowing down between 200 and 300 lines. But first, let me check what's at line 188 of the preprocessed output - the first error. And also try to narrow to when the parsing goes infinite.
Wait, actually the program timed out. It printed lines 50, 100, 200 then repeated. That's odd. Let me think about why...
Oh, it's probably because parser.New(l) and ParseProgram() with 300 lines causes the infinite loop. The timeout kicked in after the 300-line attempt hung.
Now I need to pinpoint what's between lines 200 and 300 that's triggering the infinite loop, and examine line 188 where the parse error occurs to understand the context around it.
The parser hangs between 200-300 lines of preprocessed output. The first error is at line 188. Let me narrow down:
cat > /tmp/test_debug2.go << 'EOF'
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
// Show context around line 188
fmt.Println("=== Context around line 188 ===")
for i := 180; i < 210 && i < len(lines); i++ {
fmt.Printf(" %4d: %s\n", i+1, lines[i])
}
// Try lines 200-260 in smaller steps
for _, nLines := range []int{210, 220, 230, 240, 250, 260} {
if nLines > len(lines) {
nLines = len(lines)
}
subset := strings.Join(lines[:nLines], "\n")
l := lexer.New(subset)
p := parser.New(l)
// Run parser in a goroutine with timeout
done := make(chan bool, 1)
var program interface{ }
var errs []string
type result struct {
stmts int
errs []string
}
ch := make(chan result, 1)
go func() {
prog := p.ParseProgram()
ch <- result{len(prog.Statements), p.Errors()}
}()
select {
case r := <-ch:
_ = program
errs = r.errs
_ = done
status := "OK"
if len(errs) > 0 {
status = fmt.Sprintf("ERRORS(%d)", len(errs))
}
fmt.Printf("Lines %4d: %d stmts, %s\n", nLines, r.stmts, status)
case <-func() <-chan struct{} {
c := make(chan struct{})
go func() {
<-make(chan bool) // never
}()
go func() {
import_time_pkg()
close(c)
}()
return c
}():
}
}
}
EOF
# That approach was getting complicated. Let me just try specific line counts with timeout
for n in 210 220 230 240 250; do
cat > /tmp/test_n.go << GOEOF
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
n := $n
if n > len(lines) { n = len(lines) }
subset := strings.Join(lines[:n], "\n")
l := lexer.New(subset)
p := parser.New(l)
program := p.ParseProgram()
errs := p.Errors()
status := "OK"
if len(errs) > 0 { status = fmt.Sprintf("ERRORS(%d): %s", len(errs), errs[0]) }
fmt.Printf("Lines %d: %d stmts, %s\n", n, len(program.Statements), status)
}
GOEOF
timeout 5 go run /tmp/test_n.go 2>&1 || echo "TIMEOUT/CRASH at $n lines"
doneLines 210: 23 stmts, ERRORS(6): line 188: expected ), got ! Lines 220: 24 stmts, ERRORS(7): line 188: expected ), got ! Lines 230: 24 stmts, ERRORS(9): line 188: expected ), got ! Lines 240: 24 stmts, ERRORS(15): line 188: expected ), got ! Lines 250: 24 stmts, ERRORS(15): line 188: expected ), got !
for n in 300 400 500 600 700 800 900 1000; do
cat > /tmp/test_n.go << GOEOF
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
n := $n
if n > len(lines) { n = len(lines) }
subset := strings.Join(lines[:n], "\n")
l := lexer.New(subset)
p := parser.New(l)
program := p.ParseProgram()
errs := p.Errors()
status := "OK"
if len(errs) > 0 { status = fmt.Sprintf("ERRORS(%d): %s", len(errs), errs[0]) }
fmt.Printf("Lines %d: %d stmts, %s\n", n, len(program.Statements), status)
}
GOEOF
timeout 5 go run /tmp/test_n.go 2>&1 || echo "TIMEOUT/CRASH at $n lines"
doneTIMEOUT/CRASH at 300 lines TIMEOUT/CRASH at 400 lines TIMEOUT/CRASH at 500 lines TIMEOUT/CRASH at 600 lines TIMEOUT/CRASH at 700 lines TIMEOUT/CRASH at 800 lines TIMEOUT/CRASH at 900 lines TIMEOUT/CRASH at 1000 lines
for n in 260 270 275 280 285 290 295; do
cat > /tmp/test_n.go << GOEOF
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
n := $n
if n > len(lines) { n = len(lines) }
subset := strings.Join(lines[:n], "\n")
l := lexer.New(subset)
p := parser.New(l)
program := p.ParseProgram()
errs := p.Errors()
status := "OK"
if len(errs) > 0 { status = fmt.Sprintf("ERRORS(%d): %s", len(errs), errs[0]) }
fmt.Printf("Lines %d: %d stmts, %s\n", n, len(program.Statements), status)
}
GOEOF
timeout 5 go run /tmp/test_n.go 2>&1 || echo "TIMEOUT at $n lines"
doneTIMEOUT at 260 lines TIMEOUT at 270 lines TIMEOUT at 275 lines TIMEOUT at 280 lines TIMEOUT at 285 lines TIMEOUT at 290 lines TIMEOUT at 295 lines
for n in 251 252 253 254 255 256 257 258 259; do
cat > /tmp/test_n.go << GOEOF
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
n := $n
if n > len(lines) { n = len(lines) }
subset := strings.Join(lines[:n], "\n")
l := lexer.New(subset)
p := parser.New(l)
program := p.ParseProgram()
errs := p.Errors()
status := "OK"
if len(errs) > 0 { status = fmt.Sprintf("ERRORS(%d): %s", len(errs), errs[0]) }
fmt.Printf("Lines %d: %d stmts, %s\n", n, len(program.Statements), status)
}
GOEOF
timeout 5 go run /tmp/test_n.go 2>&1 || echo "TIMEOUT at $n lines"
doneLines 251: 25 stmts, ERRORS(16): line 188: expected ), got ! Lines 252: 25 stmts, ERRORS(16): line 188: expected ), got ! Lines 253: 25 stmts, ERRORS(16): line 188: expected ), got ! TIMEOUT at 254 lines Lines 255: 25 stmts, ERRORS(15): line 188: expected ), got ! TIMEOUT at 256 lines TIMEOUT at 257 lines TIMEOUT at 258 lines TIMEOUT at 259 lines
Interesting - 253 works but 254 hangs (mostly), and 255 works but 256 hangs. The non-determinism might be due to go compilation overhead and timing. Let me look at what's at lines 254-256 of the preprocessed output.
cat > /tmp/show_lines.go << 'EOF'
package main
import (
"fmt"
"os"
"strings"
"github.com/codeassociates/occam2go/preproc"
)
func main() {
pp := preproc.New(
preproc.WithIncludePaths([]string{"kroc/modules/course/libsrc"}),
)
result, err := pp.ProcessFile("kroc/demos/complexity/concurrent_hello_1.occ")
if err != nil {
fmt.Fprintf(os.Stderr, "preproc error: %v\n", err)
os.Exit(1)
}
lines := strings.Split(result, "\n")
fmt.Println("=== Lines 180-300 ===")
for i := 179; i < 300 && i < len(lines); i++ {
fmt.Printf("%4d: %s\n", i+1, lines[i])
}
}
EOF
go run /tmp/show_lines.go 2>&1=== Lines 180-300 ===
180: -- [@code ch] anyway. These rules for fieldwidth are the same as those used by
181: -- the Pascal [@text write] procedure.
182: -- @param ch Character
183: -- @param field Field width to right-justify in
184: -- @param out Channel to write to
185: PROC out.ch (VAL BYTE ch, VAL INT field, CHAN BYTE out!)
186: --{{{
187: SEQ
188: out.repeat (' ', field - 1, out!)
189: out ! ch
190: --}}}
191: :
192: --}}}
193: --{{{ PROC out.string (VAL []BYTE s, VAL INT field, CHAN BYTE out!)
194: --* Write a string to a channel.
195: -- This outputs [@code s] in a fieldwidth [@code field] down [@code out].
196: -- @param s String
197: -- @param field Field width to right-justify in
198: -- @param out Channel to write to
199: PROC out.string (VAL []BYTE s, VAL INT field, CHAN BYTE out!)
200: --{{{
201: VAL INT length IS SIZE s:
202: SEQ
203: out.repeat (' ', field - length, out!)
204: SEQ i = 0 FOR length
205: out ! s[i]
206: --}}}
207: :
208: --}}}
209: --{{{ PROC out.byte (VAL BYTE b, VAL INT field, CHAN BYTE out!)
210: --* Write a byte in decimal to a channel.
211: -- This outputs [@code b] in a fieldwidth [@code field] down [@code out]. If
212: -- the fieldwidth is too wide for [@code b], it right-justifies [@code b] with
213: -- spaces on the left. If the field is not wide enough, it prints the [@code
214: -- b] anyway. These rules for fieldwidth are the same as those used by the
215: -- Pascal [@text write] procedure.
216: -- @param b Byte
217: -- @param field Field width to right-justify in
218: -- @param out Channel to write to
219: PROC out.byte (VAL BYTE b, VAL INT field, CHAN BYTE out!)
220: --{{{
221: VAL BYTE hundreds IS b/100:
222: VAL BYTE rest IS b\100:
223: VAL BYTE tens IS rest/10:
224: VAL BYTE ones IS rest\10:
225: IF
226: hundreds > 0
227: SEQ
228: out.repeat (' ', field - 3, out!)
229: out ! hundreds + '0'
230: out ! tens + '0'
231: out ! ones + '0'
232: tens > 0
233: SEQ
234: out.repeat (' ', field - 2, out!)
235: out ! tens + '0'
236: out ! ones + '0'
237: TRUE
238: SEQ
239: out.repeat (' ', field - 1, out!)
240: out ! ones + '0'
241: --}}}
242: :
243: --}}}
244: --{{{ PROC out.int (VAL INT n, VAL INT field, CHAN BYTE out!)
245: --* Write an integer in decimal to a channel.
246: -- This outputs [@code n] in a fieldwidth [@code field] down [@code out]. The
247: -- rules for fieldwidth are as [@ref out.byte].
248: -- @param n Integer
249: -- @param field Field width to right-justify in
250: -- @param out Channel to write to
251: PROC out.int (VAL INT n, VAL INT field, CHAN BYTE out!)
252: --{{{
253: IF
254: n = (MOSTNEG INT)
255: --{{{ minint
256: out.string ("-2147483648", field, out!)
257: --}}}
258: n = 0
259: --{{{ zero
260: SEQ
261: IF
262: 1 < field
263: out.repeat (' ', field - 1, out!)
264: TRUE
265: SKIP
266: out ! '0'
267: --}}}
268: TRUE
269: --{{{ anything else
270: VAL INT max.digits IS 20:
271: [max.digits]INT D:
272: INT x, i:
273: SEQ
274: --{{{ check negative
275: IF
276: n < 0
277: x := -n
278: TRUE -- (n > 0)
279: x := n
280: --}}}
281: --{{{ decompose
282: SEQ
283: i := 0
284: WHILE x > 0
285: SEQ
286: D[i] := x\10
287: x := x/10
288: i := i + 1
289: --}}}
290: --{{{ pad
291: IF
292: n > 0
293: out.repeat (' ', field - i, out!)
294: TRUE
295: SEQ
296: out.repeat (' ', (field - 1) - i, out!)
297: out ! '-'
298: --}}}
299: --{{{ output
300:Now I can see the issue. Look at line 254:
n = (MOSTNEG INT)
The parser sees (MOSTNEG INT) in an expression context. MOSTNEG is probably a keyword. Let me check how it's parsed.
But actually, let me check the "expected ), got !" error at line 188 first:
out.repeat (' ', field - 1, out!)
The out! is the channel direction modifier. In a procedure call, out! means the channel out is being passed with its send direction. The parser is seeing the ( of the argument list, and when it encounters out!, it sees out then ! and doesn't know how to handle the ! inside a parenthesized expression/argument list.
I think I'm confusing the syntax here. In occam, when you declare a procedure with CHAN BYTE out!, that marks the channel as send-only in the parameter list. But when you actually call the procedure, you shouldn't include the ! — you just pass out directly. The direction annotation belongs in the declaration, not the call.
Wait, but this code shows out! being passed at the call site, which suggests this might be KRoC's extended syntax where you can annotate directions at both declaration and call sites. That means my parser needs to handle channel! and channel? as valid expressions when they appear as arguments.
The real issue though is probably not the channel syntax — it's the infinite loop at line 254. The condition n = (MOSTNEG INT) is suspicious. Let me trace through how the parser handles this expression, particularly how it deals with MOSTNEG INT inside parentheses and what happens when it tries to parse the comparison. I'm realizing the core issue is that parseIfStatement enters an infinite loop when the condition expression fails to consume any tokens, which is a fundamental safety problem that needs fixing first. The out! syntax in procedure calls is likely triggering this by causing the parser to get stuck in a bad state, so I should address the infinite loop vulnerability directly, then add support for channel direction annotations, and finally identify what other language features are missing.
Found the problem area. Line 254 has n = (MOSTNEG INT) as an IF condition, and the call sites at line 188 use out! (channel direction in call args). Let me check how the parser handles these:
{
"pattern": "MOSTNEG|MOSTPOS",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true
}2421: case lexer.MOSTNEG_KW, lexer.MOSTPOS_KW: 2423: isNeg := token.Type == lexer.MOSTNEG_KW
{
"pattern": "MOSTNEG|MOSTPOS",
"path": "/home/david/projects/code-associates/occam2go/lexer/token.go",
"output_mode": "content",
"-n": true
}90: MOSTNEG_KW 91: MOSTPOS_KW 178: MOSTNEG_KW: "MOSTNEG", 179: MOSTPOS_KW: "MOSTPOS", 224: "MOSTNEG": MOSTNEG_KW, 225: "MOSTPOS": MOSTPOS_KW,
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 2415,
"limit": 30
} 2415→ token := p.curToken
2416→ p.nextToken()
2417→ left = &ast.SizeExpr{
2418→ Token: token,
2419→ Expr: p.parseExpression(PREFIX),
2420→ }
2421→ case lexer.MOSTNEG_KW, lexer.MOSTPOS_KW:
2422→ token := p.curToken
2423→ isNeg := token.Type == lexer.MOSTNEG_KW
2424→ // Expect a type name next
2425→ if !p.peekTokenIs(lexer.INT_TYPE) && !p.peekTokenIs(lexer.BYTE_TYPE) &&
2426→ !p.peekTokenIs(lexer.BOOL_TYPE) && !p.peekTokenIs(lexer.REAL_TYPE) &&
2427→ !p.peekTokenIs(lexer.REAL32_TYPE) && !p.peekTokenIs(lexer.REAL64_TYPE) {
2428→ p.addError(fmt.Sprintf("expected type after %s, got %s", token.Literal, p.peekToken.Type))
2429→ return nil
2430→ }
2431→ p.nextToken()
2432→ left = &ast.MostExpr{
2433→ Token: token,
2434→ ExprType: p.curToken.Literal,
2435→ IsNeg: isNeg,
2436→ }
2437→ case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
2438→ token := p.curToken
2439→ p.nextToken()
2440→ left = &ast.TypeConversion{
2441→ Token: token,
2442→ TargetType: token.Literal,
2443→ Expr: p.parseExpression(PREFIX),
2444→ }OK so MOSTNEG INT IS handled as an expression prefix (line 2421). But the issue is that in the IF condition at line 254, we have:
n = (MOSTNEG INT)
When parsing this expression:1. Parse n → identifier
2. See = infix → equality
3. Parse right side → parse (MOSTNEG INT)
4. Left paren → starts a grouped expression
5. Parse inner: MOSTNEG → prefix, expects type next → INT → creates MostExpr
6. Then expects ) to close the group
MOSTNEG is handled. Let me check how the expression parser handles parenthesized expressions — the (MOSTNEG INT) pattern:
{
"pattern": "case lexer.LPAREN",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true,
"-A": 20
}2360: case lexer.LPAREN:
2361- p.nextToken()
2362- left = p.parseExpression(LOWEST)
2363- if !p.expectPeek(lexer.RPAREN) {
2364- return nil
2365- }
2366- case lexer.MINUS:
2367- token := p.curToken
2368- p.nextToken()
2369- left = &ast.UnaryExpr{
2370- Token: token,
2371- Operator: "-",
2372- Right: p.parseExpression(PREFIX),
2373- }
2374- case lexer.NOT:
2375- token := p.curToken
2376- p.nextToken()
2377- left = &ast.UnaryExpr{
2378- Token: token,
2379- Operator: "NOT",
2380- Right: p.parseExpression(PREFIX),The LPAREN case handles grouped expressions properly. It calls parseExpression(LOWEST) then expects RPAREN. Since MOSTNEG_KW is handled in the expression parser, (MOSTNEG INT) should parse fine.
So maybe the issue isn't with MOSTNEG INT specifically. Let me investigate what actually causes the hang. Let me write a more targeted test that shows what specific token the parser gets stuck on.
Actually, wait. Let me look at the earlier error first: "line 188: expected ), got !". This is from line 188: out.repeat (' ', field - 1, out!).
The ! after out is throwing off the parser when it's inside the parenthesized call. When expectPeek(RPAREN) encounters the ! instead, it returns nil and leaves the parser in a bad state that doesn't properly advance past the expression. This cascades into later constructs. The infinite loop is happening inside an IF statement—the stack trace shows ParseProgram → parseStatement → parseProcDecl → parseStatement → parseIfStatement, so it's the IF at line 253 inside the out.int procedure that's getting stuck.
I need to tackle this in two parts: first, add a safety check in the parseIfStatement loop to detect when the parser isn't making progress and break out, then identify and fix the actual unsupported language constructs like out! in call arguments.
Let me write a focused test that just parses the out.int procedure to isolate what's actually causing the problem.
cat > /tmp/test_minimal.go << 'EOF'
package main
import (
"fmt"
"strings"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
)
func tryParse(label, source string) {
l := lexer.New(source)
p := parser.New(l)
done := make(chan string, 1)
go func() {
prog := p.ParseProgram()
errs := p.Errors()
if len(errs) > 0 {
done <- fmt.Sprintf("%d stmts, ERRORS(%d): %s", len(prog.Statements), len(errs), strings.Join(errs, "; "))
} else {
done <- fmt.Sprintf("%d stmts, OK", len(prog.Statements))
}
}()
select {
case result := <-done:
fmt.Printf("%-30s %s\n", label+":", result)
case <-func() <-chan struct{} {
c := make(chan struct{})
go func() {
import "time"
time.Sleep(2 * time.Second)
close(c)
}()
return c
}():
fmt.Printf("%-30s INFINITE LOOP\n", label+":")
}
}
func main() {
tryParse("simple IF", `
PROC test ()
IF
TRUE
SKIP
:`)
tryParse("IF with (MOSTNEG INT)", `
PROC test ()
INT n:
SEQ
n := 0
IF
n = (MOSTNEG INT)
SKIP
:`)
tryParse("out! in call", `
PROC test (CHAN BYTE out!)
out.repeat (' ', 3, out!)
:`)
tryParse("VAL []BYTE param", `
PROC test (VAL []BYTE s, CHAN BYTE out!)
SKIP
:`)
tryParse("nested IF with TRUE", `
PROC test ()
IF
TRUE
SEQ
IF
TRUE
SKIP
:`)
}
EOF
# Fix the time import issue
cat > /tmp/test_minimal.go << 'EOF'
package main
import (
"fmt"
"strings"
"time"
"github.com/codeassociates/occam2go/lexer"
"github.com/codeassociates/occam2go/parser"
)
func tryParse(label, source string) {
l := lexer.New(source)
p := parser.New(l)
done := make(chan string, 1)
go func() {
prog := p.ParseProgram()
errs := p.Errors()
if len(errs) > 0 {
done <- fmt.Sprintf("%d stmts, ERRORS(%d): %s", len(prog.Statements), len(errs), strings.Join(errs, "; "))
} else {
done <- fmt.Sprintf("%d stmts, OK", len(prog.Statements))
}
}()
select {
case result := <-done:
fmt.Printf("%-35s %s\n", label+":", result)
case <-time.After(2 * time.Second):
fmt.Printf("%-35s INFINITE LOOP\n", label+":")
}
}
func main() {
tryParse("simple IF", `
PROC test ()
IF
TRUE
SKIP
:`)
tryParse("IF with (MOSTNEG INT)", `
PROC test ()
INT n:
SEQ
n := 0
IF
n = (MOSTNEG INT)
SKIP
:`)
tryParse("out! in call", `
PROC test (CHAN BYTE out!)
out.repeat (' ', 3, out!)
:`)
tryParse("VAL []BYTE param", `
PROC test (VAL []BYTE s, CHAN BYTE out!)
SKIP
:`)
tryParse("nested IF with TRUE", `
PROC test ()
IF
TRUE
SEQ
IF
TRUE
SKIP
:`)
tryParse("IF with var decl in TRUE branch", `
PROC test ()
IF
TRUE
VAL INT max.digits IS 20:
[max.digits]INT D:
INT x, i:
SEQ
SKIP
:`)
tryParse("occam fold markers", `
PROC test ()
--{{{
SKIP
--}}}
:`)
tryParse("BYTE arithmetic", `
PROC test ()
VAL BYTE b IS 42:
VAL BYTE hundreds IS b/100:
SKIP
:`)
}
EOF
timeout 30 go run /tmp/test_minimal.go 2>&1simple IF: 1 stmts, OK IF with (MOSTNEG INT): 1 stmts, OK out! in call: 1 stmts, ERRORS(3): line 3: expected ), got !; line 3: unexpected token: !; line 3: unexpected token: ) VAL []BYTE param: 1 stmts, OK nested IF with TRUE: 1 stmts, OK IF with var decl in TRUE branch: INFINITE LOOP occam fold markers: 1 stmts, OK BYTE arithmetic: 1 stmts, OK
Found it. "IF with var decl in TRUE branch" causes the infinite loop. The issue is that in occam, an IF branch body can contain variable declarations before statements:
IF
TRUE
VAL INT max.digits IS 20:
[max.digits]INT D:
INT x, i:
SEQ
SKIP
The parser doesn't handle multiple statements (declarations + body) inside an IF branch. Let me look at how the IF body is parsed:
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 2217,
"limit": 25
} 2217→ choice := ast.IfChoice{}
2218→ choice.Condition = p.parseExpression(LOWEST)
2219→
2220→ // Skip newlines and expect INDENT for body
2221→ for p.peekTokenIs(lexer.NEWLINE) {
2222→ p.nextToken()
2223→ }
2224→
2225→ if p.peekTokenIs(lexer.INDENT) {
2226→ p.nextToken() // consume INDENT
2227→ p.nextToken() // move to body
2228→ choice.Body = p.parseStatement()
2229→
2230→ // Advance past the last token of the statement if needed
2231→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
2232→ p.nextToken()
2233→ }
2234→ }
2235→
2236→ stmt.Choices = append(stmt.Choices, choice)
2237→ }
2238→
2239→ return stmt
2240→}
2241→Now I see the issue. On line 2228, the IF branch body is parsed with p.parseStatement(), which only parses a single statement. But in occam, the body of an IF branch can contain multiple declarations followed by a process. This is a common occam pattern where you can have local declarations scoped to a particular branch.
For example:
IF
TRUE
VAL INT max.digits IS 20: -- declaration
[max.digits]INT D: -- declaration
INT x, i: -- declaration
SEQ -- process
...
The parser calls parseStatement() which would parse just VAL INT max.digits IS 20: and return. Then the IF parser tries to continue parsing the next IF choice, but it's still inside the INDENT block of the TRUE branch. The remaining declarations and SEQ are at the same indent level but they're not IF conditions — they're continuation of the body.
Since the parser doesn't recognize these as conditions, parseExpression(LOWEST) likely fails to consume tokens and the loop continues infinitely. The real issue is that occam allows declarations to precede a process within a scoped body, so the IF branch parser needs to handle this specification pattern where multiple declarations can be followed by a single process, rather than expecting just one statement.
Let me look at how parseBlockStatements() handles this in other constructs like SEQ blocks to see if there's already a pattern I can follow.
The root cause is clear. Line 2228 calls parseStatement() which parses only a single statement, but occam IF branches can have multiple declarations (specifications) scoped before the process. Let me check how block parsing handles this pattern elsewhere:
{
"pattern": "parseBlockStatements",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true,
"-A": 3
}1392: block.Statements = p.parseBlockStatements()
1393-
1394- return block
1395-}
--
1427: block.Statements = p.parseBlockStatements()
1428-
1429- return block
1430-}
--
1649:func (p *Parser) parseBlockStatements() []ast.Statement {
1650- var statements []ast.Statement
1651- startLevel := p.indentLevel
1652-{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1649,
"limit": 80
} 1649→func (p *Parser) parseBlockStatements() []ast.Statement {
1650→ var statements []ast.Statement
1651→ startLevel := p.indentLevel
1652→
1653→ p.nextToken() // move past INDENT
1654→
1655→ for !p.curTokenIs(lexer.EOF) {
1656→ // Skip newlines
1657→ for p.curTokenIs(lexer.NEWLINE) {
1658→ p.nextToken()
1659→ }
1660→
1661→ // Handle DEDENT tokens
1662→ // If we're at a DEDENT and indentLevel has dropped below startLevel,
1663→ // this DEDENT ends our block - stop parsing
1664→ // If indentLevel >= startLevel, this DEDENT is from a nested block - skip it
1665→ for p.curTokenIs(lexer.DEDENT) {
1666→ if p.indentLevel < startLevel {
1667→ return statements
1668→ }
1669→ p.nextToken() // skip nested block's DEDENT
1670→ }
1671→
1672→ // Skip any more newlines after DEDENT
1673→ for p.curTokenIs(lexer.NEWLINE) {
1674→ p.nextToken()
1675→ }
1676→
1677→ if p.curTokenIs(lexer.EOF) {
1678→ break
1679→ }
1680→
1681→ // Double-check we haven't gone below our level
1682→ if p.indentLevel < startLevel {
1683→ break
1684→ }
1685→
1686→ stmt := p.parseStatement()
1687→ if stmt != nil {
1688→ statements = append(statements, stmt)
1689→ }
1690→
1691→ // After parsing a statement, we need to advance.
1692→ // But if we're already at NEWLINE/DEDENT/EOF, the next iteration will handle it.
1693→ // Only advance if we're still on the last token of the statement.
1694→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
1695→ p.nextToken()
1696→ }
1697→ }
1698→
1699→ return statements
1700→}
1701→
1702→func (p *Parser) parseProcDecl() *ast.ProcDecl {
1703→ proc := &ast.ProcDecl{Token: p.curToken}
1704→
1705→ if !p.expectPeek(lexer.IDENT) {
1706→ return nil
1707→ }
1708→ proc.Name = p.curToken.Literal
1709→
1710→ if !p.expectPeek(lexer.LPAREN) {
1711→ return nil
1712→ }
1713→
1714→ proc.Params = p.parseProcParams()
1715→
1716→ if !p.expectPeek(lexer.RPAREN) {
1717→ return nil
1718→ }
1719→
1720→ // Skip to next line and expect indented body
1721→ for p.peekTokenIs(lexer.NEWLINE) {
1722→ p.nextToken()
1723→ }
1724→
1725→ if !p.peekTokenIs(lexer.INDENT) {
1726→ p.addError("expected indented body after PROC declaration")
1727→ return proc
1728→ }Now I understand the architecture. The parseBlockStatements() method handles multiple statements in a block (SEQ, PAR, etc.), and it correctly handles declarations. But the IF parser doesn't use parseBlockStatements() — instead it calls parseStatement() directly for each branch body.
The issue is that in occam, an IF branch body can have "specifications" (declarations) before the actual process. So we need the IF parser to handle the case where the body starts with declarations.
In occam, the general structure of a "process" at the semantic level is:
{specification}
process
Where specifications (VAL abbrevs, variable declarations, etc.) scope over the process.
This pattern shows up everywhere in occam. The real question is whether to modify parseStatement() to handle specs-before-process universally, create a dedicated helper for IF/CASE branch bodies, or leverage parseBlockStatements() which already handles this correctly. Looking at how the parser currently works with declarations scoping over subsequent processes, I think the cleanest approach would be to ensure branch body parsing follows the same pattern.
Now I understand the pattern. The IF branch body at line 2228 uses parseStatement() for a single statement, but occam allows declarations (specifications) scoped to the branch body. Let me check how the parser handles declarations more broadly — do they nest with their scoped body?
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 140,
"limit": 100
} 140→
141→// ParseProgram parses the entire program
142→func (p *Parser) ParseProgram() *ast.Program {
143→ program := &ast.Program{}
144→ program.Statements = []ast.Statement{}
145→
146→ for !p.curTokenIs(lexer.EOF) {
147→ stmt := p.parseStatement()
148→ if stmt != nil {
149→ program.Statements = append(program.Statements, stmt)
150→ }
151→ p.nextToken()
152→ }
153→
154→ return program
155→}
156→
157→func (p *Parser) parseStatement() ast.Statement {
158→ // Skip newlines
159→ for p.curTokenIs(lexer.NEWLINE) {
160→ p.nextToken()
161→ }
162→
163→ switch p.curToken.Type {
164→ case lexer.VAL:
165→ return p.parseAbbreviation()
166→ case lexer.INITIAL:
167→ return p.parseInitialDecl()
168→ case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
169→ if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) {
170→ return p.parseFuncDecl()
171→ }
172→ return p.parseVarDeclOrAbbreviation()
173→ case lexer.LBRACKET:
174→ return p.parseArrayDecl()
175→ case lexer.CHAN:
176→ return p.parseChanDecl()
177→ case lexer.PROTOCOL:
178→ return p.parseProtocolDecl()
179→ case lexer.RECORD:
180→ return p.parseRecordDecl()
181→ case lexer.TIMER:
182→ return p.parseTimerDecl()
183→ case lexer.SEQ:
184→ return p.parseSeqBlock()
185→ case lexer.PAR:
186→ return p.parseParBlock()
187→ case lexer.ALT:
188→ return p.parseAltBlock()
189→ case lexer.SKIP:
190→ return &ast.Skip{Token: p.curToken}
191→ case lexer.STOP:
192→ return &ast.Stop{Token: p.curToken}
193→ case lexer.PROC:
194→ return p.parseProcDecl()
195→ case lexer.WHILE:
196→ return p.parseWhileLoop()
197→ case lexer.IF:
198→ return p.parseIfStatement()
199→ case lexer.CASE:
200→ return p.parseCaseStatement()
201→ case lexer.IDENT:
202→ // Check for record variable declaration: TYPENAME var:
203→ if p.recordNames[p.curToken.Literal] && p.peekTokenIs(lexer.IDENT) {
204→ return p.parseRecordVarDecl()
205→ }
206→ // Could be assignment, indexed assignment, indexed send/receive, send, receive, or procedure call
207→ if p.peekTokenIs(lexer.LBRACKET) {
208→ return p.parseIndexedOperation()
209→ }
210→ if p.peekTokenIs(lexer.ASSIGN) {
211→ return p.parseAssignment()
212→ }
213→ if p.peekTokenIs(lexer.COMMA) {
214→ return p.parseMultiAssignment()
215→ }
216→ if p.peekTokenIs(lexer.SEND) {
217→ return p.parseSend()
218→ }
219→ if p.peekTokenIs(lexer.RECEIVE) {
220→ if p.timerNames[p.curToken.Literal] {
221→ return p.parseTimerRead()
222→ }
223→ return p.parseReceive()
224→ }
225→ return p.parseProcCall()
226→ case lexer.INDENT, lexer.DEDENT, lexer.EOF:
227→ return nil
228→ default:
229→ p.addError(fmt.Sprintf("unexpected token: %s", p.curToken.Type))
230→ return nil
231→ }
232→}
233→
234→func (p *Parser) parseVarDecl() *ast.VarDecl {
235→ decl := &ast.VarDecl{
236→ Token: p.curToken,
237→ Type: p.curToken.Literal,
238→ }
239→Now I see the full picture. In occam, declarations (specifications) scope over the subsequent process. When you have:VAL INT max.digits IS 20:
[max.digits]INT D:
INT x, i:
SEQ ...
This is a nested scoping pattern. VAL INT max.digits IS 20: scopes over everything after it, [max.digits]INT D: scopes over everything after it, and so on.
In the current parser, each declaration is parsed as an independent statement. The scoping is handled by the codegen, which just emits them sequentially (Go has similar scoping rules within a function).
The problem is that IF branches only parse a single statement for their body. When the body starts with a declaration like VAL INT max.digits IS 20:, that becomes the entire body, leaving the subsequent declarations and the actual process unparsed. The parser then tries to interpret those remaining elements as IF conditions, which fails. I need to change how IF branch bodies are parsed to handle a sequence of declarations followed by a process, rather than just calling parseStatement() once.
Actually, I'm realizing the semantics here—in occam, an IF branch body is exactly one process, but it can be preceded by specifications that act as scoped declarations. So what looks like multiple statements is really nested scoping: the VAL declaration scopes over the array declaration, which scopes over the variable declarations, which scope over the SEQ block. This is similar to how you'd write it in Go with nested variable declarations.
The fix is to use the existing parseBlockStatements() function, which already handles multiple statements at the same indentation level. But the AST's IfChoice struct only has a single Statement field for the body, so I need to modify it to support multiple statements instead.
{
"pattern": "IfChoice|IfStatement",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-n": true,
"-A": 8
}198:// IfStatement represents an IF statement
199:type IfStatement struct {
200- Token lexer.Token // the IF token
201: Choices []IfChoice
202- Replicator *Replicator // optional replicator for IF i = start FOR count
203-}
204-
205:type IfChoice struct {
206- Condition Expression
207- Body Statement
208-}
209-
210:func (i *IfStatement) statementNode() {}
211:func (i *IfStatement) TokenLiteral() string { return i.Token.Literal }
212-
213-// CaseStatement represents a CASE statement
214-type CaseStatement struct {
215- Token lexer.Token // the CASE token
216- Selector Expression // the selector expression
217- Choices []CaseChoice
218-}
219-The IfChoice has a single Body Statement. To handle declarations scoped to an IF branch, I need to either:
Body to []Statement — but that changes the codegen tooActually, looking at how occam works, the body of an IF branch can have specifications (declarations) followed by a process. The simplest approach is to parse all statements in the IF body as a list, and wrap them in some kind of block.
Wait, let me look at how CaseChoice handles this... does it have the same issue?
Actually, let me look at how CASE bodies work.Also, let me think about what the right architectural approach is here.
I'm realizing the core issue: specifications can precede any process in occam, not just in SEQ/PAR blocks. The parser already handles this for block statements via parseBlockStatements(), but IF/CASE/WHILE/ALT bodies each parse a single statement. I should create a helper function that parses any preceding declarations and wraps them with the following process, then use that wherever we currently call parseStatement() for a body.
Actually, the simpler approach is to change the body fields to accept multiple statements and use parseBlockStatements() directly. But I need to check how the INDENT token is handled—parseBlockStatements() expects to consume it, while the IF parser already does that. Let me trace through the token flow to see if I can reuse the existing function or if I need to adjust the logic. I'm realizing the cleanest approach is to change IfChoice.Body to hold a slice of statements instead of a single statement, then parse multiple statements at the same indent level in the IF parser, similar to how parseBlockStatements works. Looking at the codegen, I should be able to emit all the statements in the IF body rather than just one.
Now I have a clear picture of the bug. The IfChoice.Body is a single Statement, but occam allows declarations before the process in an IF branch body. This is a significant architectural issue. Let me plan the fix properly.
Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach. In plan mode, you should: 1. Thoroughly explore the codebase to understand existing patterns 2. Identify similar features and architectural approaches 3. Consider multiple approaches and their trade-offs 4. Use AskUserQuestion if you need to clarify the approach 5. Design a concrete implementation strategy 6. When ready, use ExitPlanMode to present your plan for approval Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.
Let me explore the codebase more thoroughly to understand how the parser handles similar patterns, how the codegen generates IF statements, and what other places might have the same single-statement body issue.
Let me explore the codebase to understand all the related patterns before designing a fix.
{
"subagent_type": "Explore",
"prompt": "I need to understand how the occam2go parser and codegen handle bodies of IF, CASE, ALT, and WHILE constructs - specifically whether they support multiple declarations (specifications) before the process in branch bodies.\n\nThe bug: `parseIfStatement()` in parser.go calls `parseStatement()` once for each IF branch body (line 2228), but occam allows multiple declarations scoped to a branch body. This causes an infinite loop when parsing code like:\n```\nIF\n TRUE\n VAL INT max.digits IS 20:\n [max.digits]INT D:\n INT x, i:\n SEQ\n ...\n```\n\nPlease investigate:\n\n1. Read parser.go around lines 2161-2240 (parseIfStatement) - how IF bodies are parsed\n2. Read parser.go around lines 2242-2350 (parseCaseStatement) - does CASE have the same single-statement body issue?\n3. Read parser.go around lines for parseAltBlock - does ALT have the same issue?\n4. Read parser.go around lines for parseWhileLoop - does WHILE have the same issue?\n5. Look at how codegen.go handles IF statements (generateIfStatement or similar)\n6. Look at how codegen.go handles CASE statements\n7. Look at the AST definitions for IfChoice, CaseChoice, AltCase in ast.go\n8. Check if WHILE body is a single Statement or []Statement\n9. Look at parseProcDecl to see how PROC body is parsed (lines 1702-1760)\n\nBe very thorough - read all relevant sections."
}{
"subagent_type": "Explore",
"prompt": "I need to understand what other language constructs in the occam \"course\" library will cause parser errors, beyond the IF branch body infinite loop bug.\n\nThe first parse error when transpiling the course library is: \"line 188: expected ), got !\" which comes from this occam code:\n```\nout.repeat (' ', field - 1, out!)\n```\nWhere `out!` is a channel direction annotation at the call site (passing a channel with its send direction).\n\nPlease investigate:\n\n1. Read the file /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/utils.occ (full file) and identify ALL occam constructs that might not be supported by the parser. Look for:\n - `out!` or `in?` direction annotations at call sites (not just declarations)\n - `--{{{` and `--}}}` fold markers (should be comments, probably fine)\n - Any unusual syntax\n \n2. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/string.occ and identify unsupported constructs\n\n3. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_cycles.occ and /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/demo_nets.occ for unsupported constructs\n\n4. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/random.occ for unsupported constructs\n\n5. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/file_in.occ - identify unsupported constructs\n\n6. Read /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/float_io.occ - identify unsupported constructs\n\n7. Check how the parser currently handles proc call arguments in parser.go - search for parseProcCall and how it parses argument lists. Does it handle `channel!` or `channel?` in arguments?\n\nFor each file, list the specific constructs that would NOT be handled by the current parser based on the parseStatement switch in parser.go (lines 157-231)."
}[
{
"type": "text",
"text": "Perfect! Now I have all the information I need. Let me create a comprehensive summary of my findings:\n\n## Summary of Unsupported Language Constructs in the Course Library\n\nBased on my thorough investigation of the occam source files and parser implementation, here are all the unsupported constructs that will cause parse errors:\n\n### 1. **Channel Direction Annotations at Call Sites (CRITICAL - causes the reported error)**\n\n**Location:** `utils.occ` lines 58, 73, 98, 104, 109, 126, 133, 163, 166, 189, etc.\n\n**Example:** \n```occam\nout.repeat (' ', field - 1, out!)\n```\n\n**Issue:** The parser's `parseProcCall()` function (lines 1893-1925 in parser.go) calls `parseExpression(LOWEST)` for each argument, but `parseExpression()` does NOT handle the `!` (send) or `?` (receive) tokens. These tokens are only parsed in parameter declarations (lines 1875-1879), not in argument expressions.\n\nThe lexer recognizes `SEND (!)` and `RECEIVE (?)` as tokens, but `parseExpression()` has no case for them in its switch statement (lines 2327-2448). When the parser encounters `out!` in an argument position, it parses `out` as an identifier and then fails when it tries to parse `!` as an infix operator or statement terminator.\n\n**Error:** \"expected ), got !\" at the position of the `!`\n\n**Affected Files:** \n- `utils.occ`: Multiple instances (lines 58, 73, 98, 104, 109, 126, 133, 163, 166, 189, 227, 244, 258, 288, 296, 308, 402, 404, 447, 541, 638, 749, 806, 808, 827, 847, 866, 886, 904, 920, 937, 1092-1093, 1128)\n- `string.occ`: Lines 302, 275, 292-293, 298\n- `file_in.occ`: Lines 242, 258\n- `float_io.occ`: (numerous instances in complex REAL32TOSTRING procedure)\n- `demo_cycles.occ`: Lines 39-41, 50, 52, 62-64, 73-76, 85, 95\n- `demo_nets.occ`: Lines 39-41, 50, 52, 62-64, 73-76, 85, 87\n\n---\n\n### 2. **Fold Markers in Comments (Probably Fine)**\n\n**Locations:** Various files use `--{{{` and `--}}}` markers\n\n**Example:**\n```occam\n--{{{ out stuff\n--}}}\n```\n\n**Status:** These are valid occam comments (lines starting with `--`), so the lexer should handle them fine. Not a parser issue.\n\n---\n\n### 3. **IF Nested Inside IF (Potential Issue - Mentioned in System Context)**\n\nThe system context mentions \"IF branch body infinite loop bug.\" Looking at the code:\n\n**Location:** `utils.occ` lines 643, 150, 198, etc. - nested `IF` expressions inside `IF` statements\n\n**Example:**\n```occam\nIF\n IF i = 1 FOR SIZE a\n a[length.a - i] <> NULL\n length.a := length.a - (i - 1)\n TRUE\n length.a := 0\n```\n\nThis is a **conditional replicator** (`IF i = 1 FOR ...`), which appears to be supported. However, the parser may have issues with certain nested `IF` patterns.\n\n---\n\n### 4. **VAL RETYPES (Type Reinterpretation)**\n\n**Location:** `float_io.occ` line 139\n\n**Example:**\n```occam\nVAL INT X RETYPES X :\n```\n\n**Status:** This is an advanced occam feature for type reinterpretation/casting at the declaration level. The parser does NOT support the `RETYPES` keyword - it has no token type or parsing logic for it.\n\n**Impact:** Would cause \"unexpected token: IDENT\" or similar error when encountering `RETYPES`.\n\n---\n\n### 5. **LONGPROD and LONGDIV Built-in Functions**\n\n**Locations:** `float_io.occ` lines 242, 244, 269, 291, 300, etc.\n\n**Example:**\n```occam\nZfrac, Carry := LONGPROD (PowerTenFrac[St], BiasTenFrac[Sb], 0)\nPlaces, Zfrac, Carry := NORMALISE (Zfrac, Carry)\nXfrac, Carry := LONGDIV (Xfrac >> 1, 0, Zfrac)\n```\n\n**Status:** These appear to be KRoC-specific intrinsic functions. The parser would treat `LONGPROD` as an identifier and attempt to parse it as a function call, which should work syntactically. However, if the code generation doesn't support these functions, they'll fail at the codegen stage, not the parser stage.\n\n---\n\n### 6. **Pragma Directives**\n\n**Locations:** Multiple files (e.g., `utils.occ` lines 170, 268, 530; `file_in.occ` lines 243)\n\n**Example:**\n```occam\n#PRAGMA DEFINED D\n```\n\n**Status:** The preprocessor ignores `#PRAGMA` (line 3 of CLAUDE.md states \"ignores `#PRAGMA`\"). This should be handled as a blank line by the preprocessor, so not a parser issue.\n\n---\n\n### 7. **#IF / #ELSE / #ENDIF Preprocessor Directives**\n\n**Location:** `random.occ` lines 50, 71, 84\n\n**Example:**\n```occam\n#IF (TARGET.BITS.PER.WORD = 32)\n --{{{ 32-bit PRNG\n ...\n#ELSE\n --{{{ 16-bit PRNG\n ...\n#ENDIF\n```\n\n**Status:** The preprocessor supports this (documented in CLAUDE.md). Should be fine.\n\n---\n\n### 8. **Special Operators in float_io.occ**\n\n**Location:** Lines using bitwise operations on REAL32 values\n\n**Example:**\n```occam\nVAL SignBit IS #80000000 :\nXexp := Xexp - Zexp\n```\n\n**Status:** Bitwise operators and hexadecimal literals are supported. Not an issue.\n\n---\n\n## Root Cause Analysis\n\nThe **primary issue** blocking transpilation of the course library is the **lack of support for channel direction annotations (`!` and `?`) at call sites**. \n\nIn occam, when you pass a channel to a procedure, you can specify its direction:\n- `out!` means pass the send-only end\n- `out?` means pass the receive-only end \n- `out` alone means pass the channel unrestricted\n\nThe current parser only handles direction annotations in **parameter declarations**:\n```occam\nPROC out.repeat (VAL BYTE ch, VAL INT n, CHAN BYTE out!) -- OK in declaration\n```\n\nBut **not in call sites**:\n```occam\nout.repeat (' ', field - 1, out!) -- FAILS - parser can't handle out! here\n```\n\n**Solution Required:** Extend `parseExpression()` to handle `SEND` and `RECEIVE` tokens as postfix operators on channel identifiers, creating a new AST node type (e.g., `ChannelDirected`) that captures the channel name and its direction restriction.\n\n### Secondary Issues (if the above is fixed):\n\n1. **VAL RETYPES** - Would need lexer token and parser support\n2. **Potential nested IF edge cases** - Mentioned in the system context; needs verification with actual parse testing"
},
{
"type": "text",
"text": "agentId: aa6a82d (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 54144\ntool_uses: 13\nduration_ms: 37187</usage>"
}
][
{
"type": "text",
"text": "Perfect. Now I have a complete understanding. Let me prepare a comprehensive analysis for the user.\n\n## Comprehensive Investigation Report: Parser and Codegen Handling of IF, CASE, ALT, WHILE Bodies\n\nI've thoroughly examined the parser and codegen code. Here's my detailed analysis:\n\n---\n\n### 1. PARSEIFSTATEMENT (parser.go lines 2161-2240)\n\n**The Bug is Confirmed:**\n- Line 2228: `choice.Body = p.parseStatement()` - parses **only ONE statement per IF branch body**\n- The AST struct `IfChoice` (ast.go:205-208) has `Body Statement` (singular, not `[]Statement`)\n\n```go\ntype IfChoice struct {\n Condition Expression\n Body Statement // ← SINGLE statement only\n}\n```\n\n**The Problem:** When the parser encounters an IF branch body like:\n```\nIF\n TRUE\n VAL INT max.digits IS 20:\n [max.digits]INT D:\n INT x, i:\n SEQ\n ...\n```\n\n1. `parseStatement()` returns after parsing `VAL INT max.digits IS 20:` as an Abbreviation\n2. The parser is now positioned on the next token\n3. The loop in `parseIfStatement()` (line 2190-2237) continues iteration\n4. It tries to parse `[max.digits]INT D:` as a new **IF choice** (thinking the indentation level hasn't changed)\n5. This causes the parser to enter an infinite loop because `[max.digits]INT D:` is not a valid condition expression\n\n**Current Logic (lines 2220-2233):**\n```go\nif p.peekTokenIs(lexer.INDENT) {\n p.nextToken() // consume INDENT\n p.nextToken() // move to body\n choice.Body = p.parseStatement() // ← Single statement only\n \n // Advance past the last token of the statement if needed\n if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n }\n}\n```\n\n---\n\n### 2. PARSECASESTATEMENT (parser.go lines 2242-2320)\n\n**SAME PROBLEM EXISTS:**\n- Line 2308: `choice.Body = p.parseStatement()` - only one statement\n- `CaseChoice` struct (ast.go:220-224) also has `Body Statement` (singular)\n\n```go\ntype CaseChoice struct {\n Values []Expression\n IsElse bool\n Body Statement // ← SINGLE statement only\n}\n```\n\n**Code Structure (lines 2305-2314):**\n```go\nif p.peekTokenIs(lexer.INDENT) {\n p.nextToken() // consume INDENT\n p.nextToken() // move to body\n choice.Body = p.parseStatement() // ← Single statement only\n \n if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n }\n}\n```\n\n---\n\n### 3. PARSEALTBLOCK and PARSEALTCASE (parser.go lines 1467-1646)\n\n**SAME PROBLEM EXISTS:**\n- Line 1639: `altCase.Body = p.parseStatement()` - only one statement\n- `AltCase` struct (ast.go:392-401) has `Body Statement` (singular)\n\n```go\ntype AltCase struct {\n Guard Expression\n Channel string\n ChannelIndex Expression\n Variable string\n Body Statement // ← SINGLE statement only\n IsTimer bool\n Timer string\n Deadline Expression\n}\n```\n\n**Code Location (lines 1631-1644):**\n```go\nif !p.peekTokenIs(lexer.INDENT) {\n p.addError(\"expected indented body after ALT case\")\n return altCase\n}\np.nextToken() // consume INDENT\np.nextToken() // move into body\n\naltCase.Body = p.parseStatement() // ← Single statement only\n\n// Skip to end of body block\nfor !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n}\n```\n\n---\n\n### 4. PARSEWHILELOOP (parser.go lines 2132-2159)\n\n**SAME PROBLEM EXISTS:**\n- Line 2151: `loop.Body = p.parseStatement()` - only one statement\n- `WhileLoop` struct (ast.go:189-193) has `Body Statement` (singular)\n\n```go\ntype WhileLoop struct {\n Token lexer.Token\n Condition Expression\n Body Statement // ← SINGLE statement only\n}\n```\n\n**Code (lines 2143-2151):**\n```go\nif !p.peekTokenIs(lexer.INDENT) {\n p.addError(\"expected indented block after WHILE condition\")\n return loop\n}\np.nextToken() // consume INDENT\np.nextToken() // move to first statement\n\nloop.Body = p.parseStatement() // ← Single statement only\n```\n\n---\n\n### 5. CONTRAST: PARSEPROCBODY (parser.go lines 1702-1761)\n\n**THIS WORKS CORRECTLY:**\n- Uses `parseBlockStatements()` function (line 1758) which returns `[]Statement` (multiple)\n- Proc body structure: `Body []Statement` (array)\n\n```go\nproc.Body = append(proc.Body, stmt) // ← Appends multiple statements\n```\n\n**Key Loop Pattern (lines 1735-1761):**\n```go\nfor !p.curTokenIs(lexer.EOF) {\n // Skip newlines\n for p.curTokenIs(lexer.NEWLINE) {\n p.nextToken()\n }\n \n // Handle DEDENTs\n for p.curTokenIs(lexer.DEDENT) {\n if p.indentLevel < bodyLevel {\n goto procBodyDone\n }\n p.nextToken()\n }\n \n // ... newline skipping ...\n \n if p.curTokenIs(lexer.EOF) || p.indentLevel < bodyLevel {\n break\n }\n \n stmt := p.parseStatement()\n if stmt != nil {\n proc.Body = append(proc.Body, stmt) // ← Builds array\n }\n}\n```\n\nThe difference is `parseBlockStatements()` (lines 1649-1698) properly loops and collects **multiple statements** at the same indentation level.\n\n---\n\n### 6. CODEGEN HANDLING\n\n**All codegens assume SINGLE body statements:**\n\n**generateIfStatement (codegen.go:1511-1582):**\n```go\nif choice.Body != nil {\n g.generateStatement(choice.Body) // ← Expects single Statement\n}\n```\n\n**generateCaseStatement (codegen.go:1584-1612):**\n```go\nif choice.Body != nil {\n g.generateStatement(choice.Body) // ← Expects single Statement\n}\n```\n\n**generateAltBlock (codegen.go:1204-1258):**\n```go\nif c.Body != nil {\n g.generateStatement(c.Body) // ← Expects single Statement\n}\n```\n\n**generateWhileLoop (codegen.go:1496-1509):**\n```go\nif loop.Body != nil {\n g.generateStatement(loop.Body) // ← Expects single Statement\n}\n```\n\n---\n\n### 7. AST STRUCTURE SUMMARY\n\n| Construct | Body Field | Type | Issue |\n|-----------|-----------|------|-------|\n| `IfChoice` | `Body` | `Statement` | SINGLE |\n| `CaseChoice` | `Body` | `Statement` | SINGLE |\n| `AltCase` | `Body` | `Statement` | SINGLE |\n| `WhileLoop` | `Body` | `Statement` | SINGLE |\n| `ProcDecl` | `Body` | `[]Statement` | MULTIPLE ✓ |\n\n---\n\n### 8. ROOT CAUSE ANALYSIS\n\n**The Bug Chain:**\n\n1. **AST design**: IF/CASE/ALT/WHILE branches can only hold ONE statement\n2. **Parser design**: `parseIfStatement()`, `parseCaseStatement()`, `parseAltCase()`, `parseWhileLoop()` all call `parseStatement()` once (not a loop)\n3. **Parser behavior**: After parsing one statement, if more declarations/statements follow at the same indentation, the parser tries to parse them as **new choices/conditions** instead of continuing the body\n4. **Infinite loop**: When parsing `VAL INT x IS 20: [x]INT arr: ...`, after the first line, the parser attempts to parse the array declaration as a new IF condition, which fails to parse as an expression, potentially causing the parser to loop\n\n---\n\n### 9. WORKING PATTERN (PROCS/FUNCTIONS)\n\nFor reference, `ProcDecl` handles this correctly:\n- AST: `Body []Statement` - array\n- Parser: Uses loop with `parseBlockStatements()` - collects multiple statements\n- Codegen: Iterates: `for _, stmt := range proc.Body { g.generateStatement(stmt) }`\n\n---\n\n### 10. THE FIX REQUIRED\n\nTo support multiple declarations/statements in IF, CASE, ALT, and WHILE branch bodies:\n\n1. **Change AST structs** from `Body Statement` to `Body []Statement`\n - `IfChoice`: `Body []Statement`\n - `CaseChoice`: `Body []Statement`\n - `AltCase`: `Body []Statement`\n - `WhileLoop`: `Body []Statement`\n\n2. **Change Parser logic** to use `parseBlockStatements()` approach instead of single `parseStatement()`\n\n3. **Update Codegen** to iterate through multiple statements:\n ```go\n for _, stmt := range choice.Body {\n g.generateStatement(stmt)\n }\n ```\n\nThis mirrors the proven pattern already working in PROC/FUNCTION body parsing.\n\n---\n\n### Key File Locations:\n- **Parser**: `/home/david/projects/code-associates/occam2go/parser/parser.go` (lines 1467-1646, 2132-2240, 2242-2320)\n- **AST**: `/home/david/projects/code-associates/occam2go/ast/ast.go` (lines 189-228, 383-401)\n- **Codegen**: `/home/david/projects/code-associates/occam2go/codegen/codegen.go` (lines 1204-1258, 1496-1612)"
},
{
"type": "text",
"text": "agentId: a704383 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 48454\ntool_uses: 35\nduration_ms: 60125</usage>"
}
]Excellent! I now have a thorough understanding of the issues. Let me summarize:
Two main bugs causing the infinite loop/OOM:
IF branch body parsing (root cause of infinite loop): IfChoice.Body is a single Statement, but occam allows multiple declarations scoped to a branch body. Same issue exists in CASE, ALT, and WHILE constructs. The fix is to change Body to []Statement and use a block-parsing loop.
Channel direction annotations at call sites (out!, in?): The parser doesn't handle ! or ? after channel identifiers in procedure call arguments.
Now I'm prioritizing which fixes are needed for the course library. Both bugs are blocking transpilation, but I should check which files are affected—utils.occ and the demo files heavily use out! annotations, and several files have IF branches with declarations that would trigger the infinite loop.
For the demo program itself, it imports the entire course library through #INCLUDE "course.module", so it depends on fixing those core issues. I'm also noticing the channel syntax in the demo might be CHAN INT rather than CHAN OF INT, which I should verify since that could be another compatibility issue.
Now I need to prioritize the implementation plan. The critical fixes are updating the AST to handle statement blocks properly in control structures, adding channel direction annotations when parsing function arguments, and implementing safety checks in parsing loops to prevent infinite loops. Before diving into implementation, though, I should clarify whether all library files need to compile successfully or just the ones the demo actually uses.
Since the preprocessor includes the entire course module textually, every file gets parsed regardless of whether we call its functions. That means even unused files like float_io.occ need to parse without errors or infinite loops. Rather than implementing every feature like RETYPES, I could make the parser more resilient by skipping constructs it doesn't recognize, while focusing on fixing the specific issues blocking the demo—the multi-statement bodies and channel direction annotations.
Looking at the build configuration, I'm wondering if float_io.occ could be conditionally excluded from the library sources instead of trying to parse it.
The real issue is that all seven files get bundled into the course module, so I can't selectively skip problematic ones without modifying the build output. I could generate a custom course.module that leaves out the most complex files, but that's just a workaround—the parser needs to be robust enough to handle everything gracefully, even if it skips unsupported constructs.
So my plan is to tackle the core issues: fix the infinite loops in multi-statement bodies, add channel direction annotations at call sites, and implement proper error recovery so the parser doesn't get stuck. Then I'll see what else breaks when I try to transpile.
{
"subagent_type": "Plan",
"prompt": "I need to design an implementation plan for fixing parser bugs in the occam2go transpiler that prevent transpiling the occam \"course\" library module. Here's the full context:\n\n## Problem\nWhen transpiling a program that includes the course library (`#INCLUDE \"course.module\"`), the parser enters an infinite loop and causes OOM. There are two main issues:\n\n### Issue 1: Multi-statement IF/CASE/ALT/WHILE bodies (causes infinite loop)\nIn occam, branch bodies can have multiple declarations (specifications) scoped before the process:\n```occam\nIF\n TRUE\n VAL INT max.digits IS 20:\n [max.digits]INT D:\n INT x, i:\n SEQ\n ...\n```\n\nBut the parser only calls `parseStatement()` once per branch body. After parsing the first declaration, the parser tries to interpret subsequent declarations as new IF conditions, creating an infinite loop.\n\n**Affected constructs and their AST types:**\n- `IfChoice` (ast.go:205-208): `Body Statement` (single)\n- `CaseChoice` (ast.go:220-224): `Body Statement` (single)\n- `AltCase` (ast.go:392-401): `Body Statement` (single)\n- `WhileLoop` (ast.go:189-193): `Body Statement` (single)\n\n**Working pattern to follow:** `ProcDecl` (ast.go) uses `Body []Statement` and `parseBlockStatements()` at parser.go:1649-1700. The codegen iterates with `for _, stmt := range proc.Body`.\n\n### Issue 2: Channel direction annotations at call sites\nOccam allows direction annotations when passing channels to procedures:\n```occam\nout.repeat (' ', field - 1, out!) -- out! = send direction\nin.something (data, in?) -- in? = receive direction\n```\n\nThe parser's `parseProcCall()` (parser.go ~1893-1925) calls `parseExpression(LOWEST)` for each argument. When it encounters `out!`, it parses `out` as an IDENT, then hits `!` (SEND token) which isn't handled in expression parsing, causing \"expected ), got !\" error.\n\n**Current call argument parsing:** In `parseProcCall`, the argument list is parsed by calling `parseExpression(LOWEST)` for each comma-separated argument.\n\n## Current Code Structure\n\n### AST (ast/ast.go)\n```go\ntype IfChoice struct {\n Condition Expression\n Body Statement // NEEDS to be []Statement\n}\ntype CaseChoice struct {\n Values []Expression\n IsElse bool\n Body Statement // NEEDS to be []Statement\n}\ntype AltCase struct {\n // ... guards, channel info ...\n Body Statement // NEEDS to be []Statement\n}\ntype WhileLoop struct {\n Token lexer.Token\n Condition Expression\n Body Statement // NEEDS to be []Statement\n}\n```\n\n### Parser body parsing pattern (parser.go)\nAll four constructs follow this pattern for body parsing:\n```go\nif p.peekTokenIs(lexer.INDENT) {\n p.nextToken() // consume INDENT\n p.nextToken() // move to body\n choice.Body = p.parseStatement() // ← ONLY ONE statement\n // advance past statement\n}\n```\n\nThe working pattern in `parseBlockStatements()` (parser.go:1649-1700):\n```go\nfunc (p *Parser) parseBlockStatements() []ast.Statement {\n var statements []ast.Statement\n startLevel := p.indentLevel\n p.nextToken() // move past INDENT\n for !p.curTokenIs(lexer.EOF) {\n // Skip newlines and handle DEDENTs\n // ...\n if p.indentLevel < startLevel { break }\n stmt := p.parseStatement()\n if stmt != nil {\n statements = append(statements, stmt)\n }\n // advance past statement\n }\n return statements\n}\n```\n\n### Codegen patterns (codegen/codegen.go)\nIF: `g.generateStatement(choice.Body)` (single statement)\nCASE: `g.generateStatement(choice.Body)` (single statement)\nALT: `g.generateStatement(c.Body)` (single statement)\nWHILE: `g.generateStatement(loop.Body)` (single statement)\nPROC: `for _, stmt := range proc.Body { g.generateStatement(stmt) }` (loop)\n\n### Parser proc call (parser.go ~1893-1925)\n```go\nfunc (p *Parser) parseProcCall() *ast.ProcCall {\n call := &ast.ProcCall{...}\n if p.peekTokenIs(lexer.LPAREN) {\n p.nextToken() // consume LPAREN\n // Parse arguments\n for !p.peekTokenIs(lexer.RPAREN) {\n p.nextToken()\n arg := p.parseExpression(LOWEST)\n call.Args = append(call.Args, arg)\n // skip comma\n }\n p.nextToken() // consume RPAREN\n }\n return call\n}\n```\n\n## Lexer tokens available\n- `SEND` = `!` \n- `RECEIVE` = `?`\n- These are already defined in lexer/token.go\n\n## Design Requirements\n1. Change Body from single Statement to []Statement in IfChoice, CaseChoice, AltCase, WhileLoop\n2. Update parser to use block-parsing loop for these bodies (follow parseBlockStatements pattern)\n3. Update codegen to iterate over Body slices\n4. Update all existing tests that reference Body field\n5. Handle channel direction at call sites (e.g., `out!` treated as just passing the channel identifier - the direction is informational for the type system but doesn't change the Go codegen)\n6. Add safety: prevent infinite loops in parsing loops by checking for no progress\n\n## Files to modify\n- `ast/ast.go` - Change Body fields\n- `parser/parser.go` - Update body parsing, add direction handling in expressions\n- `codegen/codegen.go` - Update to iterate over body slices\n- `parser/parser_test.go` - Update tests\n- `codegen/codegen_test.go` - Update tests\n- `codegen/e2e_test.go` - Add e2e test for the new features\n\nPlease provide a detailed, step-by-step implementation plan."
}[
{
"type": "text",
"text": "Good. The `VariantCase.Body` has the same single-statement issue. However, the user's requirements specifically mention `IfChoice`, `CaseChoice`, `AltCase`, and `WhileLoop`. Let me check whether VariantCase bodies can also have multi-statement occam bodies in practice. Since the user hasn't mentioned it and it's a separate construct, I'll note it as a follow-up but focus on the four requested structs.\n\nNow I have all the information I need. Here is the comprehensive implementation plan.\n\n---\n\n## Detailed Implementation Plan: Fix Parser Bugs for Multi-Statement Bodies and Channel Direction at Call Sites\n\n### Overview\n\nThere are two bugs to fix:\n\n1. **Multi-statement IF/CASE/ALT/WHILE bodies**: The `Body` field in `IfChoice`, `CaseChoice`, `AltCase`, and `WhileLoop` is currently a single `Statement`. It needs to become `[]Statement` to support occam's specification-scoped declarations before the process in branch bodies.\n\n2. **Channel direction annotations at call sites**: When passing channels to procedures, occam allows `out!` (send direction) or `in?` (receive direction) annotations. The parser's expression parsing does not handle `!` or `?` following an identifier in expression context, causing errors.\n\n---\n\n### Step 1: AST Changes (`/home/david/projects/code-associates/occam2go/ast/ast.go`)\n\nChange the `Body` field from `Statement` to `[]Statement` in four structs:\n\n**WhileLoop (line 189-193):**\n```go\ntype WhileLoop struct {\n Token lexer.Token\n Condition Expression\n Body []Statement // was: Statement\n}\n```\n\n**IfChoice (line 205-208):**\n```go\ntype IfChoice struct {\n Condition Expression\n Body []Statement // was: Statement\n}\n```\n\n**CaseChoice (line 220-224):**\n```go\ntype CaseChoice struct {\n Values []Expression\n IsElse bool\n Body []Statement // was: Statement\n}\n```\n\n**AltCase (line 392-401):**\n```go\ntype AltCase struct {\n Guard Expression\n Channel string\n ChannelIndex Expression\n Variable string\n Body []Statement // was: Statement\n IsTimer bool\n Timer string\n Deadline Expression\n}\n```\n\n**Note on VariantCase (line 447-451):** This has the same pattern (`Body Statement`) but is out of scope for this change. It could be addressed in a follow-up if needed.\n\n---\n\n### Step 2: Parser Changes (`/home/david/projects/code-associates/occam2go/parser/parser.go`)\n\n#### 2a: Create a helper function `parseBodyStatements()`\n\nThis is the core parser fix. Create a new function that follows the `parseBlockStatements()` pattern (lines 1649-1700) but is called after the INDENT has already been consumed. The caller passes in the start indent level.\n\nLocation: Add near `parseBlockStatements()` around line 1700.\n\n```go\n// parseBodyStatements parses multiple statements inside an indented body block.\n// The INDENT token has already been consumed by the caller.\n// Returns a slice of statements forming the body.\nfunc (p *Parser) parseBodyStatements() []Statement {\n var statements []ast.Statement\n startLevel := p.indentLevel\n\n p.nextToken() // move past INDENT to first statement\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: save position to prevent infinite loops\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 // Infinite loop guard: if parseStatement made no progress, break\n if p.curToken == prevToken && p.peekToken == prevPeek {\n p.nextToken() // force progress\n }\n\n // Advance past statement if needed\n if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n }\n }\n\n return statements\n}\n```\n\nThis is essentially identical to `parseBlockStatements()` with the addition of an infinite loop guard. The `parseBlockStatements()` function itself could also benefit from this safety guard, but that is a separate concern.\n\n#### 2b: Update `parseIfStatement()` (lines 2225-2234)\n\nCurrent code (lines 2225-2233):\n```go\nif p.peekTokenIs(lexer.INDENT) {\n p.nextToken() // consume INDENT\n p.nextToken() // move to body\n choice.Body = p.parseStatement()\n\n // Advance past the last token of the statement if needed\n if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n }\n}\n```\n\nChange to:\n```go\nif p.peekTokenIs(lexer.INDENT) {\n p.nextToken() // consume INDENT\n choice.Body = p.parseBodyStatements()\n}\n```\n\n#### 2c: Update `parseCaseStatement()` (lines 2305-2314)\n\nSame pattern. Current code:\n```go\nif p.peekTokenIs(lexer.INDENT) {\n p.nextToken() // consume INDENT\n p.nextToken() // move to body\n choice.Body = p.parseStatement()\n\n if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n }\n}\n```\n\nChange to:\n```go\nif p.peekTokenIs(lexer.INDENT) {\n p.nextToken() // consume INDENT\n choice.Body = p.parseBodyStatements()\n}\n```\n\n#### 2d: Update `parseWhileLoop()` (lines 2148-2156)\n\nCurrent code:\n```go\np.nextToken() // consume INDENT\np.nextToken() // move to first statement\n\nloop.Body = p.parseStatement()\n\n// Consume until DEDENT\nfor !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n}\n```\n\nChange to:\n```go\np.nextToken() // consume INDENT\nloop.Body = p.parseBodyStatements()\n```\n\nThis eliminates the dangerous \"consume until DEDENT\" loop that was a secondary infinite loop risk.\n\n#### 2e: Update `parseAltCase()` (lines 1635-1644)\n\nCurrent code:\n```go\np.nextToken() // consume INDENT\np.nextToken() // move into body\n\n// Parse the body (first statement)\naltCase.Body = p.parseStatement()\n\n// Skip to end of body block\nfor !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n p.nextToken()\n}\n```\n\nChange to:\n```go\np.nextToken() // consume INDENT\naltCase.Body = p.parseBodyStatements()\n```\n\n#### 2f: Handle channel direction annotations in expression parsing\n\nIn `parseProcCall()` (lines 1893-1925), the arguments are parsed with `parseExpression(LOWEST)`. When the parser encounters `out!` or `in?`, `parseExpression` parses `out` as an `IDENT`, then the infix loop checks `peekToken` -- it does not match any infix operator, so it returns just the identifier. But then `parseProcCall` checks `peekTokenIs(COMMA)` or `peekTokenIs(RPAREN)` and finds `!` or `?` instead, causing the error.\n\nThe fix should be in `parseProcCall()` specifically: after parsing each argument expression, if the next token is `SEND` (`!`) or `RECEIVE` (`?`) and it is followed by `COMMA` or `RPAREN`, skip the direction token. This approach is safe because `!` and `?` are not valid infix operators in expressions.\n\nLocation: In `parseProcCall()` at lines 1911-1918, after each `parseExpression(LOWEST)`:\n\n```go\np.nextToken() // move to first arg\ncall.Args = append(call.Args, p.parseExpression(LOWEST))\n// Skip optional channel direction annotation (out! or in?)\nif p.peekTokenIs(lexer.SEND) || p.peekTokenIs(lexer.RECEIVE) {\n p.nextToken() // consume the ! or ?\n}\n\nfor p.peekTokenIs(lexer.COMMA) {\n p.nextToken() // consume comma\n p.nextToken() // move to next arg\n call.Args = append(call.Args, p.parseExpression(LOWEST))\n // Skip optional channel direction annotation\n if p.peekTokenIs(lexer.SEND) || p.peekTokenIs(lexer.RECEIVE) {\n p.nextToken() // consume the ! or ?\n }\n}\n```\n\n**Also apply the same fix in `parseFuncCallExpr()`** (lines 2103-2130) since function calls parse arguments the same way.\n\n---\n\n### Step 3: Codegen Changes (`/home/david/projects/code-associates/occam2go/codegen/codegen.go`)\n\nEvery place that accesses `Body` on these four types must change from calling `g.generateStatement(x.Body)` to iterating `for _, stmt := range x.Body { g.generateStatement(stmt) }`, and from `g.containsX(x.Body)` to a loop.\n\n#### 3a: Create a helper to iterate over body slices\n\nAdd a helper that generates statements from a body slice. This reduces repetition:\n\n```go\nfunc (g *Generator) generateBodyStatements(body []ast.Statement) {\n for _, stmt := range body {\n g.generateStatement(stmt)\n }\n}\n```\n\nAnd helpers for the `containsX` scanners:\n\n```go\nfunc (g *Generator) bodyContains(body []ast.Statement, check func(ast.Statement) bool) bool {\n for _, stmt := range body {\n if check(stmt) {\n return true\n }\n }\n return false\n}\n```\n\n#### 3b: Update `generateWhileLoop()` (line 1496-1509)\n\nChange from:\n```go\nif loop.Body != nil {\n g.generateStatement(loop.Body)\n}\n```\nTo:\n```go\nfor _, stmt := range loop.Body {\n g.generateStatement(stmt)\n}\n```\n\n#### 3c: Update `generateIfStatement()` (lines 1511-1582)\n\nIn both the replicated and non-replicated paths, change:\n```go\nif choice.Body != nil {\n g.generateStatement(choice.Body)\n}\n```\nTo:\n```go\nfor _, stmt := range choice.Body {\n g.generateStatement(stmt)\n}\n```\n\nThis applies at lines 1551-1552 (replicated path) and lines 1574-1575 (normal path).\n\n#### 3d: Update `generateCaseStatement()` (lines 1584-1612)\n\nChange at lines 1605-1606:\n```go\nif choice.Body != nil {\n g.generateStatement(choice.Body)\n}\n```\nTo:\n```go\nfor _, stmt := range choice.Body {\n g.generateStatement(stmt)\n}\n```\n\n#### 3e: Update `generateAltBlock()` (lines 1204-1258)\n\nChange at lines 1252-1253:\n```go\nif c.Body != nil {\n g.generateStatement(c.Body)\n}\n```\nTo:\n```go\nfor _, stmt := range c.Body {\n g.generateStatement(stmt)\n}\n```\n\n#### 3f: Update all `containsX()` scanner functions\n\nThere are 6 scanner functions that walk the tree and access `.Body` on the affected types:\n- `containsPar()` (lines 186-238)\n- `containsPrint()` (lines 240-298)\n- `containsTimer()` (lines 300-361)\n- `containsStop()` (lines 363-421)\n- `containsMostExpr()` (lines 423-523)\n- `collectChanProtocols()` (lines 868-923)\n- `collectRecordVars()` (lines 925-979)\n\nFor each scanner, every occurrence of the pattern:\n```go\nif choice.Body != nil && g.containsX(choice.Body) {\n return true\n}\n```\nor:\n```go\nif s.Body != nil && g.containsX(s.Body) {\n return true\n}\n```\n\nMust become:\n```go\nfor _, stmt := range choice.Body {\n if g.containsX(stmt) {\n return true\n }\n}\n```\nor:\n```go\nfor _, stmt := range s.Body {\n if g.containsX(stmt) {\n return true\n }\n}\n```\n\n**Specific locations by function (all in codegen.go):**\n\n| Function | Type | Old Line(s) |\n|---|---|---|\n| `containsPar` | `AltCase` | 198 |\n| `containsPar` | `WhileLoop` | 215-216 |\n| `containsPar` | `IfChoice` | 220-221 |\n| `containsPar` | `CaseChoice` | 226-227 |\n| `containsPar` | `VariantCase` | 232-233 |\n| `containsPrint` | `AltCase` | 258-259 |\n| `containsPrint` | `WhileLoop` | 275-276 |\n| `containsPrint` | `IfChoice` | 280-281 |\n| `containsPrint` | `CaseChoice` | 286-287 |\n| `containsPrint` | `VariantCase` | 292-293 |\n| `containsTimer` | `AltCase` | 309 |\n| `containsTimer` | `WhileLoop` | 338-339 |\n| `containsTimer` | `IfChoice` | 343-344 |\n| `containsTimer` | `CaseChoice` | 349-350 |\n| `containsTimer` | `VariantCase` | 355-356 |\n| `containsStop` | `AltCase` | 381-382 |\n| `containsStop` | `WhileLoop` | 398-399 |\n| `containsStop` | `IfChoice` | 403-404 |\n| `containsStop` | `CaseChoice` | 409-410 |\n| `containsStop` | `VariantCase` | 415-416 |\n| `containsMostExpr` | `WhileLoop` | 468-469 |\n| `containsMostExpr` | `IfChoice` | 476 |\n| `containsMostExpr` | `CaseChoice` | 490 |\n| `containsMostExpr` | `AltCase` | 511-512 |\n| `containsMostExpr` | `VariantCase` | 517-518 |\n| `collectChanProtocols` | `WhileLoop` | 901-902 |\n| `collectChanProtocols` | `IfChoice` | 906-907 |\n| `collectChanProtocols` | `CaseChoice` | 912-913 |\n| `collectChanProtocols` | `AltCase` | 918-919 |\n| `collectRecordVars` | `WhileLoop` | 957-958 |\n| `collectRecordVars` | `IfChoice` | 962-963 |\n| `collectRecordVars` | `CaseChoice` | 968-969 |\n| `collectRecordVars` | `AltCase` | 974-975 |\n\n**Note:** `VariantCase.Body` is NOT in scope for this change (per the requirements). The VariantCase locations should be left as-is unless we decide to extend the change to VariantCase as well. However, leaving VariantCase as `Statement` while changing the other four to `[]Statement` creates an inconsistency. I recommend also changing `VariantCase.Body` to `[]Statement` in the same pass for consistency, but this is optional.\n\n---\n\n### Step 4: Test Updates\n\n#### 4a: Parser tests (`/home/david/projects/code-associates/occam2go/parser/parser_test.go`)\n\nTests that reference `.Body` on the affected types need to change from checking `Body == nil` to checking `len(Body) == 0` or accessing `Body[0]`:\n\n- **TestWhileLoop** (line 374): `if loop.Body == nil` -> `if len(loop.Body) == 0`\n- **TestIfStatement** (lines 408, 416): `if ifStmt.Choices[0].Body == nil` -> `if len(ifStmt.Choices[0].Body) == 0`\n- **TestReplicatedIf** (line 580): Same pattern\n- **TestCaseStatement** (lines 1020, 1028, 1036): Same pattern\n\n#### 4b: Add new parser tests for multi-statement bodies\n\nAdd tests that verify multiple declarations + process in IF/CASE/WHILE/ALT bodies:\n\n```go\nfunc TestIfWithMultiStatementBody(t *testing.T) {\n input := `IF\n TRUE\n VAL INT max IS 20:\n [max]INT D:\n INT x:\n SEQ\n x := 1\n`\n // Should parse as 1 IF choice with 4 body statements\n // (3 declarations + 1 SEQ block)\n}\n```\n\nSimilarly for CASE, WHILE, and ALT with multi-statement bodies.\n\n#### 4c: Add parser test for channel direction at call site\n\n```go\nfunc TestProcCallWithChannelDirection(t *testing.T) {\n input := `foo(x, out!, in?)\n`\n // Should parse as ProcCall with 3 args: x, out, in\n // The ! and ? are informational and should be consumed/ignored\n}\n```\n\n#### 4d: Codegen tests (`/home/david/projects/code-associates/occam2go/codegen/codegen_test.go`)\n\nThe codegen unit tests use `transpile()` which goes through parser -> codegen. Since the tests use occam source strings, most codegen tests should continue to work unchanged because they compile occam source to Go, and single-statement bodies will just become `[]Statement` with one element.\n\nThe codegen test at line 149 (`TestIfStatement`) should continue to work as-is since it tests the transpiled output string.\n\nAdd new codegen tests:\n- Multi-statement IF body: Verify the generated Go code contains all declarations followed by the process.\n- Channel direction at call site: Verify the generated Go code passes the channel without direction marker.\n\n#### 4e: E2E tests (`/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go`)\n\nAdd new e2e tests:\n\n```go\nfunc TestE2E_IfWithLocalDecls(t *testing.T) {\n occam := `SEQ\n INT result:\n result := 0\n IF\n TRUE\n VAL INT max IS 5:\n INT x:\n SEQ\n x := max * 2\n result := x\n print.int(result)\n`\n output := transpileCompileRun(t, occam)\n expected := \"10\\n\"\n // ...\n}\n\nfunc TestE2E_CaseWithLocalDecls(t *testing.T) {\n occam := `SEQ\n INT x, result:\n x := 2\n result := 0\n CASE x\n 2\n VAL INT factor IS 10:\n INT temp:\n SEQ\n temp := x * factor\n result := temp\n ELSE\n result := 0\n print.int(result)\n`\n output := transpileCompileRun(t, occam)\n expected := \"20\\n\"\n // ...\n}\n\nfunc TestE2E_ChannelDirectionAtCallSite(t *testing.T) {\n occam := `PROC sender(CHAN OF INT out!)\n out ! 42\nPROC receiver(CHAN OF INT in?)\n INT x:\n in ? x\n print.int(x)\nSEQ\n CHAN OF INT c:\n PAR\n sender(c!)\n receiver(c?)\n`\n output := transpileCompileRun(t, occam)\n expected := \"42\\n\"\n // ...\n}\n```\n\n---\n\n### Step 5: Update Documentation (`TODO.md`)\n\nUpdate `TODO.md` to note that multi-statement IF/CASE/ALT/WHILE bodies and channel direction annotations at call sites are now supported.\n\n---\n\n### Implementation Order and Dependencies\n\n1. **AST changes first** (Step 1) -- this will cause compiler errors in parser and codegen\n2. **Parser changes** (Step 2) -- fixes the compiler errors in parser; can run parser tests after\n3. **Codegen changes** (Step 3) -- fixes the compiler errors in codegen; can run all tests after\n4. **Test updates** (Step 4a-4d) -- fix existing broken tests, add new tests\n5. **Run full test suite** -- `go test ./...` to verify everything passes\n6. **Documentation** (Step 5)\n\n### Potential Challenges\n\n1. **DEDENT tracking in `parseBodyStatements()`**: The body block parsing must correctly detect when the body's INDENT/DEDENT pair is complete. The `parseBlockStatements()` pattern uses `startLevel := p.indentLevel` right after consuming INDENT, which correctly tracks this. The new function follows the same approach.\n\n2. **Infinite loop safety**: The new `parseBodyStatements()` includes a progress guard (checking if `curToken` and `peekToken` are unchanged after `parseStatement()`). This prevents any future parsing bugs from causing OOM.\n\n3. **Backward compatibility**: Single-statement bodies (the common case) will now be parsed as `[]Statement` with exactly one element. All existing tests and programs must continue to work. The codegen change from `g.generateStatement(body)` to a loop over body elements handles both cases transparently.\n\n4. **Channel direction at call sites**: The fix in `parseProcCall` checks `peekTokenIs(SEND)` or `peekTokenIs(RECEIVE)` after parsing each argument. This is safe because `!` and `?` cannot appear as the start of a new expression or as a valid token after an argument in any other context within argument lists.\n\n5. **VariantCase consistency**: Leaving `VariantCase.Body` as a single `Statement` while changing the other four types creates an inconsistency. This is acceptable for now, but should be tracked as a follow-up.\n\n---\n\n### Critical Files for Implementation\n\n- `/home/david/projects/code-associates/occam2go/ast/ast.go` - Change Body fields from Statement to []Statement in IfChoice, CaseChoice, AltCase, WhileLoop\n- `/home/david/projects/code-associates/occam2go/parser/parser.go` - Add parseBodyStatements(), update parseIfStatement/parseCaseStatement/parseWhileLoop/parseAltCase to use it, handle channel direction in parseProcCall\n- `/home/david/projects/code-associates/occam2go/codegen/codegen.go` - Update all 30+ locations that access Body on the four types: generators and containsX scanners\n- `/home/david/projects/code-associates/occam2go/parser/parser_test.go` - Fix existing tests (Body nil -> len check), add multi-statement body and channel direction tests\n- `/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go` - Add e2e tests for multi-statement bodies and channel direction at call sites"
},
{
"type": "text",
"text": "agentId: addff78 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 106280\ntool_uses: 30\nduration_ms: 181266</usage>"
}
]Now I have comprehensive information from the exploration and planning agents. Let me review the key findings and write my final plan. Let me also read a few critical files to verify the plan agent's findings before writing the plan.
Let me verify a few key details before finalizing the plan.
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1893,
"limit": 40
}{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 186,
"limit": 60
} 1893→func (p *Parser) parseProcCall() *ast.ProcCall {
1894→ call := &ast.ProcCall{
1895→ Token: p.curToken,
1896→ Name: p.curToken.Literal,
1897→ }
1898→
1899→ if !p.peekTokenIs(lexer.LPAREN) {
1900→ // No arguments
1901→ return call
1902→ }
1903→
1904→ p.nextToken() // consume (
1905→
1906→ if p.peekTokenIs(lexer.RPAREN) {
1907→ p.nextToken() // consume )
1908→ return call
1909→ }
1910→
1911→ p.nextToken() // move to first arg
1912→ call.Args = append(call.Args, p.parseExpression(LOWEST))
1913→
1914→ for p.peekTokenIs(lexer.COMMA) {
1915→ p.nextToken() // consume comma
1916→ p.nextToken() // move to next arg
1917→ call.Args = append(call.Args, p.parseExpression(LOWEST))
1918→ }
1919→
1920→ if !p.expectPeek(lexer.RPAREN) {
1921→ return nil
1922→ }
1923→
1924→ return call
1925→}
1926→
1927→func (p *Parser) parseFuncDecl() *ast.FuncDecl {
1928→ fn := &ast.FuncDecl{
1929→ Token: p.curToken,
1930→ ReturnTypes: []string{p.curToken.Literal},
1931→ }
1932→ 186→func (g *Generator) containsPar(stmt ast.Statement) bool {
187→ switch s := stmt.(type) {
188→ case *ast.ParBlock:
189→ return true
190→ case *ast.SeqBlock:
191→ for _, inner := range s.Statements {
192→ if g.containsPar(inner) {
193→ return true
194→ }
195→ }
196→ case *ast.AltBlock:
197→ for _, c := range s.Cases {
198→ if c.Body != nil && g.containsPar(c.Body) {
199→ return true
200→ }
201→ }
202→ case *ast.ProcDecl:
203→ for _, inner := range s.Body {
204→ if g.containsPar(inner) {
205→ return true
206→ }
207→ }
208→ case *ast.FuncDecl:
209→ for _, inner := range s.Body {
210→ if g.containsPar(inner) {
211→ return true
212→ }
213→ }
214→ case *ast.WhileLoop:
215→ if s.Body != nil && g.containsPar(s.Body) {
216→ return true
217→ }
218→ case *ast.IfStatement:
219→ for _, choice := range s.Choices {
220→ if choice.Body != nil && g.containsPar(choice.Body) {
221→ return true
222→ }
223→ }
224→ case *ast.CaseStatement:
225→ for _, choice := range s.Choices {
226→ if choice.Body != nil && g.containsPar(choice.Body) {
227→ return true
228→ }
229→ }
230→ case *ast.VariantReceive:
231→ for _, c := range s.Cases {
232→ if c.Body != nil && g.containsPar(c.Body) {
233→ return true
234→ }
235→ }
236→ }
237→ return false
238→}
239→
240→func (g *Generator) containsPrint(stmt ast.Statement) bool {
241→ switch s := stmt.(type) {
242→ case *ast.ProcCall:
243→ return printBuiltins[s.Name]
244→ case *ast.SeqBlock:
245→ for _, inner := range s.Statements {{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1496,
"limit": 120
} 1496→func (g *Generator) generateWhileLoop(loop *ast.WhileLoop) {
1497→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1498→ g.write("for ")
1499→ g.generateExpression(loop.Condition)
1500→ g.write(" {\n")
1501→ g.indent++
1502→
1503→ if loop.Body != nil {
1504→ g.generateStatement(loop.Body)
1505→ }
1506→
1507→ g.indent--
1508→ g.writeLine("}")
1509→}
1510→
1511→func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
1512→ if stmt.Replicator != nil {
1513→ // Replicated IF: IF i = start FOR count → for loop with break on first match
1514→ v := stmt.Replicator.Variable
1515→ if stmt.Replicator.Step != nil {
1516→ counter := "_repl_" + v
1517→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1518→ g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
1519→ g.generateExpression(stmt.Replicator.Count)
1520→ g.write(fmt.Sprintf("; %s++ {\n", counter))
1521→ g.indent++
1522→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1523→ g.write(fmt.Sprintf("%s := ", v))
1524→ g.generateExpression(stmt.Replicator.Start)
1525→ g.write(fmt.Sprintf(" + %s * ", counter))
1526→ g.generateExpression(stmt.Replicator.Step)
1527→ g.write("\n")
1528→ } else {
1529→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1530→ g.write(fmt.Sprintf("for %s := ", v))
1531→ g.generateExpression(stmt.Replicator.Start)
1532→ g.write(fmt.Sprintf("; %s < ", v))
1533→ g.generateExpression(stmt.Replicator.Start)
1534→ g.write(" + ")
1535→ g.generateExpression(stmt.Replicator.Count)
1536→ g.write(fmt.Sprintf("; %s++ {\n", v))
1537→ g.indent++
1538→ }
1539→
1540→ for i, choice := range stmt.Choices {
1541→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1542→ if i == 0 {
1543→ g.write("if ")
1544→ } else {
1545→ g.write("} else if ")
1546→ }
1547→ g.generateExpression(choice.Condition)
1548→ g.write(" {\n")
1549→ g.indent++
1550→
1551→ if choice.Body != nil {
1552→ g.generateStatement(choice.Body)
1553→ }
1554→ g.writeLine("break")
1555→
1556→ g.indent--
1557→ }
1558→ g.writeLine("}")
1559→
1560→ g.indent--
1561→ g.writeLine("}")
1562→ } else {
1563→ for i, choice := range stmt.Choices {
1564→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1565→ if i == 0 {
1566→ g.write("if ")
1567→ } else {
1568→ g.write("} else if ")
1569→ }
1570→ g.generateExpression(choice.Condition)
1571→ g.write(" {\n")
1572→ g.indent++
1573→
1574→ if choice.Body != nil {
1575→ g.generateStatement(choice.Body)
1576→ }
1577→
1578→ g.indent--
1579→ }
1580→ g.writeLine("}")
1581→ }
1582→}
1583→
1584→func (g *Generator) generateCaseStatement(stmt *ast.CaseStatement) {
1585→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1586→ g.write("switch ")
1587→ g.generateExpression(stmt.Selector)
1588→ g.write(" {\n")
1589→
1590→ for _, choice := range stmt.Choices {
1591→ if choice.IsElse {
1592→ g.writeLine("default:")
1593→ } else {
1594→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1595→ g.write("case ")
1596→ for i, val := range choice.Values {
1597→ if i > 0 {
1598→ g.write(", ")
1599→ }
1600→ g.generateExpression(val)
1601→ }
1602→ g.write(":\n")
1603→ }
1604→ g.indent++
1605→ if choice.Body != nil {
1606→ g.generateStatement(choice.Body)
1607→ }
1608→ g.indent--
1609→ }
1610→
1611→ g.writeLine("}")
1612→}
1613→
1614→func (g *Generator) generateExpression(expr ast.Expression) {
1615→ switch e := expr.(type) {{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1200,
"limit": 65
} 1200→ g.writeLine("wg.Wait()")
1201→ }
1202→}
1203→
1204→func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
1205→ // ALT becomes Go select statement
1206→ // For guards, we use a pattern with nil channels
1207→
1208→ // Check if any cases have guards
1209→ hasGuards := false
1210→ for _, c := range alt.Cases {
1211→ if c.Guard != nil {
1212→ hasGuards = true
1213→ break
1214→ }
1215→ }
1216→
1217→ if hasGuards {
1218→ // Generate channel variables for guarded cases
1219→ for i, c := range alt.Cases {
1220→ if c.Guard != nil {
1221→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1222→ g.write(fmt.Sprintf("var _alt%d chan ", i))
1223→ // We don't know the channel type here, so use interface{}
1224→ // Actually, we should use the same type as the original channel
1225→ // For now, let's just reference the original channel conditionally
1226→ g.write(fmt.Sprintf("int = nil\n")) // Assuming int for now
1227→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1228→ g.write(fmt.Sprintf("if "))
1229→ g.generateExpression(c.Guard)
1230→ g.write(fmt.Sprintf(" { _alt%d = %s }\n", i, c.Channel))
1231→ }
1232→ }
1233→ }
1234→
1235→ g.writeLine("select {")
1236→ for i, c := range alt.Cases {
1237→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1238→ if c.IsTimer {
1239→ g.write("case <-time.After(time.Duration(")
1240→ g.generateExpression(c.Deadline)
1241→ g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
1242→ } else if c.Guard != nil {
1243→ g.write(fmt.Sprintf("case %s = <-_alt%d:\n", c.Variable, i))
1244→ } else if c.ChannelIndex != nil {
1245→ g.write(fmt.Sprintf("case %s = <-%s[", c.Variable, c.Channel))
1246→ g.generateExpression(c.ChannelIndex)
1247→ g.write("]:\n")
1248→ } else {
1249→ g.write(fmt.Sprintf("case %s = <-%s:\n", c.Variable, c.Channel))
1250→ }
1251→ g.indent++
1252→ if c.Body != nil {
1253→ g.generateStatement(c.Body)
1254→ }
1255→ g.indent--
1256→ }
1257→ g.writeLine("}")
1258→}
1259→
1260→func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
1261→ // Track reference parameters for this procedure
1262→ oldRefParams := g.refParams
1263→ newRefParams := make(map[string]bool)
1264→ // Inherit parent's ref params for closure captures when nestedAPI Error: 500 {"type":"error","error":{"type":"api_error","message":"Internal server error"},"request_id":"req_011CYGqkNxGw2tcV69Xcp1E2"}