Claude Code transcript - page 1/1

Please take another look at issue #72 in light of all the blocking issues having now been fixed.

Thinking

The user wants me to look at issue #72 on GitHub. Let me fetch it.

$ Bash
View GitHub issue #72
gh issue view 72
title:	Investigate transpiler compatibility with example program from the Occam2 book
state:	OPEN
author:	dboreham
labels:	
comments:	1
assignees:	
projects:	
milestone:	
number:	72
--
A code example from the book "Programming in Occam2" has been added to this repo here: ./historical-examples/life.occ

Please analyze this program to determine why it doesn't parse and run with the current version of the transpiler.
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/historical-examples/life.occ"
}
     1→--  Code copied from Programming in occam®2
     2→--  © Geraint Jones, Michael Goldsmith 1988, 2001.
     3→--  Permission is granted to copy this material for private study; for other uses please contact occam-book@comlab.ox.ac.uk
     4→--
     5→--  The program in this chapter plays Life on a terminal screen.
     6→--
     7→
     8→--
     9→--  configuration constants
    10→--
    11→
    12→VAL INT array.width  IS 20 :   -- number of cells across the board
    13→VAL INT array.height IS 20 :   -- number of cells down the board
    14→
    15→VAL INT radius     IS 1 :       -- of the `sphere of influence'
    16→VAL INT diameter   IS (2 * radius) + 1 :
    17→VAL INT neighbours IS (diameter * diameter) - 1 :
    18→
    19→VAL INT number.of.cells IS array.height * array.width :
    20→VAL INT number.of.links IS neighbours * number.of.cells :
    21→
    22→--
    23→--  protocols
    24→--
    25→
    26→PROTOCOL STATE IS BOOL :
    27→
    28→VAL BOOL alive IS TRUE :
    29→VAL BOOL dead  IS NOT alive :
    30→
    31→PROTOCOL COMMAND
    32→  CASE
    33→    set.state; BOOL
    34→    evolve
    35→    terminate
    36→:
    37→
    38→PROTOCOL RESPONSE IS BOOL; BOOL :
    39→--
    40→--  cell processes
    41→--
    42→
    43→PROC broadcast.present.state([][][]CHAN OF STATE link,
    44→                             VAL INT x, y, VAL BOOL state )
    45→  PAR d = 0 FOR neighbours
    46→    link[x][y][d] ! state
    47→:
    48→
    49→PROC calculate.next.state([][][]CHAN OF STATE link,
    50→                          VAL []INT nx, ny,
    51→                          VAL BOOL state, BOOL next.state )
    52→  INT count :       -- number of living neighbours
    53→  SEQ
    54→    [neighbours]BOOL state.of.neighbour :
    55→    SEQ
    56→      PAR d = 0 FOR neighbours
    57→        link[nx[d]][ny[d]][d] ? state.of.neighbour[d]
    58→      count := 0
    59→      SEQ d = 0 FOR neighbours
    60→        IF
    61→          state.of.neighbour[d] = alive
    62→            count := count + 1
    63→          state.of.neighbour[d] = dead
    64→            SKIP
    65→    IF
    66→      count < 2     -- death from isolation
    67→        next.state := dead
    68→      count = 2     -- this cell is stable
    69→        next.state := state
    70→      count = 3     -- stable if alive, a birth if dead
    71→        next.state := alive
    72→      count > 3     -- death from overcrowding
    73→        next.state := dead
    74→:
    75→
    76→
    77→
    78→
    79→
    80→
    81→
    82→
    83→
    84→
    85→
    86→
    87→
    88→
    89→
    90→PROC cell([][][]CHAN OF STATE link,
    91→          VAL INT x, y, VAL []INT nx, ny,
    92→          CHAN OF COMMAND control,
    93→          CHAN OF RESPONSE sense         )
    94→  BOOL state, not.finished :
    95→  SEQ
    96→    state := dead       -- the whole board starts off dead
    97→    not.finished := TRUE
    98→    WHILE not.finished
    99→      control ? CASE
   100→
   101→        set.state; state
   102→          SKIP          -- state has been set to the new value
   103→
   104→        evolve
   105→          BOOL next.state :
   106→          SEQ
   107→            PAR
   108→              broadcast.present.state(link, x, y, state)
   109→              SEQ
   110→                calculate.next.state(link, nx, ny,
   111→                                     state, next.state )
   112→                sense ! (state <> next.state); next.state
   113→            state := next.state
   114→
   115→        terminate
   116→          not.finished := FALSE
   117→:
   118→
   119→--
   120→--  terminal-dependent output routines
   121→--
   122→
   123→PROC clear.screen(CHAN OF BYTE terminal)
   124→  -- clear screen sequence for an ANSI terminal
   125→  write.string(terminal, "*#1B[2J")
   126→:
   127→
   128→PROC move.cursor(CHAN OF BYTE terminal, VAL INT x, y)
   129→  -- left-handed co-ordinates, origin 0,0 at top left
   130→  CHAN OF DATA.ITEM c :
   131→  PAR
   132→    write.formatted(terminal, "*#1B[%d;%dH", c)
   133→    SEQ
   134→      c ! data.int; y + 1
   135→      c ! data.int; x + 1
   136→:
   137→
   138→
   139→
   140→
   141→--
   142→--  display routines
   143→--
   144→
   145→PROC initialize.display(CHAN OF BYTE screen)
   146→  -- display an entirely dead board
   147→  clear.screen(screen)
   148→:
   149→
   150→PROC clean.up.display(CHAN OF BYTE screen)
   151→  move.cursor(screen, 0, array.height)
   152→:
   153→
   154→PROC display.state(CHAN OF BYTE screen, VAL INT x, y, VAL BOOL state)
   155→  SEQ
   156→    move.cursor(screen, x, y)
   157→    IF
   158→      state = alive
   159→        screen ! '**'
   160→      state = dead
   161→        screen ! '*s'
   162→:
   163→
   164→--
   165→--  controller states
   166→--
   167→
   168→VAL INT idle         IS 0 :  -- controller activity values
   169→VAL INT editing      IS 1 :
   170→VAL INT single.step  IS 2 :
   171→VAL INT free.running IS 3 :
   172→VAL INT terminated   IS 4 :
   173→
   174→
   175→INT FUNCTION new.activity(VAL BYTE char)
   176→  INT activity :
   177→  VALOF
   178→    CASE char     -- typed on the keyboard ...
   179→      'q', 'Q'              -- ... Q to finish program
   180→        activity := terminated
   181→      's', 'S'              -- ... S to halt evolution
   182→        activity := idle
   183→      'e', 'E'              -- ... E to start editing
   184→        activity := editing
   185→      'r', 'R'              -- ... R to start evolution
   186→        activity := free.running
   187→      ELSE  -- ... or anything else for one generation
   188→        activity := single.step
   189→    RESULT activity
   190→:
   191→
   192→PROC display.activity(CHAN OF BYTE screen, VAL INT activity)
   193→  SEQ
   194→    move.cursor(screen, array.width+1, array.height/2)
   195→    CASE activity
   196→      idle
   197→        write.string(screen, "Idle")
   198→      editing
   199→        write.string(screen, "Edit")
   200→      single.step
   201→        write.string(screen, "Step")
   202→      free.running
   203→        write.string(screen, "Busy")
   204→      terminated
   205→        write.string(screen, "Done")
   206→:
   207→
   208→--
   209→--  generation
   210→--
   211→
   212→PROC generation(CHAN OF BYTE screen,
   213→                [][]CHAN OF COMMAND control,
   214→                [][]CHAN OF RESPONSE sense,
   215→                BOOL active                 )
   216→  SEQ
   217→    PAR x = 0 FOR array.width
   218→      PAR y = 0 FOR array.height
   219→        control[x][y] ! evolve
   220→    active := FALSE
   221→    SEQ x = 0 FOR array.width
   222→      SEQ y = 0 FOR array.height
   223→        BOOL changed, next.state :
   224→        SEQ
   225→          sense[x][y] ? changed; next.state
   226→          IF
   227→            changed
   228→              SEQ
   229→                display.state(screen, x, y, next.state)
   230→                active := TRUE
   231→            NOT changed
   232→              SKIP
   233→:
   234→
   235→
   236→
   237→
   238→
   239→
   240→
   241→
   242→
   243→--
   244→--  editor
   245→--
   246→
   247→INT FUNCTION min(VAL INT a, b)
   248→  INT min :
   249→  VALOF
   250→    IF
   251→      a <= b
   252→        min := a
   253→      b <= a
   254→        min := b
   255→    RESULT min
   256→:
   257→
   258→INT FUNCTION max(VAL INT a, b)
   259→  INT max :
   260→  VALOF
   261→    IF
   262→      a >= b
   263→        max := a
   264→      b >= a
   265→        max := b
   266→    RESULT max
   267→:
   268→
   269→
   270→
   271→
   272→
   273→
   274→
   275→
   276→
   277→
   278→
   279→
   280→
   281→
   282→
   283→
   284→
   285→
   286→
   287→
   288→
   289→
   290→
   291→
   292→
   293→
   294→PROC editor(CHAN OF BYTE keyboard, screen,
   295→            [][]CHAN OF COMMAND control   )
   296→  INT x, y :
   297→  BOOL editing :
   298→  SEQ
   299→    -- initialize co-ordinates to centre of board
   300→    x, y := array.width / 2, array.height / 2
   301→    editing := TRUE
   302→    WHILE editing
   303→      BYTE char :
   304→      SEQ
   305→        move.cursor(screen, x, y)
   306→        keyboard ? char
   307→        CASE char
   308→          'A'        -- move up, if possible
   309→            y := max(y - 1, 0)
   310→          'B'        -- move down, if possible
   311→            y := min(y + 1, array.height - 1)
   312→          'C'        -- move right, if possible
   313→            x := min(x + 1, array.width - 1)
   314→          'D'        -- move left, if possible
   315→            x := max(x - 1, 0)
   316→          '*s', '**'
   317→            VAL BOOL state IS (char = '**') = alive :
   318→            PAR
   319→              control[x][y] ! set.state; state
   320→              display.state(screen, x, y, state)
   321→          'q', 'Q'
   322→            editing := FALSE
   323→          ELSE
   324→            SKIP     -- ignore anything else
   325→:
   326→
   327→
   328→
   329→
   330→
   331→
   332→
   333→
   334→
   335→
   336→
   337→
   338→
   339→
   340→
   341→
   342→
   343→
   344→
   345→--
   346→--  controller
   347→--
   348→
   349→PROC controller(CHAN OF BYTE keyboard, screen,
   350→                [][]CHAN OF COMMAND control,
   351→                [][]CHAN OF RESPONSE sense    )
   352→  INT activity :
   353→  SEQ
   354→    activity := idle
   355→    initialize.display(screen)
   356→    WHILE activity <> terminated
   357→      SEQ
   358→        display.activity(screen, activity)
   359→        BYTE char :
   360→        PRI ALT
   361→          (activity <> editing) & keyboard ? char
   362→            activity := new.activity(char)
   363→          (activity <> idle) & SKIP
   364→            CASE activity
   365→              editing
   366→                SEQ
   367→                  editor(keyboard, screen, control)
   368→                  activity := idle
   369→              free.running, single.step
   370→                BOOL changing :
   371→                SEQ
   372→                  generation(screen, control, sense, changing)
   373→                  IF
   374→                    (activity = single.step) OR (NOT changing)
   375→                      activity := idle
   376→                    (activity = free.running) AND changing
   377→                      SKIP
   378→    display.activity(screen, activity)
   379→    PAR x = 0 FOR array.width
   380→      PAR y = 0 FOR array.height
   381→        control[x][y] ! terminate
   382→    clean.up.display(screen)
   383→:
   384→
   385→
   386→
   387→
   388→
   389→
   390→
   391→
   392→
   393→
   394→
   395→
   396→--
   397→--  structure of the program
   398→--
   399→
   400→[array.width][array.height][neighbours]CHAN OF STATE link :
   401→[array.width][array.height]CHAN OF COMMAND control :
   402→[array.width][array.height]CHAN OF RESPONSE sense :
   403→PAR
   404→  controller(terminal.keyboard, terminal.screen, control, sense)
   405→  PAR x = 0 FOR array.width
   406→    PAR y = 0 FOR array.height
   407→      VAL INT left  IS ((x - 1) + array.width)  \ array.width  :
   408→      VAL INT right IS  (x + 1)                 \ array.width  :
   409→      VAL INT up    IS  (y + 1)                 \ array.height :
   410→      VAL INT down  IS ((y - 1) + array.height) \ array.height :
   411→      VAL [neighbours]INT nx IS
   412→            [ right, x,    left, left, left, x,  right, right ] :
   413→      VAL [neighbours]INT ny IS
   414→            [ down,  down, down, y,    up,   up, up,    y     ] :
   415→      cell(link, x, y, nx, ny, control[x][y], sense[x][y])
   416→
   417→
$ Bash
View comments on issue #72
gh issue view 72 --comments
author:	associate-1
association:	member
edited:	false
status:	none
--
Analysis complete — see the detailed findings in [this comment on PR #71](https://github.com/codeassociates/occam2go/pull/71#issuecomment-3945869818).

Seven unsupported features were identified and filed as individual issues: #73, #74, #75, #76, #77, #78, #79.
--
Thinking

Let me check the status of all those blocking issues (#73-#79) and then try to transpile the file to see what happens now.

Let me check the status of those blocking issues and try transpiling the file.

$ Bash
Check status of blocking issues #73-#79
gh issue list --state all --search "73 74 75 76 77 78 79" --json number,title,state --jq '.[] | select(.number >= 73 and .number <= 79) | "\(.number) [\(.state)] \(.title)"'
73 [CLOSED] Support protocol declaration trailing colon terminator
75 [CLOSED] Support CASE with comma-separated match values and ELSE
78 [CLOSED] Support parenthesized guard expressions in ALT
79 [CLOSED] Support multi-line abbreviations (IS as line continuation)
76 [CLOSED] Support PRI ALT (priority alternation)
77 [CLOSED] Support SKIP guard in ALT (condition & SKIP)
74 [OPEN] Support multi-dimensional arrays (declaration, parameters, indexing)
$ Bash
Build transpiler and try transpiling life.occ
go build -o occam2go . && ./occam2go -o /tmp/life.go historical-examples/life.occ 2>&1
Exit code 1
Parse errors:
  historical-examples/life.occ:58: expected type after array size, got NEWLINE
  historical-examples/life.occ:106: expected variant tag name, got SEQ
  historical-examples/life.occ:107: expected variant tag name, got INDENT
  historical-examples/life.occ:107: expected variant tag name, got PAR
  historical-examples/life.occ:108: expected variant tag name, got INDENT
  historical-examples/life.occ:108: expected variant tag name, got (
  historical-examples/life.occ:108: expected variant tag name, got ,
  historical-examples/life.occ:108: expected variant tag name, got ,
  historical-examples/life.occ:108: expected variant tag name, got ,
  historical-examples/life.occ:108: expected variant tag name, got )
  historical-examples/life.occ:109: expected variant tag name, got SEQ
  historical-examples/life.occ:110: expected variant tag name, got INDENT
  historical-examples/life.occ:110: expected variant tag name, got (
  historical-examples/life.occ:110: expected variant tag name, got ,
  historical-examples/life.occ:110: expected variant tag name, got ,
  historical-examples/life.occ:110: expected variant tag name, got ,
  historical-examples/life.occ:111: expected variant tag name, got ,
  historical-examples/life.occ:111: expected variant tag name, got )
  historical-examples/life.occ:112: expected variant tag name, got !
  historical-examples/life.occ:112: expected variant tag name, got (
  historical-examples/life.occ:112: expected variant tag name, got <>
  historical-examples/life.occ:112: expected variant tag name, got )
  historical-examples/life.occ:112: expected variant tag name, got ;
  historical-examples/life.occ:113: expected variant tag name, got :=

Parse errors:
  historical-examples/life.occ:58: expected type after array size, got NEWLINE
  historical-examples/life.occ:106: expected variant tag name, got SEQ
  historical-examples/life.occ:107: expected variant tag name, got INDENT
  historical-examples/life.occ:107: expected variant tag name, got PAR
  historical-examples/life.occ:108: expected variant tag name, got INDENT
  historical-examples/life.occ:108: expected variant tag name, got (
  historical-examples/life.occ:108: expected variant tag name, got ,
  historical-examples/life.occ:108: expected variant tag name, got ,
  historical-examples/life.occ:108: expected variant tag name, got ,
  historical-examples/life.occ:108: expected variant tag name, got )
  historical-examples/life.occ:109: expected variant tag name, got SEQ
  historical-examples/life.occ:110: expected variant tag name, got INDENT
  historical-examples/life.occ:110: expected variant tag name, got (
  historical-examples/life.occ:110: expected variant tag name, got ,
  historical-examples/life.occ:110: expected variant tag name, got ,
  historical-examples/life.occ:110: expected variant tag name, got ,
  historical-examples/life.occ:111: expected variant tag name, got ,
  historical-examples/life.occ:111: expected variant tag name, got )
  historical-examples/life.occ:112: expected variant tag name, got !
  historical-examples/life.occ:112: expected variant tag name, got (
  historical-examples/life.occ:112: expected variant tag name, got <>
  historical-examples/life.occ:112: expected variant tag name, got )
  historical-examples/life.occ:112: expected variant tag name, got ;
  historical-examples/life.occ:113: expected variant tag name, got :=
Thinking

So there are two main issues remaining:

  1. Line 58: [neighbours]BOOL state.of.neighbour : — this is an array declaration inside a SEQ block. The error says "expected type after array size, got NEWLINE". Let me look at this more carefully. Actually looking at line 54: [neighbours]BOOL state.of.neighbour : — this is an array declaration where the size is a named constant (not an integer literal). The parser might be having trouble with this.

Wait, actually line 54 is:

    [neighbours]BOOL state.of.neighbour :

And the error is at line 58 which is:

      count := 0

Hmm, let me re-read the error: historical-examples/life.occ:58: expected type after array size, got NEWLINE.

Looking at the actual code structure around those lines, I see the array declaration [neighbours]BOOL state.of.neighbour : is followed by a SEQ block that contains a PAR loop reading from links, then the count assignment. The parser error about expecting a type after the array size suggests the issue is with how the array is being declared in this nested context.

Actually, I think the real problem might be with multi-dimensional arrays — the program uses 3D arrays like [array.width][array.height][neighbours]CHAN OF STATE link : elsewhere, and issue #74 shows that's still an open problem. Even though this particular line is just a 1D array, the parser might be getting confused by the overall structure. The line numbers seem to be misaligned between the error message and the actual source—the parser is reporting an error at line 58 for count := 0, but that doesn't match the syntax error description of "expected type after array size." This suggests the real problem is earlier in the file, possibly with how the array declaration is structured, and the error is cascading to a later line.

Looking at line 400 with the 3D channel array declaration, I'm wondering if the parser doesn't properly support multi-dimensional arrays, which would explain why it's getting confused about the array syntax and reporting errors at unexpected locations. Looking at the actual parse errors rather than the issue tracker—there are two distinct problems here. The first error about "expected type after array size, got NEWLINE" suggests a syntax issue with how arrays are being declared, possibly related to multi-dimensional arrays. The second cluster of errors around line 106 is about variant protocol cases that have multiple statements in their bodies. Let me focus on what the parser is actually complaining about rather than what the documentation claims is supported. I need to trace through the parser's behavior more carefully. The issue is that when it encounters control ? CASE, it expects variant tag names, but the preprocessor output is showing SEQ where a tag should be. Let me systematically work through what's happening: the evolve case has a declaration BOOL next.state : followed by a body with SEQ, and the parser is getting confused about where the actual statements begin versus where the tag should be.

Six of the seven blocking issues are closed, with only #74 (multi-dimensional arrays) still open. Let me analyze the remaining parse errors systematically.

$ Bash
View details of open issue #74
gh issue view 74
title:	Support multi-dimensional arrays (declaration, parameters, indexing)
state:	OPEN
author:	associate-1
labels:	
comments:	1
assignees:	
projects:	
milestone:	
number:	74
--
## Description

The transpiler only supports single-dimensional arrays. The Life example requires multi-dimensional arrays for its cell grid and channel network.

## Occam2 syntax

**Declarations:**
```occam
[array.width][array.height][neighbours]CHAN OF STATE link :
[array.width][array.height]CHAN OF COMMAND control :
```

**Proc parameters:**
```occam
PROC broadcast.present.state([][][]CHAN OF STATE link, ...)
PROC generation([][]CHAN OF COMMAND control, [][]CHAN OF RESPONSE sense, ...)
```

**Indexing (chained):**
```occam
link[x][y][d] ! state
link[nx[d]][ny[d]][d] ? state.of.neighbour[d]
control[x][y] ! evolve
sense[x][y] ? changed; next.state
```

## Expected Go output

```go
// Declarations — nested slices with init loops
link := make([][][]chan bool, array_width)
for i := range link {
    link[i] = make([][]chan bool, array_height)
    for j := range link[i] {
        link[i][j] = make([]chan bool, neighbours)
        for k := range link[i][j] {
            link[i][j][k] = make(chan bool)
        }
    }
}

// Parameters
func broadcast_present_state(link [][][]chan bool, ...)

// Indexing
link[x][y][d] <- state
control[x][y] <- _proto_COMMAND_evolve{}
```

## Current behavior

```
expected type after [], got [
expected :=, !, or ? after link[...], got [
```

## Scope

This is the largest feature gap in life.occ, requiring changes across:
- **Parser**: multi-dimensional array declarations, multi-dimensional open/fixed proc params, chained index expressions for send/receive/assignment
- **AST**: extend array-related nodes to support multiple dimensions
- **Codegen**: nested slice initialization, multi-level indexing in expressions

## Context

Found in `historical-examples/life.occ` (PR #71). This is the single most pervasive error source, appearing at lines 43, 46, 49, 57, 90, 213, 219, 225, 295, 319, 350, 381, 400-402, 415.
Task
Analyze life.occ parse errors
{
  "subagent_type": "Explore",
  "prompt": "I'm trying to understand why the occam2go transpiler fails to parse `historical-examples/life.occ`. The errors are:\n\n1. `life.occ:58: expected type after array size, got NEWLINE`\n2. `life.occ:106+: expected variant tag name, got SEQ` (and many more)\n\nI need to understand what features in life.occ are not yet supported. Look at:\n\n1. The file `historical-examples/life.occ` — especially around lines 43-74 (3D array params like `[][][]CHAN OF STATE`), lines 90-117 (variant protocol `control ? CASE` with multi-statement bodies), and lines 400-415 (3D array declarations like `[array.width][array.height][neighbours]CHAN OF STATE`)\n\n2. The parser code `parser/parser.go` — search for how array declarations are parsed (look for parseVarDecl or similar), how many dimensions are supported, and how variant protocol receive (`? CASE`) is parsed.\n\n3. The codegen code `codegen/codegen.go` — search for how multi-dimensional arrays and variant protocol receives are generated.\n\nKey questions:\n- Does the parser support 3D arrays (both declarations and parameters)? The CLAUDE.md mentions 2D support.\n- Does the parser support variant protocol channel receive (`chan ? CASE tag1; body ...`)?\n- Are there any other unsupported features in life.occ?\n\nAlso check what's currently in `TODO.md` about these features.\n\nBe thorough — search for \"3\" dimensional, triple bracket patterns, variant receive, and CASE after `?` in the parser."
}
[
  {
    "type": "text",
    "text": "Aha! I found the bug! Look at line 174-175:\n\n```go\ncase lexer.LBRACKET:\n    return p.parseArrayDecl()\n```\n\nWhen parseStatement sees an `LBRACKET`, it **always** treats it as an array declaration. But `LBRACKET` can also start an array literal or, in this case, an indexed operation like `link[5]` which is a channel name followed by indices.\n\nThe issue is that when parsing `link[5] ? state.of.neighbour[0]` inside a PAR block, the parser encounters `[` and mistakenly thinks it's starting an array declaration instead of recognizing that it's part of an indexed channel receive.\n\nThe fix would be to check if the `[` is preceded by an identifier (channel/array name), and if so, treat it as an indexed operation instead of an array declaration. But currently, in parseStatement, the parser only checks the current token type, not what came before.\n\nNow let me write up my findings. Let me compile all the information I've gathered:\n\n## Summary\n\nI've identified **two major unsupported features** in `historical-examples/life.occ`:\n\n### **1. Multi-indexed Channel Receive (PRIMARY ISSUE)**\n\nThe parser incorrectly treats `link[i][j][k] ? var` and similar multi-indexed receives as array declarations.\n\n**Problem:** In `parseStatement()` (line 174-175), when the parser sees a `[` token, it always calls `parseArrayDecl()`. This fails when the `[` is actually part of an indexed channel operation like:\n```occam\nlink[nx[d]][ny[d]][d] ? state.of.neighbour[d]\n```\n\n**Location:** Line 57 of life.occ (inside PAR block within procedure `calculate.next.state`)\n- Error message: `expected type after array size, got NEWLINE` (line 58 reported)\n- Parser sees `link[nx[d]]` and tries to parse it as array declaration `[nx[d]]BOOL ...` instead of channel index + receive\n\n**Why it fails:** The `parseArrayDecl()` function expects: `[size]TYPE var :` but instead encounters `[index]` followed by `[` again or `?` (receive operator).\n\n**Tests confirming the bug:**\n```bash\n# Works (no indexing):\nlink[5] ? x\n\n# Fails (indexed receive):\nPAR\n  link[5] ? state.of.neighbour[0]\n  \n# Fails (2D indexing):\nPAR\n  link[0][d] ? state.of.neighbour[d]\n  \n# Fails (3D indexing):\nPAR\n  link[x][y][d] ? state.of.neighbour[d]\n```\n\n**Files affected:**\n- Line 57: `link[nx[d]][ny[d]][d] ? state.of.neighbour[d]` (calculate.next.state proc)\n- Plus cascading errors in variant protocol receive parsing (see issue #2)\n\n### **2. Variant Protocol with Multi-Statement Bodies**\n\nThe parser fails to correctly parse variant channel receive (`? CASE`) when the case body contains multiple statements like `SEQ`, `PAR`, or declarations.\n\n**Problem:** In `parseVariantReceive()` (lines 1308-1407), when parsing a variant case body, the parser calls `p.parseStatement()` which expects a single statement. However, occam allows multi-statement bodies that may span multiple lines with indentation.\n\n**Example from life.occ (lines 99-117):**\n```occam\ncontrol ? CASE\n  set.state; state\n    SKIP\n  \n  evolve\n    BOOL next.state :\n    SEQ\n      PAR\n        broadcast.present.state(...)\n        SEQ\n          calculate.next.state(...)\n          sense ! (state <> next.state); next.state\n      state := next.state\n  \n  terminate\n    not.finished := FALSE\n```\n\n**Error cascade:** Line 106 onwards, parser reports \"expected variant tag name, got SEQ\" because it encounters the `SEQ` multi-statement body and doesn't know how to handle it.\n\n**Root cause:** The variant receive parser expects: `tag ; var \\n INDENT statement` but receives: `tag \\n INDENT BOOL next.state : \\n INDENT SEQ ...`\n\nThe parser also doesn't handle the optional variable declaration pattern like:\n```occam\nevolve\n  BOOL next.state :\n  (multi-statement body)\n```\n\n### **3. Open Proc Parameters with Fixed-Size Dimensions (NOT AN ISSUE)**\n\nThe parser **DOES** correctly support 3D and higher-dimensional array parameters with `[][]...` syntax. Testing confirms:\n```occam\nPROC test([][][]CHAN OF STATE link)  ✓ Works\n```\n\nThe issue is only with:\n1. Indexed receives of multi-dimensional arrays (issue #1)\n2. Variant protocol receives with complex bodies (issue #2)\n\n### **Current TODO Status**\n\nFrom `TODO.md` (lines 85-93), variant protocol `CASE` receive is NOT listed as unimplemented. However, checking the code shows it's only partially implemented:\n- Simple variant receive works: `c ? CASE tag; x -> SKIP`\n- **Missing:** Multi-statement bodies and declarations in case bodies\n\nNo mention of indexed multi-dimensional channel receives being problematic.\n\n### **Expected Features Failing**\n\nThe transpiler needs to support:\n1. **Indexed multi-dimensional channel receives** in all contexts (PAR, SEQ, etc.)\n   - Detect when `[` starts an indexed operation (preceded by identifier) vs. array declaration\n   - Parse multi-level indices like `link[i][j][k]`\n   - Correctly handle in receive operations: `link[i][j][k] ? var`\n\n2. **Variant protocol receive with complex bodies**\n   - Allow optional variable declarations in variant case: `tag ; var : body`\n   - Support multi-statement bodies (SEQ, PAR, etc.) in variant cases\n   - Handle proper indentation/DEDENT tracking for nested structures"
  },
  {
    "type": "text",
    "text": "agentId: a25a29457becad186 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 57100\ntool_uses: 52\nduration_ms: 252868</usage>"
  }
]
Thinking

Great, now I have a good understanding of the issues. Let me also look at what life.occ needs beyond what's currently supported. The agent identified two major issues:

  1. Multi-dimensional array declarations, indexing, and receives — issue #74 is still open. But wait, let me check the CLAUDE.md again:

From CLAUDE.md:

| `[w][h]CHAN OF INT cs:` | `cs := make([][]chan int, w)` + nested init loops |
| `[3][4]INT grid:` | `grid := make([][]int, 3)` + nested init loops |


So 2D is implemented. But life.occ needs 3D arrays. And also the parser might have issues with indexed receives in certain contexts.

  1. Variant protocol receive with multi-statement bodies — the control ? CASE construct.

Let me also check the TODO.md to understand what's listed.

But actually, let me think about what the user is asking. They want me to "take another look at issue #72 in light of all the blocking issues having now been fixed." So they think all blocking issues are fixed, but #74 is still open. Let me verify by checking which issues are actually blocking.

Wait, let me re-read the issue list: - #73 [CLOSED] - protocol trailing colon - #74 [OPEN] - multi-dimensional arrays - #75 [CLOSED] - CASE with comma-separated match values and ELSE - #76 [CLOSED] - PRI ALT - #77 [CLOSED] - SKIP guard in ALT - #78 [CLOSED] - parenthesized guard expressions - #79 [CLOSED] - multi-line abbreviations

So #74 is still open. But the CLAUDE.md says 2D arrays are supported, which seems to contradict the open status. Let me look more carefully at what's actually blocking #72.

The agent research found that:
1. The first error at line 58 is about an indexed channel receive inside a PAR block — `link[nx[d]][ny[d]][d] ? state.of.neighbour[d]`. The parser misinterprets the `[` as starting an array declaration.
2. The second cluster of errors (line 106+) is about variant protocol receive with multi-statement bodies.

So the real issue isn't just multi-dimensional arrays in general — it's specifically about how the parser handles indexed channel receives when they appear inside PAR blocks. The parser is getting confused by the bracket syntax and treating it as an array declaration rather than an array access operation. I'm looking at the actual structure of the code around the error location — the array declaration uses a named constant neighbours as its size, and the parser might be struggling with that syntax rather than the assignment on line 58. I'm recounting the actual line numbers from the source file to match them against the error message. Looking at lines 52-58, the error at line 58 "expected type after array size" doesn't seem to correspond to count := — it's pointing somewhere else, which suggests the line numbers in the error might be from preprocessed output rather than the original source.

The real issue is probably earlier on line 54 with [neighbours]BOOL state.of.neighbour : — the parser is failing there and then cascading into confusion on subsequent lines, misinterpreting what comes next.

Looking at the documentation, it shows that identifier-based array sizes work for channels like [w][h]CHAN OF INT cs:, so they should work for regular arrays too. But maybe the parser only supports this syntax for certain types, or there's something specific about how BOOL arrays with named sizes are being handled that's causing the failure.

Let me trace through what's actually happening in the parser when it encounters that array declaration, because the error message about expecting a type after the array size suggests the parser might be getting confused about where the size ends and the type begins. The user wants me to revisit issue #72 now that the blocking issues are resolved, so I should run some targeted tests to identify exactly what's still broken and create a clear summary of the remaining work needed.

Let me run some targeted tests to pin down exactly what's failing.

$ Bash
Test 3D channel array declaration
cat > /tmp/test_3d_decl.occ << 'EOF'
VAL INT W IS 3 :
VAL INT H IS 3 :
VAL INT N IS 8 :
[W][H][N]CHAN OF INT link :
SEQ
  link[0][0][0] ! 42
EOF
./occam2go -o /tmp/test_3d_decl.go /tmp/test_3d_decl.occ 2>&1
$ Bash
Test 3D open array param
cat > /tmp/test_3d_param.occ << 'EOF'
PROC test([][][]CHAN OF INT link, VAL INT x, y)
  link[x][y][0] ! 42
:
SEQ
  SKIP
EOF
./occam2go -o /tmp/test_3d_param.go /tmp/test_3d_param.occ 2>&1
$ Bash
Test named-constant array size in body
cat > /tmp/test_named_array.occ << 'EOF'
VAL INT N IS 8 :
PROC test(VAL INT x)
  INT count :
  SEQ
    [N]BOOL flags :
    SEQ
      count := 0
:
SEQ
  test(1)
EOF
./occam2go -o /tmp/test_named_array.go /tmp/test_named_array.occ 2>&1
$ Bash
Test variant protocol receive (simple)
cat > /tmp/test_variant_recv.occ << 'EOF'
PROTOCOL CMD
  CASE
    go
    stop
:
PROC test(CHAN OF CMD c)
  BOOL running :
  SEQ
    running := TRUE
    WHILE running
      c ? CASE
        go
          SKIP
        stop
          running := FALSE
:
CHAN OF CMD ch :
PAR
  test(ch)
  ch ! go
EOF
./occam2go -o /tmp/test_variant_recv.go /tmp/test_variant_recv.occ 2>&1
Thinking

Good - 3D declarations, 3D open array params, named constant array sizes, and simple variant protocol receive all work. Now let me test the more complex cases from life.occ.

Good — 3D arrays, named-constant sizes, and simple variant receive all work. Let me test the harder cases from life.occ.

$ Bash
Test variant receive with multi-statement bodies
cat > /tmp/test_variant_complex.occ << 'EOF'
PROTOCOL CMD
  CASE
    set.state; BOOL
    evolve
    terminate
:
PROC test(CHAN OF CMD control)
  BOOL state, not.finished :
  SEQ
    state := FALSE
    not.finished := TRUE
    WHILE not.finished
      control ? CASE
        set.state; state
          SKIP
        evolve
          BOOL next.state :
          SEQ
            next.state := TRUE
            state := next.state
        terminate
          not.finished := FALSE
:
CHAN OF CMD ch :
PAR
  test(ch)
  SEQ
    ch ! set.state; TRUE
    ch ! evolve
    ch ! terminate
EOF
./occam2go -o /tmp/test_variant_complex.go /tmp/test_variant_complex.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_variant_complex.occ:18: expected variant tag name, got SEQ
  /tmp/test_variant_complex.occ:19: expected variant tag name, got INDENT
  /tmp/test_variant_complex.occ:19: expected variant tag name, got :=
  /tmp/test_variant_complex.occ:19: expected variant tag name, got TRUE
  /tmp/test_variant_complex.occ:20: expected variant tag name, got :=

Parse errors:
  /tmp/test_variant_complex.occ:18: expected variant tag name, got SEQ
  /tmp/test_variant_complex.occ:19: expected variant tag name, got INDENT
  /tmp/test_variant_complex.occ:19: expected variant tag name, got :=
  /tmp/test_variant_complex.occ:19: expected variant tag name, got TRUE
  /tmp/test_variant_complex.occ:20: expected variant tag name, got :=
$ Bash
Test 3D channel array receive
cat > /tmp/test_3d_recv.occ << 'EOF'
VAL INT W IS 2 :
VAL INT H IS 2 :
VAL INT N IS 2 :
[W][H][N]CHAN OF INT link :
PAR
  link[0][0][0] ! 42
  INT x :
  SEQ
    link[0][0][0] ? x
    write.full.string(x)
EOF
./occam2go -o /tmp/test_3d_recv.go /tmp/test_3d_recv.occ 2>&1
<tool_use_error>Sibling tool call errored</tool_use_error>
$ Bash
Test sequential protocol
cat > /tmp/test_seq_proto.occ << 'EOF'
PROTOCOL RESPONSE IS BOOL; BOOL :
PROC test(CHAN OF RESPONSE sense)
  sense ! TRUE; FALSE
:
CHAN OF RESPONSE ch :
BOOL a, b :
PAR
  test(ch)
  ch ? a; b
EOF
./occam2go -o /tmp/test_seq_proto.go /tmp/test_seq_proto.occ 2>&1
<tool_use_error>Sibling tool call errored</tool_use_error>
$ Bash
Test 3D channel array receive
cat > /tmp/test_3d_recv.occ << 'EOF'
VAL INT W IS 2 :
VAL INT H IS 2 :
VAL INT N IS 2 :
[W][H][N]CHAN OF INT link :
PAR
  link[0][0][0] ! 42
  INT x :
  SEQ
    link[0][0][0] ? x
    write.full.string(x)
EOF
./occam2go -o /tmp/test_3d_recv.go /tmp/test_3d_recv.occ 2>&1
$ Bash
Test sequential protocol
cat > /tmp/test_seq_proto.occ << 'EOF'
PROTOCOL RESPONSE IS BOOL; BOOL :
PROC test(CHAN OF RESPONSE sense)
  sense ! TRUE; FALSE
:
CHAN OF RESPONSE ch :
BOOL a, b :
PAR
  test(ch)
  ch ? a; b
EOF
./occam2go -o /tmp/test_seq_proto.go /tmp/test_seq_proto.occ 2>&1
Thinking

So now I've found the remaining issue. The 3D channel arrays work fine, sequential protocols work fine. The only remaining issue is:

Variant protocol receive (? CASE) with multi-statement bodies — specifically when a case body has a declaration followed by a compound statement (like BOOL next.state : followed by SEQ).

Let me test a simpler variant with multi-statement bodies (no declarations, just multi-statements):

3D arrays and sequential protocols work fine. The only remaining issue is variant protocol receive with multi-statement bodies. Let me narrow it down further.

$ Bash
Test variant recv with SEQ body (no decls)
cat > /tmp/test_variant_seq.occ << 'EOF'
PROTOCOL CMD
  CASE
    go
    stop
:
PROC test(CHAN OF CMD control)
  BOOL running :
  SEQ
    running := TRUE
    WHILE running
      control ? CASE
        go
          SEQ
            running := TRUE
            SKIP
        stop
          running := FALSE
:
CHAN OF CMD ch :
PAR
  test(ch)
  SEQ
    ch ! go
    ch ! stop
EOF
./occam2go -o /tmp/test_variant_seq.go /tmp/test_variant_seq.occ 2>&1
$ Bash
Test variant recv with decl + SEQ body
cat > /tmp/test_variant_decl.occ << 'EOF'
PROTOCOL CMD
  CASE
    go
    stop
:
PROC test(CHAN OF CMD control)
  BOOL running :
  SEQ
    running := TRUE
    WHILE running
      control ? CASE
        go
          BOOL x :
          SEQ
            x := TRUE
            running := x
        stop
          running := FALSE
:
CHAN OF CMD ch :
PAR
  test(ch)
  SEQ
    ch ! go
    ch ! stop
EOF
./occam2go -o /tmp/test_variant_decl.go /tmp/test_variant_decl.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_variant_decl.occ:14: expected variant tag name, got SEQ
  /tmp/test_variant_decl.occ:15: expected variant tag name, got INDENT
  /tmp/test_variant_decl.occ:15: expected variant tag name, got :=
  /tmp/test_variant_decl.occ:15: expected variant tag name, got TRUE
  /tmp/test_variant_decl.occ:16: expected variant tag name, got :=

Parse errors:
  /tmp/test_variant_decl.occ:14: expected variant tag name, got SEQ
  /tmp/test_variant_decl.occ:15: expected variant tag name, got INDENT
  /tmp/test_variant_decl.occ:15: expected variant tag name, got :=
  /tmp/test_variant_decl.occ:15: expected variant tag name, got TRUE
  /tmp/test_variant_decl.occ:16: expected variant tag name, got :=
Thinking

Now I've isolated the issue precisely. Variant protocol receive works fine with single statements and even with compound statements (like SEQ with multiple lines). But it fails when a variant case body has a declaration (like BOOL x :) followed by a compound statement.

In occam, declarations create a "scoped block" — the declaration is followed by its scope (the statements that follow at the same indent level). So inside a variant case:

go
  BOOL x :
  SEQ
    x := TRUE
    running := x

The BOOL x : is a scoped declaration whose scope is the rest of the indented block (SEQ ... running := x).

The parser currently handles single statements in variant case bodies but doesn't handle scoped declarations. I need to find the parseVariantReceive function and see how it's structured.

Found it — the issue is specifically variant protocol receive with scoped declarations inside case bodies. Let me look at the parser code.

Grep
{
  "pattern": "func.*parseVariantReceive",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content"
}
1308:func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {
1409:func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndices []ast.Expression, token lexer.Token) *ast.VariantReceive {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1308,
  "limit": 100
}
  1308→func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {
  1309→	stmt := &ast.VariantReceive{
  1310→		Token:   token,
  1311→		Channel: channel,
  1312→	}
  1313→
  1314→	// Skip to next line
  1315→	for p.peekTokenIs(lexer.NEWLINE) {
  1316→		p.nextToken()
  1317→	}
  1318→
  1319→	// Expect INDENT
  1320→	if !p.peekTokenIs(lexer.INDENT) {
  1321→		p.addError("expected indented block after ? CASE")
  1322→		return stmt
  1323→	}
  1324→	p.nextToken() // consume INDENT
  1325→	startLevel := p.indentLevel
  1326→	p.nextToken() // move into block
  1327→
  1328→	// Parse variant cases (similar to parseCaseStatement pattern)
  1329→	for !p.curTokenIs(lexer.EOF) {
  1330→		// Skip newlines
  1331→		for p.curTokenIs(lexer.NEWLINE) {
  1332→			p.nextToken()
  1333→		}
  1334→
  1335→		// Handle DEDENT tokens
  1336→		for p.curTokenIs(lexer.DEDENT) {
  1337→			if p.indentLevel < startLevel {
  1338→				return stmt
  1339→			}
  1340→			p.nextToken()
  1341→		}
  1342→
  1343→		// Skip any more newlines after DEDENT
  1344→		for p.curTokenIs(lexer.NEWLINE) {
  1345→			p.nextToken()
  1346→		}
  1347→
  1348→		if p.curTokenIs(lexer.EOF) {
  1349→			break
  1350→		}
  1351→
  1352→		if p.indentLevel < startLevel {
  1353→			break
  1354→		}
  1355→
  1356→		// Safety guard: record position before parsing to detect no-progress
  1357→		prevToken := p.curToken
  1358→		prevPeek := p.peekToken
  1359→
  1360→		// Parse a variant case: tag [; var]* \n INDENT body
  1361→		vc := ast.VariantCase{}
  1362→
  1363→		if !p.curTokenIs(lexer.IDENT) {
  1364→			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
  1365→			p.nextToken() // skip unrecognized token to avoid infinite loop
  1366→			continue
  1367→		}
  1368→		vc.Tag = p.curToken.Literal
  1369→
  1370→		// Parse optional variables after semicolons: tag ; x ; y
  1371→		for p.peekTokenIs(lexer.SEMICOLON) {
  1372→			p.nextToken() // move to ;
  1373→			if !p.expectPeek(lexer.IDENT) {
  1374→				return stmt
  1375→			}
  1376→			vc.Variables = append(vc.Variables, p.curToken.Literal)
  1377→		}
  1378→
  1379→		// Skip newlines and expect INDENT for body
  1380→		for p.peekTokenIs(lexer.NEWLINE) {
  1381→			p.nextToken()
  1382→		}
  1383→
  1384→		if p.peekTokenIs(lexer.INDENT) {
  1385→			p.nextToken() // consume INDENT
  1386→			p.nextToken() // move to body
  1387→			vc.Body = p.parseStatement()
  1388→
  1389→			// Advance past the last token of the statement if needed
  1390→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1391→				p.nextToken()
  1392→			}
  1393→		}
  1394→
  1395→		stmt.Cases = append(stmt.Cases, vc)
  1396→
  1397→		// No-progress guard: if we haven't moved, break to prevent infinite loop
  1398→		if p.curToken == prevToken && p.peekToken == prevPeek {
  1399→			p.nextToken() // force progress
  1400→			if p.curToken == prevToken {
  1401→				break
  1402→			}
  1403→		}
  1404→	}
  1405→
  1406→	return stmt
  1407→}
Thinking

Now I see the issue. At line 1387:

vc.Body = p.parseStatement()

The parser only calls parseStatement() once, which parses a single statement. But in occam, a variant case body can contain scoped declarations followed by a process. For example:

evolve
  BOOL next.state :
  SEQ
    ...

Here BOOL next.state : is a declaration that scopes over the SEQ that follows. The parser needs to handle this as a block of statements (potentially with declarations), not just a single statement.

The pattern in occam is that declarations scope over the following statements.

I need to understand how declarations work in the parser — when parseStatement() encounters a type keyword like BOOL, it should recognize this as a declaration and parse both the declaration and the process it scopes over. Let me check how this pattern is handled elsewhere in the parser, particularly in IF or CASE bodies.

Grep
{
  "pattern": "case lexer\\.(BOOL_TYPE|INT_TYPE|BYTE_TYPE|REAL_TYPE|INT16_TYPE|INT32_TYPE|INT64_TYPE)",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-B": 2,
  "-A": 5
}
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-		lexer.INT16_TYPE, lexer.INT32_TYPE, lexer.INT64_TYPE:
170-		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) || p.peekTokenIs(lexer.INLINE) {
171-			return p.parseFuncDecl()
172-		}
173-		return p.parseVarDeclOrAbbreviation()
--
953-func (p *Parser) parseProtocolTypeName() string {
954-	switch p.curToken.Type {
955:	case lexer.INT_TYPE:
956-		return "INT"
957:	case lexer.BYTE_TYPE:
958-		return "BYTE"
959:	case lexer.BOOL_TYPE:
960-		return "BOOL"
961:	case lexer.REAL_TYPE:
962-		return "REAL"
963-	case lexer.REAL32_TYPE:
964-		return "REAL32"
965-	case lexer.REAL64_TYPE:
966-		return "REAL64"
--
1709-func (p *Parser) isAltDeclStart() bool {
1710-	switch p.curToken.Type {
1711:	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE,
1712-		lexer.INT16_TYPE, lexer.INT32_TYPE, lexer.INT64_TYPE:
1713-		return true
1714-	case lexer.VAL:
1715-		return true
1716-	case lexer.INITIAL:
--
2881-			IsNeg:    isNeg,
2882-		}
2883:	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE,
2884-		lexer.INT16_TYPE, lexer.INT32_TYPE, lexer.INT64_TYPE:
2885-		token := p.curToken
2886-		p.nextToken()
2887-		qualifier := ""
2888-		if p.curTokenIs(lexer.ROUND_KW) || p.curTokenIs(lexer.TRUNC_KW) {
Thinking

So parseStatement() dispatches to parseVarDeclOrAbbreviation() for type keywords. Let me check what that function does — specifically whether it returns just the declaration or also handles the scoped process after the declaration.

Grep
{
  "pattern": "func.*parseVarDeclOrAbbreviation",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content"
}
266:func (p *Parser) parseVarDeclOrAbbreviation() ast.Statement {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 266,
  "limit": 10
}
   266→func (p *Parser) parseVarDeclOrAbbreviation() ast.Statement {
   267→	typeToken := p.curToken
   268→	typeName := p.curToken.Literal
   269→
   270→	// Consume the name
   271→	if !p.expectPeek(lexer.IDENT) {
   272→		return nil
   273→	}
   274→	name := p.curToken.Literal
   275→
Thinking

The issue is now clear. In the variant case parsing, at line 1387 (vc.Body = p.parseStatement()), the parser calls parseStatement() which handles a single statement. When the case body starts with a declaration like BOOL next.state :, parseStatement() parses it as a VarDecl and returns. But the SEQ block that follows (which is the scope of the declaration) is left unprocessed.

In occam, declarations create scoped blocks — the declaration scopes over whatever follows at the same or deeper indentation. The parser typically handles this in parseBlockStatements() which parses multiple statements (declarations + their scoped processes).

The real problem is that parseVariantReceive() only calls parseStatement() once, so it misses the scoped block that comes after a declaration. I should change the variant case body parsing to use parseBlockStatements() instead, which properly handles the declaration-and-scope pattern that occam requires. Let me also verify what other constructs from life.occ might not be working yet.

Now I'm checking for several specific features: external library procedures like write.string, protocol types like DATA.ITEM, character case patterns, system channels like terminal.keyboard, abbreviations with comparison expressions, multi-line function calls with continuation, and various other occam-specific constructs.

The variant case body declaration is definitely causing parse errors, so I need to focus on fixing that first, then test the rest of the life.occ file to catch any other hidden issues.

Now I understand the root cause. The variant receive parser only calls parseStatement() once per case body, but occam allows scoped declarations (e.g., BOOL x : followed by SEQ ...) which require parsing a block. Let me also check for other remaining life.occ constructs.

$ Bash
Test VAL BOOL abbreviation with NOT
cat > /tmp/test_val_bool.occ << 'EOF'
VAL BOOL alive IS TRUE :
VAL BOOL dead IS NOT alive :
PROC test(VAL BOOL state)
  IF
    state = alive
      SKIP
    state = dead
      SKIP
:
SEQ
  test(TRUE)
EOF
./occam2go -o /tmp/test_val_bool.go /tmp/test_val_bool.occ 2>&1
$ Bash
Test CASE with char literals and ELSE
cat > /tmp/test_case_char.occ << 'EOF'
PROC test(VAL BYTE char)
  INT activity :
  SEQ
    CASE char
      'q', 'Q'
        activity := 4
      's', 'S'
        activity := 0
      ELSE
        activity := 2
:
SEQ
  test('q')
EOF
./occam2go -o /tmp/test_case_char.go /tmp/test_case_char.occ 2>&1
$ Bash
Test VAL abbreviation with complex expression
cat > /tmp/test_val_expr_abbrev.occ << 'EOF'
VAL BOOL alive IS TRUE :
PROC test(VAL BYTE char)
  VAL BOOL state IS (char = '*s') = alive :
  IF
    state
      SKIP
    NOT state
      SKIP
:
SEQ
  test('*s')
EOF
./occam2go -o /tmp/test_val_expr_abbrev.go /tmp/test_val_expr_abbrev.occ 2>&1

Good. Let me test the remaining tricky constructs: the CASE inside a CASE match with byte sequences, and the PRI ALT with specific life.occ patterns.

$ Bash
Test CASE with named constants and comma-separated
cat > /tmp/test_case_in_case.occ << 'EOF'
VAL INT idle IS 0 :
VAL INT editing IS 1 :
VAL INT free.running IS 3 :
VAL INT single.step IS 2 :
PROC test(VAL INT activity)
  CASE activity
    idle
      SKIP
    editing
      SKIP
    free.running, single.step
      SKIP
:
SEQ
  test(0)
EOF
./occam2go -o /tmp/test_case_in_case.go /tmp/test_case_in_case.occ 2>&1
$ Bash
Test CASE with byte literal values
cat > /tmp/test_case_byte.occ << 'EOF'
PROC test(VAL BYTE char)
  CASE char
    'A'
      SKIP
    '*s', '**'
      SKIP
    'q', 'Q'
      SKIP
    ELSE
      SKIP
:
SEQ
  test('A')
EOF
./occam2go -o /tmp/test_case_byte.go /tmp/test_case_byte.occ 2>&1
$ Bash
Test PRI ALT with guard and SKIP guard from life.occ
cat > /tmp/test_prialt_life.occ << 'EOF'
VAL INT editing IS 1 :
PROC test(CHAN OF BYTE keyboard, VAL INT activity)
  BYTE char :
  PRI ALT
    (activity <> editing) & keyboard ? char
      SKIP
    (activity <> 0) & SKIP
      SKIP
:
CHAN OF BYTE kb :
PAR
  test(kb, 0)
  kb ! 'A'
EOF
./occam2go -o /tmp/test_prialt_life.go /tmp/test_prialt_life.occ 2>&1

All other constructs work. Let me also verify the multi-line function call and the exact control ? CASE pattern from life.occ with its set.state; state variant that binds to an existing variable.

$ Bash
Test multi-line function call arguments
cat > /tmp/test_multiline_call.occ << 'EOF'
PROC add(VAL INT a, b, c,
         VAL INT d, INT result)
  result := a + b + c + d
:
INT r :
SEQ
  add(1, 2, 3,
      4, r)
  write.full.string(r)
EOF
./occam2go -o /tmp/test_multiline_call.go /tmp/test_multiline_call.occ 2>&1
$ Bash
Test variant receive binding to existing variable
cat > /tmp/test_variant_binding.occ << 'EOF'
PROTOCOL CMD
  CASE
    set.state; BOOL
    evolve
    terminate
:
PROC test(CHAN OF CMD control)
  BOOL state :
  SEQ
    state := FALSE
    control ? CASE
      set.state; state
        SKIP
      evolve
        SKIP
      terminate
        SKIP
:
CHAN OF CMD ch :
PAR
  test(ch)
  ch ! set.state; TRUE
EOF
./occam2go -o /tmp/test_variant_binding.go /tmp/test_variant_binding.occ 2>&1
Thinking

Excellent! So the ONLY remaining issue blocking life.occ is the variant protocol receive with scoped declarations inside case bodies. Specifically, when a variant case body starts with a declaration (like BOOL next.state :) followed by a compound statement (like SEQ ...), the parser fails because it only calls parseStatement() once, which parses the declaration but not its scope.

Also, there are some library/runtime dependencies that life.occ has: - write.string, write.formatted - these are library functions - CHAN OF DATA.ITEM and data.int - protocol from the course module

  • terminal.keyboard, terminal.screen - system channels

But these are runtime/library concerns, not parser/transpiler issues. The user's question is specifically about parse errors.

Let me check whether the line 58 error is also caused by the variant case issue or something separate. Looking at the original error output, there were errors at both line 58 and line 106+. I confirmed the line 106+ errors are from the variant case body declarations, but the line 58 error ("expected type after array size, got NEWLINE") might be a different problem.

Line 58 is count := 0 inside the calculate.next.state proc, which has [][][]CHAN OF STATE link as a parameter. The error message suggests it's expecting a type after an array size. I tested 3D open array params and they worked fine, so maybe the issue is with the specific combination of parameters in this proc...

Looking at the parameter list, I see VAL []INT nx, ny where both nx and ny share the VAL []INT type, followed by VAL BOOL state, BOOL next.state where state is VAL BOOL but next.state isn't. Let me test if this specific parameter pattern is causing the problem.

Let me also pin down the first error at line 58. It might be a separate issue with the proc parameter list.

$ Bash
Test calculate.next.state proc signature
cat > /tmp/test_calc_proc.occ << 'EOF'
PROC calculate.next.state([][][]CHAN OF INT link,
                          VAL []INT nx, ny,
                          VAL BOOL state, BOOL next.state)
  INT count :
  SEQ
    count := 0
:
SEQ
  SKIP
EOF
./occam2go -o /tmp/test_calc_proc.go /tmp/test_calc_proc.occ 2>&1
$ Bash
Test full calculate.next.state proc body
cat > /tmp/test_calc_body.occ << 'EOF'
VAL INT neighbours IS 8 :
VAL BOOL alive IS TRUE :
VAL BOOL dead IS NOT alive :
PROC calculate.next.state([][][]CHAN OF INT link,
                          VAL []INT nx, ny,
                          VAL BOOL state, BOOL next.state)
  INT count :
  SEQ
    [neighbours]BOOL state.of.neighbour :
    SEQ
      PAR d = 0 FOR neighbours
        link[nx[d]][ny[d]][d] ? state.of.neighbour[d]
      count := 0
      SEQ d = 0 FOR neighbours
        IF
          state.of.neighbour[d] = alive
            count := count + 1
          state.of.neighbour[d] = dead
            SKIP
    IF
      count < 2
        next.state := dead
      count = 2
        next.state := state
      count = 3
        next.state := alive
      count > 3
        next.state := dead
:
SEQ
  SKIP
EOF
./occam2go -o /tmp/test_calc_body.go /tmp/test_calc_body.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_calc_body.occ:13: expected type after array size, got NEWLINE

Parse errors:
  /tmp/test_calc_body.occ:13: expected type after array size, got NEWLINE
Thinking

So there IS a second issue. Line 13 in this test is:

        link[nx[d]][ny[d]][d] ? state.of.neighbour[d]

The error is "expected type after array size, got NEWLINE". This happens inside a PAR d = 0 FOR neighbours block. The statement starts with link (an identifier), but the parser might be seeing [ and interpreting it as an array declaration.

But wait — link is an identifier. The parser should see link as an IDENT first, then [ as an index. Let me think about why this would fail...

The problem might be that nested indices like nx[d] inside the channel receive aren't being parsed correctly. Though I tested 3D indexed receives with literal indices before and they worked fine, so maybe the issue is specifically with variable indices inside those brackets.

There's a second issue with nested indexing expressions. Let me narrow it down.

$ Bash
Test nested index expression in channel receive
cat > /tmp/test_nested_idx.occ << 'EOF'
[3]INT nx :
[3][3][3]CHAN OF INT link :
INT x :
PAR d = 0 FOR 3
  link[nx[d]][0][d] ? x
EOF
./occam2go -o /tmp/test_nested_idx.go /tmp/test_nested_idx.occ 2>&1
$ Bash
Test variable index in 3D channel receive
cat > /tmp/test_simple_idx.occ << 'EOF'
[3][3][3]CHAN OF INT link :
INT x :
INT d :
SEQ
  d := 0
  link[d][0][d] ? x
EOF
./occam2go -o /tmp/test_simple_idx.go /tmp/test_simple_idx.occ 2>&1
Thinking

Hmm, nested indexing works. So link[nx[d]][0][d] ? x works. Let me try the exact pattern from life.occ more closely.

$ Bash
Test exact pattern from life.occ
cat > /tmp/test_exact_pattern.occ << 'EOF'
VAL INT neighbours IS 8 :
[3][3][neighbours]CHAN OF INT link :
[neighbours]INT state.of.neighbour :
[neighbours]INT nx :
[neighbours]INT ny :
PAR d = 0 FOR neighbours
  link[nx[d]][ny[d]][d] ? state.of.neighbour[d]
EOF
./occam2go -o /tmp/test_exact_pattern.go /tmp/test_exact_pattern.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_exact_pattern.occ:8: expected type after array size, got NEWLINE

Parse errors:
  /tmp/test_exact_pattern.occ:8: expected type after array size, got NEWLINE
$ Bash
Test BOOL array declaration
# Test with BOOL array instead of INT
cat > /tmp/test_bool_arr.occ << 'EOF'
VAL INT neighbours IS 8 :
[neighbours]BOOL state.of.neighbour :
SEQ
  state.of.neighbour[0] := TRUE
EOF
./occam2go -o /tmp/test_bool_arr.go /tmp/test_bool_arr.occ 2>&1
$ Bash
Test scoped BOOL array declaration
# Test declaring a BOOL array inside a block
cat > /tmp/test_scoped_bool_arr.occ << 'EOF'
VAL INT neighbours IS 8 :
SEQ
  [neighbours]BOOL state.of.neighbour :
  SEQ
    state.of.neighbour[0] := TRUE
EOF
./occam2go -o /tmp/test_scoped_bool_arr.go /tmp/test_scoped_bool_arr.occ 2>&1
$ Bash
Test PAR with nested index recv (decls at top level)
# Add back pieces one at a time
cat > /tmp/test_par_recv.occ << 'EOF'
VAL INT neighbours IS 8 :
[3][3][neighbours]CHAN OF INT link :
[neighbours]BOOL state.of.neighbour :
[neighbours]INT nx :
[neighbours]INT ny :
SEQ
  PAR d = 0 FOR neighbours
    link[nx[d]][ny[d]][d] ? state.of.neighbour[d]
EOF
./occam2go -o /tmp/test_par_recv.go /tmp/test_par_recv.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_par_recv.occ:9: expected type after array size, got NEWLINE

Parse errors:
  /tmp/test_par_recv.occ:9: expected type after array size, got NEWLINE
$ Bash
Test simpler nested index receive in PAR replicator
# Simpler: just one level of nested indexing
cat > /tmp/test_par_recv2.occ << 'EOF'
[8]INT nx :
[3][3][8]CHAN OF INT link :
[8]BOOL state.of.neighbour :
SEQ
  PAR d = 0 FOR 8
    link[nx[d]][0][d] ? state.of.neighbour[d]
EOF
./occam2go -o /tmp/test_par_recv2.go /tmp/test_par_recv2.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_par_recv2.occ:7: expected type after array size, got NEWLINE

Parse errors:
  /tmp/test_par_recv2.occ:7: expected type after array size, got NEWLINE
$ Bash
Test receive into indexed BOOL variable
# Even simpler: receive into indexed BOOL array
cat > /tmp/test_recv_bool_idx.occ << 'EOF'
[8]CHAN OF INT link :
[8]BOOL flags :
SEQ
  link[0] ? flags[0]
EOF
./occam2go -o /tmp/test_recv_bool_idx.go /tmp/test_recv_bool_idx.occ 2>&1
$ Bash
Test receive into indexed INT variable
# Compare: receive into indexed INT array
cat > /tmp/test_recv_int_idx.occ << 'EOF'
[8]CHAN OF INT link :
[8]INT flags :
SEQ
  link[0] ? flags[0]
EOF
./occam2go -o /tmp/test_recv_int_idx.go /tmp/test_recv_int_idx.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_recv_bool_idx.occ:5: expected type after array size, got NEWLINE

Parse errors:
  /tmp/test_recv_bool_idx.occ:5: expected type after array size, got NEWLINE
<tool_use_error>Sibling tool call errored</tool_use_error>
$ Bash
Test receive into indexed INT variable
# Compare: receive into indexed INT array
cat > /tmp/test_recv_int_idx.occ << 'EOF'
[8]CHAN OF INT link :
[8]INT flags :
SEQ
  link[0] ? flags[0]
EOF
./occam2go -o /tmp/test_recv_int_idx.go /tmp/test_recv_int_idx.occ 2>&1
Exit code 1
Parse errors:
  /tmp/test_recv_int_idx.occ:5: expected type after array size, got NEWLINE

Parse errors:
  /tmp/test_recv_int_idx.occ:5: expected type after array size, got NEWLINE
Thinking

Interesting! So even a simple receive into an indexed array variable fails: link[0] ? flags[0]. Let me test if receive into a non-indexed variable works.

$ Bash
Test basic recv patterns
# Receive into simple variable works?
cat > /tmp/test_recv_simple.occ << 'EOF'
[8]CHAN OF INT link :
INT x :
SEQ
  link[0] ? x
EOF
./occam2go -o /tmp/test_recv_simple.go /tmp/test_recv_simple.occ 2>&1
echo "---"
# Receive into indexed var?
cat > /tmp/test_recv_idx.occ << 'EOF'
CHAN OF INT link :
[8]INT flags :
INT x :
SEQ
  link ? flags[0]
EOF
./occam2go -o /tmp/test_recv_idx.go /tmp/test_recv_idx.occ 2>&1
Exit code 1
---
Parse errors:
  /tmp/test_recv_idx.occ:6: expected type after array size, got NEWLINE

---
Parse errors:
  /tmp/test_recv_idx.occ:6: expected type after array size, got NEWLINE
Thinking

So the issue is: link ? flags[0] — receiving into an indexed variable. The parser sees flags[0] and interprets the [0] as an array size declaration rather than an index expression.

Let me look at how channel receive is parsed in the parser. The receive statement is channel ? variable, and the variable can be indexed. Let me see how it's parsed.

Actually, link[0] ? x works (the first test was empty output = success). But link ? flags[0] fails. So the issue is specifically with indexed targets in channel receive.

Let me look at how the parser handles ? variable[index].

The issue is receiving into an indexed target variable: link ? flags[0]. Let me look at how the parser handles this.

Grep
{
  "pattern": "parseAssignment|QUESTION|? .*receive|chanReceive|ChanReceive|parseReceive",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "QUESTION",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "ChanRecv|chan.*recv|channel.*receive|parseIdent",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true
}
62:	// Track timer names to distinguish timer reads from channel receives
773:		// Indexed channel receive: cs[i] ? x or cs[i][j] ? x or cs[i] ? CASE ...
1283:		return p.parseVariantReceive(channel, recvToken)
1754:	// If next token is ? then it's a channel/timer receive
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1750,
  "limit": 80
}
  1750→
  1751→	// Look ahead to determine if this is a guard or channel
  1752→	// If current is ( then it must be a guard expression
  1753→	// If next token is & then we have a guard
  1754→	// If next token is ? then it's a channel/timer receive
  1755→	if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.RECEIVE) {
  1756→		name := p.curToken.Literal
  1757→		if p.timerNames[name] {
  1758→			// Timer case: tim ? AFTER deadline
  1759→			altCase.IsTimer = true
  1760→			altCase.Timer = name
  1761→			p.nextToken() // move to ?
  1762→			if !p.expectPeek(lexer.AFTER) {
  1763→				return nil
  1764→			}
  1765→			p.nextToken() // move past AFTER
  1766→			altCase.Deadline = p.parseExpression(LOWEST)
  1767→		} else {
  1768→			// Simple case: channel ? var
  1769→			altCase.Channel = name
  1770→			p.nextToken() // move to ?
  1771→			if !p.expectPeek(lexer.IDENT) {
  1772→				return nil
  1773→			}
  1774→			altCase.Variable = p.curToken.Literal
  1775→		}
  1776→	} else if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.LBRACKET) {
  1777→		// Indexed channel case: cs[i] ? var or cs[i][j] ? var
  1778→		name := p.curToken.Literal
  1779→		altCase.Channel = name
  1780→		for p.peekTokenIs(lexer.LBRACKET) {
  1781→			p.nextToken() // move to [
  1782→			p.nextToken() // move past [
  1783→			altCase.ChannelIndices = append(altCase.ChannelIndices, p.parseExpression(LOWEST))
  1784→			if !p.expectPeek(lexer.RBRACKET) {
  1785→				return nil
  1786→			}
  1787→		}
  1788→		if !p.expectPeek(lexer.RECEIVE) {
  1789→			return nil
  1790→		}
  1791→		if !p.expectPeek(lexer.IDENT) {
  1792→			return nil
  1793→		}
  1794→		altCase.Variable = p.curToken.Literal
  1795→	} else {
  1796→		// Guard followed by & channel ? var, or guard & SKIP
  1797→		guard := p.parseExpression(LOWEST)
  1798→		altCase.Guard = guard
  1799→
  1800→		// Expect &
  1801→		if !p.peekTokenIs(lexer.AMPERSAND) {
  1802→			p.addError("expected & after guard in ALT case")
  1803→			return nil
  1804→		}
  1805→		p.nextToken() // move to &
  1806→		p.nextToken() // move past &
  1807→
  1808→		if p.curTokenIs(lexer.SKIP) {
  1809→			// Guarded SKIP: guard & SKIP
  1810→			altCase.IsSkip = true
  1811→		} else if !p.curTokenIs(lexer.IDENT) {
  1812→			p.addError(fmt.Sprintf("expected channel name or SKIP after guard, got %s", p.curToken.Type))
  1813→			return nil
  1814→		} else {
  1815→			// Channel operation after guard
  1816→			altCase.Channel = p.curToken.Literal
  1817→
  1818→			for p.peekTokenIs(lexer.LBRACKET) {
  1819→				// Indexed channel with guard: guard & cs[i] ? var or cs[i][j] ? var
  1820→				p.nextToken() // move to [
  1821→				p.nextToken() // move past [
  1822→				altCase.ChannelIndices = append(altCase.ChannelIndices, p.parseExpression(LOWEST))
  1823→				if !p.expectPeek(lexer.RBRACKET) {
  1824→					return nil
  1825→				}
  1826→			}
  1827→
  1828→			if !p.expectPeek(lexer.RECEIVE) {
  1829→				return nil
Thinking

I see — in the ALT parsing, after receiving ?, it does p.expectPeek(lexer.IDENT) to get the variable name. This only handles simple variables, not indexed ones. But that's for ALT. Let me look at the main receive parsing for regular statements (not in ALT).

Let me find where regular channel receive (chan ? var) is parsed in parseStatement or wherever.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 770,
  "limit": 100
}
   770→	}
   771→
   772→	if p.peekTokenIs(lexer.RECEIVE) {
   773→		// Indexed channel receive: cs[i] ? x or cs[i][j] ? x or cs[i] ? CASE ...
   774→		p.nextToken() // move to ?
   775→		recvToken := p.curToken
   776→
   777→		// Check for variant receive: cs[i] ? CASE
   778→		if p.peekTokenIs(lexer.CASE) {
   779→			p.nextToken() // move to CASE
   780→			return p.parseVariantReceiveWithIndex(name, indices, recvToken)
   781→		}
   782→
   783→		stmt := &ast.Receive{
   784→			Token:          recvToken,
   785→			Channel:        name,
   786→			ChannelIndices: indices,
   787→		}
   788→
   789→		if !p.expectPeek(lexer.IDENT) {
   790→			return nil
   791→		}
   792→		stmt.Variable = p.curToken.Literal
   793→
   794→		// Check for sequential receive
   795→		for p.peekTokenIs(lexer.SEMICOLON) {
   796→			p.nextToken() // move to ;
   797→			if !p.expectPeek(lexer.IDENT) {
   798→				return nil
   799→			}
   800→			stmt.Variables = append(stmt.Variables, p.curToken.Literal)
   801→		}
   802→
   803→		return stmt
   804→	}
   805→
   806→	// Default: treat as indexed assignment (shouldn't reach here normally)
   807→	p.addError(fmt.Sprintf("expected :=, !, or ? after %s[...], got %s", name, p.peekToken.Type))
   808→	return nil
   809→}
   810→
   811→func (p *Parser) parseIndexExpression(left ast.Expression) *ast.IndexExpr {
   812→	expr := &ast.IndexExpr{
   813→		Token: p.curToken,
   814→		Left:  left,
   815→	}
   816→
   817→	p.nextToken() // move past [
   818→	expr.Index = p.parseExpression(LOWEST)
   819→
   820→	if !p.expectPeek(lexer.RBRACKET) {
   821→		return nil
   822→	}
   823→
   824→	return expr
   825→}
   826→
   827→func (p *Parser) parseChanDecl() *ast.ChanDecl {
   828→	decl := &ast.ChanDecl{Token: p.curToken}
   829→
   830→	// Expect OF (optional — CHAN BYTE is shorthand for CHAN OF BYTE)
   831→	if p.peekTokenIs(lexer.OF) {
   832→		p.nextToken() // consume OF
   833→	}
   834→
   835→	// Expect type (INT, BYTE, BOOL, etc.) or protocol name (IDENT)
   836→	p.nextToken()
   837→	if p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
   838→		p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
   839→		p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
   840→		decl.ElemType = p.curToken.Literal
   841→	} else if p.curTokenIs(lexer.IDENT) {
   842→		decl.ElemType = p.curToken.Literal
   843→	} else {
   844→		p.addError(fmt.Sprintf("expected type after CHAN, got %s", p.curToken.Type))
   845→		return nil
   846→	}
   847→
   848→	// Parse channel names
   849→	for {
   850→		if !p.expectPeek(lexer.IDENT) {
   851→			return nil
   852→		}
   853→		decl.Names = append(decl.Names, p.curToken.Literal)
   854→
   855→		if p.peekTokenIs(lexer.COMMA) {
   856→			p.nextToken() // consume comma
   857→		} else {
   858→			break
   859→		}
   860→	}
   861→
   862→	if !p.expectPeek(lexer.COLON) {
   863→		return nil
   864→	}
   865→
   866→	return decl
   867→}
   868→
   869→func (p *Parser) parseProtocolDecl() *ast.ProtocolDecl {
Thinking

I see the issue. At line 789: if !p.expectPeek(lexer.IDENT) — the parser expects the receive target to be a simple identifier. But flags[0] is an indexed expression, not just an identifier.

Let me also find where simple (non-indexed) channel receives are parsed.

Grep
{
  "pattern": "RECEIVE.*IDENT|channel.*\\?.*var",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true
}
1694:		// Parse an ALT case: [guard &] channel ? var
1741:	// Check for guard: expression & channel ? var
1742:	// or: channel ? var (no guard)
1768:			// Simple case: channel ? var
1777:		// Indexed channel case: cs[i] ? var or cs[i][j] ? var
1796:		// Guard followed by & channel ? var, or guard & SKIP
1819:				// Indexed channel with guard: guard & cs[i] ? var or cs[i][j] ? var
Grep
{
  "pattern": "case lexer.IDENT:",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true,
  "-A": 3
}
204:	case lexer.IDENT:
205-		/ Check for record variable declaration: TYPENAME var:
206-		if p.recordNames[p.curToken.Literal] && p.peekTokenIs(lexer.IDENT) {
207-			return p.parseRecordVarDecl()
--
967:	case lexer.IDENT:
968-		return p.curToken.Literal
969-	default:
970-		p.addError(fmt.Sprintf("expected type name in protocol, got %s", p.curToken.Type))
--
2741:	case lexer.IDENT:
2742-		if p.peekTokenIs(lexer.LPAREN) {
2743-			left = p.parseFuncCallExpr()
2744-		} else {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 204,
  "limit": 80
}
   204→	case lexer.IDENT:
   205→		// Check for record variable declaration: TYPENAME var:
   206→		if p.recordNames[p.curToken.Literal] && p.peekTokenIs(lexer.IDENT) {
   207→			return p.parseRecordVarDecl()
   208→		}
   209→		// Could be assignment, indexed assignment, indexed send/receive, send, receive, or procedure call
   210→		if p.peekTokenIs(lexer.LBRACKET) {
   211→			return p.parseIndexedOperation()
   212→		}
   213→		if p.peekTokenIs(lexer.ASSIGN) {
   214→			return p.parseAssignment()
   215→		}
   216→		if p.peekTokenIs(lexer.COMMA) {
   217→			return p.parseMultiAssignment()
   218→		}
   219→		if p.peekTokenIs(lexer.SEND) {
   220→			return p.parseSend()
   221→		}
   222→		if p.peekTokenIs(lexer.RECEIVE) {
   223→			if p.timerNames[p.curToken.Literal] {
   224→				return p.parseTimerRead()
   225→			}
   226→			return p.parseReceive()
   227→		}
   228→		return p.parseProcCall()
   229→	case lexer.INDENT, lexer.DEDENT, lexer.EOF:
   230→		return nil
   231→	default:
   232→		p.addError(fmt.Sprintf("unexpected token: %s", p.curToken.Type))
   233→		return nil
   234→	}
   235→}
   236→
   237→func (p *Parser) parseVarDecl() *ast.VarDecl {
   238→	decl := &ast.VarDecl{
   239→		Token: p.curToken,
   240→		Type:  p.curToken.Literal,
   241→	}
   242→
   243→	// Parse variable names
   244→	for {
   245→		if !p.expectPeek(lexer.IDENT) {
   246→			return nil
   247→		}
   248→		decl.Names = append(decl.Names, p.curToken.Literal)
   249→
   250→		if p.peekTokenIs(lexer.COMMA) {
   251→			p.nextToken() // consume comma
   252→		} else {
   253→			break
   254→		}
   255→	}
   256→
   257→	if !p.expectPeek(lexer.COLON) {
   258→		return nil
   259→	}
   260→
   261→	return decl
   262→}
   263→
   264→// parseVarDeclOrAbbreviation parses either a variable declaration (INT x:)
   265→// or a non-VAL abbreviation (INT x IS expr:). Called when current token is a type keyword.
   266→func (p *Parser) parseVarDeclOrAbbreviation() ast.Statement {
   267→	typeToken := p.curToken
   268→	typeName := p.curToken.Literal
   269→
   270→	// Consume the name
   271→	if !p.expectPeek(lexer.IDENT) {
   272→		return nil
   273→	}
   274→	name := p.curToken.Literal
   275→
   276→	// Check if this is an abbreviation (next token is IS)
   277→	if p.peekTokenIs(lexer.IS) {
   278→		p.nextToken() // consume IS
   279→		p.nextToken() // move to expression
   280→		value := p.parseExpression(LOWEST)
   281→
   282→		if !p.expectPeek(lexer.COLON) {
   283→			return nil

Now let me find parseReceive to see how it handles the target variable.

Grep
{
  "pattern": "func.*parseReceive\\b",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true
}
1274:func (p *Parser) parseReceive() ast.Statement {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1274,
  "limit": 35
}
  1274→func (p *Parser) parseReceive() ast.Statement {
  1275→	channel := p.curToken.Literal
  1276→
  1277→	p.nextToken() // move to ?
  1278→	recvToken := p.curToken
  1279→
  1280→	// Check for variant receive: c ? CASE
  1281→	if p.peekTokenIs(lexer.CASE) {
  1282→		p.nextToken() // move to CASE
  1283→		return p.parseVariantReceive(channel, recvToken)
  1284→	}
  1285→
  1286→	stmt := &ast.Receive{
  1287→		Channel: channel,
  1288→		Token:   recvToken,
  1289→	}
  1290→
  1291→	if !p.expectPeek(lexer.IDENT) {
  1292→		return nil
  1293→	}
  1294→	stmt.Variable = p.curToken.Literal
  1295→
  1296→	// Check for sequential receive: c ? x ; y ; z
  1297→	for p.peekTokenIs(lexer.SEMICOLON) {
  1298→		p.nextToken() // move to ;
  1299→		if !p.expectPeek(lexer.IDENT) {
  1300→			return nil
  1301→		}
  1302→		stmt.Variables = append(stmt.Variables, p.curToken.Literal)
  1303→	}
  1304→
  1305→	return stmt
  1306→}
  1307→
  1308→func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {
Thinking

Now I see the full picture. There are actually two separate issues:

Issue 1: Receive into indexed target variable

In both parseReceive() (line 1291) and parseIndexedOperation() (line 789), after ?, the parser does p.expectPeek(lexer.IDENT) which only accepts a simple variable name. It doesn't handle flags[0] or state.of.neighbour[d] as receive targets.

This means ch ? flags[0] fails because after ?, the parser expects just an IDENT but gets IDENT followed by LBRACKET.

Let me trace through what actually happens with ch ? flags[0] step by step. The parser would recognize flags as an IDENT, but then the [0] part gets left hanging since the parser stops after consuming just the variable name and doesn't continue parsing the index operation. After parseReceive() returns with just Variable = "flags", the parser's current position is right after the identifier, leaving the [0] tokens unprocessed. When control returns to the calling context, it tries to parse the next statement starting with [, which explains the parse error about expecting a type after an array size. caller probably tries to advance and encounters [, then tries to parse it as a statement — and [ at the start of a statement is treated as an array declaration. Hence the error "expected type after array size, got NEWLINE".

Hmm wait, but then [0] would be parsed as array size 0, and after ] the parser expects a type keyword. Instead it gets NEWLINE (end of line). That matches the error: "expected type after array size, got NEWLINE".

Actually wait, I realize the path is different. The statement parser dispatches on LBRACKET and calls the array declaration parser. So [0] gets parsed as array size 0, then ] is consumed, and the parser expects a type keyword for the array element type. Since the ] is followed by NEWLINE, we get "expected type after array size, got NEWLINE".

The fix would be to handle indexed target variables in parseReceive() and parseIndexedOperation(). But looking more carefully, when the statement starts with link[0][0][0] ? flags[0], the statement parser sees IDENT link followed by [, so it calls parseIndexedOperation(). That function handles the ? flags part, but then flags[0] would have the same problem. Let me trace through parseIndexedOperation() more carefully — after the ?, it expects an IDENT to get flags, then... and stores stmt.Variable = "flags". The [0] after flags is left unparsed.

Let me trace through the test case link[0] ? flags[0]: 1. parseStatement sees link (IDENT) and peek [ → calls parseIndexedOperation() 2. parseIndexedOperation parses link, consumes indices [0] 3. sees peek ? (RECEIVE) → enters the receive handling at line 772 4. Line 774: advance to ?, save as recvToken 5. Line 778: peek is IDENT "flags", not CASE 6. Line 789: p.expectPeek(lexer.IDENT) → true, cur = "flags" 7. Line 792: stmt.Variable = "flags" 8. The receive check then sees [ at peek, not ;, so it continues parsing

After returning from parseStatement, the block parser still has "flags" as the current token but needs to advance past it. Let me check how parseBlockStatements handles the return value.

Now I have a clear picture of the remaining issues. Let me also look at how parseBlockStatements handles the cursor after parseStatement returns, to confirm my understanding.

Grep
{
  "pattern": "func.*parseBlockStatements",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true
}
1856:func (p *Parser) parseBlockStatements() []ast.Statement {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1856,
  "limit": 60
}
  1856→func (p *Parser) parseBlockStatements() []ast.Statement {
  1857→	var statements []ast.Statement
  1858→	startLevel := p.indentLevel
  1859→
  1860→	p.nextToken() // move past INDENT
  1861→
  1862→	for !p.curTokenIs(lexer.EOF) {
  1863→		// Skip newlines
  1864→		for p.curTokenIs(lexer.NEWLINE) {
  1865→			p.nextToken()
  1866→		}
  1867→
  1868→		// Handle DEDENT tokens
  1869→		// If we're at a DEDENT and indentLevel has dropped below startLevel,
  1870→		// this DEDENT ends our block - stop parsing
  1871→		// If indentLevel >= startLevel, this DEDENT is from a nested block - skip it
  1872→		for p.curTokenIs(lexer.DEDENT) {
  1873→			if p.indentLevel < startLevel {
  1874→				return statements
  1875→			}
  1876→			p.nextToken() // skip nested block's DEDENT
  1877→		}
  1878→
  1879→		// Skip any more newlines after DEDENT
  1880→		for p.curTokenIs(lexer.NEWLINE) {
  1881→			p.nextToken()
  1882→		}
  1883→
  1884→		if p.curTokenIs(lexer.EOF) {
  1885→			break
  1886→		}
  1887→
  1888→		// Double-check we haven't gone below our level
  1889→		if p.indentLevel < startLevel {
  1890→			break
  1891→		}
  1892→
  1893→		stmt := p.parseStatement()
  1894→		if stmt != nil {
  1895→			statements = append(statements, stmt)
  1896→		}
  1897→
  1898→		// After parsing a statement, we need to advance.
  1899→		// But if we're already at NEWLINE/DEDENT/EOF, the next iteration will handle it.
  1900→		// Only advance if we're still on the last token of the statement.
  1901→		if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1902→			p.nextToken()
  1903→		}
  1904→	}
  1905→
  1906→	return statements
  1907→}
  1908→
  1909→// parseBodyStatements parses multiple statements inside a branch body
  1910→// (IF choice, CASE choice, ALT case, WHILE). Called after the caller has
  1911→// consumed the INDENT token and advanced into the body.
  1912→// Returns all statements found at this indentation level.
  1913→func (p *Parser) parseBodyStatements() []ast.Statement {
  1914→	var statements []ast.Statement
  1915→	startLevel := p.indentLevel
Thinking

Now I have the complete picture. Let me also look at the Receive AST node to see if it has fields for indexed targets.

Grep
{
  "pattern": "type Receive struct",
  "path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
  "output_mode": "content",
  "-A": 10,
  "-n": true
}
374:type Receive struct {
375-	Token          lexer.Token  // the ? token
376-	Channel        string       // channel name
377-	ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x
378-	Variable       string       // variable to receive into (simple receive)
379-	Variables      []string     // additional variables for sequential receives (c ? x ; y)
380-}
381-
382-func (r *Receive) statementNode()       {}
383-func (r *Receive) TokenLiteral() string { return r.Token.Literal }
384-
Thinking

I see — the Receive struct has Variable as a string, not an expression. It can't represent indexed targets like flags[0]. The variable is always assumed to be a simple identifier.

Now let me also check the ALT case struct.

Grep
{
  "pattern": "type AltCase struct",
  "path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
  "output_mode": "content",
  "-A": 15,
  "-n": true
}
398:type AltCase struct {
399-	Guard          Expression   // optional guard condition (nil if no guard)
400-	Channel        string       // channel name
401-	ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT
402-	Variable       string       // variable to receive into
403-	Body           []Statement  // the body to execute
404-	IsTimer        bool         // true if this is a timer AFTER case
405-	IsSkip         bool         // true if this is a guarded SKIP case (guard & SKIP)
406-	Timer          string       // timer name (when IsTimer)
407-	Deadline       Expression   // AFTER deadline expression (when IsTimer)
408-	Declarations   []Statement  / scoped declarations before channel input (e.g., BYTE ch:)
409-}
410-
411-/ TimerDecl represents a timer declaration: TIMER tim:
412-type TimerDecl struct {
413-	Token lexer.Token // the TIMER token

Now I have a complete understanding. Let me check the TODO.md for what's already tracked.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/TODO.md"
}
     1→# occam2go — Implementation Status
     2→
     3→## Fully Implemented
     4→
     5→### Core Constructs
     6→- **SEQ** — Sequential execution, with replicators (`SEQ i = 0 FOR n`) and optional STEP
     7→- **PAR** — Parallel execution via goroutines + sync.WaitGroup, with replicators
     8→- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators; supports multi-statement bodies (declarations scoped before process)
     9→- **WHILE** — Loops, maps to Go `for` loops; supports multi-statement bodies
    10→- **CASE** — Pattern matching with multiple cases and ELSE branch; supports multi-statement bodies
    11→- **ALT / PRI ALT** — Channel alternation, maps to Go `select`; supports boolean guards, timer timeouts, multi-statement bodies, and replicators (`ALT i = 0 FOR n` using `reflect.Select`). PRI ALT treated identically (Go has no priority select).
    12→- **PRI PAR** — Priority parallel, treated identically to PAR (Go goroutines have no priority)
    13→- **SKIP** — No-op process
    14→- **STOP** — Error + deadlock
    15→
    16→### Data Types & Declarations
    17→- **INT, INT16, INT32, INT64, BYTE, BOOL, REAL, REAL32, REAL64** — Scalar types (INT16/32/64 map to int16/32/64, REAL/REAL64 map to float64, REAL32 maps to float32)
    18→- **Variable declarations** — `INT x, y, z:`
    19→- **Arrays** — `[n]TYPE arr:` with index expressions; multi-dimensional `[n][m]TYPE` with nested init loops
    20→- **Channels** — `CHAN OF TYPE c:` with send (`!`) and receive (`?`); `CHAN BYTE` shorthand (without `OF`)
    21→- **Channel arrays** — `[n]CHAN OF TYPE cs:` with indexed send/receive; multi-dimensional `[n][m]CHAN OF TYPE` with nested init loops; `[]CHAN`, `[][]CHAN`, etc. proc params
    22→- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only); direction annotations at call sites (`out!`, `in?`) accepted and ignored
    23→- **Timers** — `TIMER tim:` with reads and `AFTER` expressions
    24→- **Abbreviations** — `VAL INT x IS 1:`, `INT y IS z:`, untyped `VAL x IS expr:` — named constants and aliases
    25→- **INITIAL declarations** — `INITIAL INT x IS 42:` — mutable variables with initial values
    26→- **Byte literals** — `'A'`, `'0'` with occam escape sequences (`*n`, `*c`, `*t`)
    27→- **Hex integer literals** — `#FF`, `#80000000`
    28→
    29→### Procedures & Functions
    30→- **PROC** — Declaration with VAL, reference, CHAN OF, and open array (`[]TYPE`) parameters
    31→- **PROC calls** — With automatic `&`/`*` for reference params, pass-through for channels
    32→- **FUNCTION (IS form)** — `INT FUNCTION square(VAL INT x) IS x * x`
    33→- **FUNCTION (VALOF form)** — Local declarations + VALOF body + RESULT
    34→- **Multi-result FUNCTIONs** — `INT, INT FUNCTION f(...)` returning multiple values via `RESULT a, b`
    35→- **Nested PROCs/FUNCTIONs** — Local definitions inside a PROC body, compiled as Go closures
    36→- **KRoC-style colon terminators** — Optional `:` at end of PROC/FUNCTION body
    37→- **INLINE modifier** — `INT INLINE FUNCTION f(...)` — accepted and ignored (optimization hint only)
    38→- **Built-in print** — `print.int`, `print.bool`, `print.string`, `print.newline`
    39→
    40→### Expressions & Operators
    41→- **Arithmetic** — `+`, `-`, `*`, `/`, `\` (modulo)
    42→- **Comparison** — `=`, `<>`, `<`, `>`, `<=`, `>=`
    43→- **Logical** — `AND`, `OR`, `NOT`
    44→- **Bitwise** — `/\`, `\/`, `><`, `~`, `<<`, `>>`
    45→- **AFTER** — As boolean expression (maps to `>`)
    46→- **Parenthesized expressions**
    47→- **Array indexing** — `arr[i]`, `arr[expr]`, multi-dimensional `grid[i][j]`
    48→- **String literals** — Double-quoted strings
    49→- **Type conversions** — `INT expr`, `INT16 expr`, `INT32 expr`, `INT64 expr`, `BYTE expr`, `BOOL expr`, `REAL32 expr`, `REAL64 expr` (including BOOL↔numeric conversions, and ROUND/TRUNC qualifiers for float↔int conversions)
    50→- **Checked arithmetic** — `PLUS`, `MINUS`, `TIMES` — modular (wrapping) operators
    51→- **MOSTNEG/MOSTPOS** — Type min/max constants for INT, INT16, INT32, INT64, BYTE, REAL32, REAL64
    52→- **SIZE operator** — `SIZE arr`, `SIZE "str"` maps to `len()`
    53→- **Array slices** — `[arr FROM n FOR m]` with slice assignment
    54→- **Array literals** — `[1, 2, 3]` — inline array/table expressions
    55→- **Multi-assignment** — `a, b := f(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`
    56→- **Multi-line expression continuation** — Binary operators and `:=` at end of line continue expression on next line
    57→
    58→### Protocols
    59→- **Simple** — `PROTOCOL SIG IS INT` (type alias)
    60→- **Sequential** — `PROTOCOL PAIR IS INT ; BYTE` (struct)
    61→- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types), including dotted tag names (`bar.data`, `bar.terminate`)
    62→
    63→### Records
    64→- **RECORD** — Struct types with field access via bracket syntax (`p[x]`)
    65→
    66→### Type Reinterpretation & Intrinsics
    67→- **RETYPES** — Bit-level type reinterpretation (`VAL INT X RETYPES X :` for float32→int, `VAL [2]INT X RETYPES X :` for float64→int pair)
    68→- **Transputer intrinsics** — `LONGPROD`, `LONGDIV`, `LONGSUM`, `LONGDIFF`, `NORMALISE`, `SHIFTLEFT`, `SHIFTRIGHT` — extended-precision arithmetic as Go helper functions
    69→- **CAUSEERROR** — Error-raising primitive, maps to `panic("CAUSEERROR")`
    70→
    71→### Preprocessor
    72→- **`#IF` / `#ELSE` / `#ENDIF`** — Conditional compilation with `TRUE`, `FALSE`, `DEFINED()`, `NOT`, equality
    73→- **`#DEFINE`** — Symbol definition
    74→- **`#INCLUDE`** — File inclusion with search paths and include guards
    75→- **`#COMMENT` / `#PRAGMA` / `#USE`** — Ignored (blank lines)
    76→- **Predefined symbols** — `TARGET.BITS.PER.WORD = 64`
    77→
    78→### Tooling
    79→- **gen-module** — Generate `.module` files from KRoC SConscript build files
    80→
    81→---
    82→
    83→## Not Yet Implemented
    84→
    85→### Required for shared_screen module (extends course module)
    86→
    87→| Feature | Notes | Used in |
    88→|---------|-------|---------|
    89→| **`DATA TYPE X IS TYPE:`** | Simple type alias (e.g. `DATA TYPE COLOUR IS BYTE:`). | shared_screen.inc |
    90→| **`DATA TYPE X RECORD`** | Alternative record syntax (vs current `RECORD X`). | shared_screen.inc |
    91→| **Counted array protocol** | `BYTE::[]BYTE` — length-prefixed array in protocols. | shared_screen.inc, shared_screen.occ |
    92→| **`RESULT` param qualifier** | `RESULT INT len` on PROC params (output-only, like a write-only reference). | float_io.occ |
    93→
    94→### Other language features
    95→
    96→| Feature | Notes |
    97→|---------|-------|
    98→| ~~**PRI ALT / PRI PAR**~~ | ~~Priority variants of ALT and PAR.~~ **Implemented** — treated as ALT/PAR (Go has no priority select). |
    99→| **PLACED PAR** | Assigning processes to specific hardware. |
   100→| **PORT OF** | Hardware port mapping. |
   101→| **`VAL []BYTE` abbreviations** | `VAL []BYTE cmap IS "0123456789ABCDEF":` — named string constants. |
   102→| **`#PRAGMA DEFINED`** | Compiler hint to suppress definedness warnings. Can be ignored. |
   103→
Thinking

Now I have a comprehensive understanding. Let me summarize the findings for the user.

There are two remaining issues preventing life.occ from parsing:

  1. Receive into indexed targetsch ? flags[0], link[x][y][d] ? state.of.neighbour[d]
  2. The Receive AST node uses Variable string which can't represent indexed targets
  3. Both parseReceive() and parseIndexedOperation() only accept a simple IDENT after ?
  4. The same limitation exists in ALT case parsing (AltCase.Variable is a string)
  5. This also blocks sequential receives where the targets themselves are indexed expressions

  6. Variant receive with scoped declarations in case bodiescontrol ? CASE ... evolve -> BOOL next.state : SEQ ...

  7. The variant receive parser processes each case body as a single statement, but can't handle a declaration followed by its scope body
  8. In occam, BOOL x : SEQ ... is a declaration that scopes over the following process, and the parser needs to recognize this pattern within case bodies

I'm also noticing that issue #74 about multi-dimensional arrays shows as open, but the codebase actually handles 2D and even 3D arrays in various contexts. The real limitation might be narrower than the issue suggests. Additionally, the life.occ example relies on runtime library functions that won't be available during transpilation, though that's a runtime concern rather than a parsing problem.

Here's a comprehensive analysis of where life.occ stands now.

Status

Of the 7 blocking issues (#73–#79), 6 are closed and the features work correctly. #74 (multi-dimensional arrays) is still listed as open, but 2D and 3D array declarations, parameters, and indexing actually do work now. The remaining parse failures are from two distinct bugs:

Remaining Issue 1: Receive into indexed target variables

Lines affected: 57, 225 (and others)

The parser only accepts a simple identifier after ? in channel receive. Indexed targets like flags[0] or state.of.neighbour[d] fail.

-- Works:
link[0][0][0] ? x

-- Fails:
link[0][0][0] ? flags[0]              -- indexed target
sense[x][y] ? changed; next.state     -- (this works since changed/next.state are plain idents)

Root cause: Both parseReceive() (line 1291) and parseIndexedOperation() (line 789) do p.expectPeek(lexer.IDENT) after ?, which only accepts a bare identifier. The Receive AST node stores Variable string — it can't represent an indexed target. The leftover [0] tokens then get misinterpreted as an array declaration, producing the "expected type after array size" error.

Fix scope: AST (Receive.Variable → support indexed targets), parser (both parseReceive and parseIndexedOperation), codegen (generate indexed receive targets), and the same in ALT case parsing.

Remaining Issue 2: Variant receive with scoped declarations in case bodies

Lines affected: 99–117

When a variant ? CASE body starts with a declaration (e.g., BOOL next.state :) followed by a compound statement, parsing fails.

-- Works:
control ? CASE
  evolve
    SEQ              -- single compound statement, fine
      ...

-- Fails:
control ? CASE
  evolve
    BOOL next.state :   -- declaration scoping over SEQ
    SEQ
      ...

Root cause: parseVariantReceive() at line 1387 calls p.parseStatement() once per case body. A single statement (even SEQ) is fine, but a scoped declaration (BOOL x : followed by SEQ ...) needs block-level parsing that handles the declaration-scopes-over-process pattern.

Fix scope: Parser only — change the variant case body parsing to use parseBodyStatements() (or similar) instead of a single parseStatement() call, matching how IF/CASE/ALT bodies already work.

Beyond parsing: runtime dependencies

Even after fixing these two parser bugs, life.occ won't run standalone because it depends on external library routines not defined in the file: - write.string, write.formatted (output library) - CHAN OF DATA.ITEM, data.int (formatted I/O protocol)
- terminal.keyboard, terminal.screen (system channels)

These would need to be stubbed or provided by transpiling the course module alongside.

Recommendation

Issue #74 can likely be closed (or narrowed) since multi-dimensional arrays work. The two bugs above should be filed as new issues — they're both well-scoped parser fixes that don't require architectural changes.

Thank you. Please go ahead and close #74 then open the two new issues you mentioned above.

$ Bash
Close issue #74
gh issue close 74 --comment "Closing — multi-dimensional arrays (2D and 3D) are now fully supported for declarations, parameters, and indexing. The remaining parse failures in life.occ are caused by two separate bugs filed as new issues."
✓ Closed issue codeassociates/occam2go#74 (Support multi-dimensional arrays (declaration, parameters, indexing))
$ Bash
Create issue for indexed receive targets
gh issue create --title "Support receive into indexed target variables" --body "$(cat <<'EOF'
## Description

The parser only accepts a simple identifier after `?` in channel receive statements. Indexed target variables like `flags[0]` or `state.of.neighbour[d]` cause parse failures.

## Occam2 syntax

```occam
[8]CHAN OF INT link :
[8]BOOL flags :
SEQ
  link[0] ? flags[0]
```

```occam
link[nx[d]][ny[d]][d] ? state.of.neighbour[d]
```

## Current behavior

```
expected type after array size, got NEWLINE
```

The parser consumes the variable name (`flags`) but leaves the index expression (`[0]`) unprocessed. The leftover `[` token is then misinterpreted as the start of an array declaration.

## Root cause

Both `parseReceive()` (line 1291) and `parseIndexedOperation()` (line 789) do `p.expectPeek(lexer.IDENT)` after `?`, accepting only a bare identifier. The `Receive` AST node stores `Variable string` which cannot represent an indexed target. The same limitation exists in ALT case parsing (`AltCase.Variable` is also a `string`).

## Expected Go output

```go
// link[0] ? flags[0]
flags[0] = <-link[0]

// link[nx[d]][ny[d]][d] ? state.of.neighbour[d]
state_of_neighbour[d] = <-link[nx[d]][ny[d]][d]
```

## Fix scope

- **AST**: Extend `Receive.Variable` (and `AltCase.Variable`) to support indexed targets — either add a `VariableIndices []Expression` field or change `Variable` to an `Expression`
- **Parser**: Update `parseReceive()`, `parseIndexedOperation()` receive handling, and ALT case parsing to accept `IDENT[expr]` after `?`
- **Codegen**: Generate indexed receive targets

## Context

Found in `historical-examples/life.occ` (issue #72). Affects lines 57, 225, and any other receive-into-indexed-array pattern.
EOF
)"
https://github.com/codeassociates/occam2go/issues/85
$ Bash
Create issue for variant receive scoped decls
gh issue create --title "Support scoped declarations in variant receive case bodies" --body "$(cat <<'EOF'
## Description

The variant protocol receive (`? CASE`) parser only handles a single statement per case body. When a case body starts with a scoped declaration (e.g., `BOOL next.state :`) followed by a compound statement (`SEQ ...`), parsing fails.

## Occam2 syntax

```occam
control ? CASE
  set.state; state
    SKIP
  evolve
    BOOL next.state :
    SEQ
      next.state := TRUE
      state := next.state
  terminate
    not.finished := FALSE
```

The `evolve` case body has a declaration (`BOOL next.state :`) that scopes over the `SEQ` block — this is standard occam scoping where declarations scope over the following process.

## Current behavior

```
expected variant tag name, got SEQ
expected variant tag name, got INDENT
expected variant tag name, got :=
...
```

The parser calls `parseStatement()` once, which parses the `BOOL next.state :` declaration and returns. The remaining `SEQ ...` block is left unprocessed, and its tokens are misinterpreted as variant tag names.

## Root cause

`parseVariantReceive()` at line 1387 calls `p.parseStatement()` once per case body. This handles single statements (including compound ones like `SEQ`) but not the declaration-scopes-over-process pattern where a declaration is followed by a process at the same indent level.

## Expected behavior

Case bodies should support multi-statement blocks with scoped declarations, matching how IF, CASE, and ALT bodies already work (via `parseBodyStatements()`).

## Fix scope

- **Parser only**: Change the variant case body parsing to use block-level parsing (e.g., `parseBodyStatements()` or similar) instead of a single `parseStatement()` call

## Context

Found in `historical-examples/life.occ` (issue #72), lines 99–117 (the `cell` proc's `control ? CASE` block).
EOF
)"
https://github.com/codeassociates/occam2go/issues/86

Done:

Thank you. Please go ahead and work on a fix for #86.

EnterPlanMode
{}
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.
Task
Explore variant receive and body parsing patterns
{
  "subagent_type": "Explore",
  "prompt": "I need to fix issue #86: variant protocol receive (`? CASE`) case bodies don't support scoped declarations. The fix is in `parser/parser.go`.\n\nPlease explore these specific things:\n\n1. **`parseVariantReceive()`** (line 1308) — read the full function, especially line 1387 where `vc.Body = p.parseStatement()` is called. This is the bug: it only parses one statement.\n\n2. **`parseVariantReceiveWithIndex()`** (line 1409) — read this function too, it likely has the same bug.\n\n3. **How other constructs handle multi-statement bodies with scoped declarations** — look at how IF, CASE, and ALT parse their case/branch bodies. Specifically:\n   - `parseCaseStatement()` — how does it handle case bodies?\n   - `parseIfStatement()` — how does it handle IF branch bodies?\n   - `parseAltCases()` — how does it handle ALT case bodies?\n   Look for patterns where `parseBodyStatements()` is used, or where declarations are handled before a process.\n\n4. **`parseBodyStatements()`** (around line 1913) — read this function to understand how it handles blocks with declarations.\n\n5. **The `VariantCase` and `VariantReceive` AST nodes** in `ast/ast.go` — what does the `Body` field look like? Is it `Statement` (single) or `[]Statement` (multiple)?\n\n6. **Codegen for variant receive** in `codegen/codegen.go` — search for `VariantReceive` or `VariantCase` to see how variant case bodies are generated. This will need to handle `[]Statement` if the AST changes.\n\nThe goal is to understand the exact pattern used elsewhere so we can replicate it for variant receive case bodies."
}
[
  {
    "type": "text",
    "text": "## Exploration Complete: Issue #86 - Variant Protocol Receive Case Bodies\n\nI have thoroughly explored the codebase to understand the bug and the patterns used elsewhere. Here's my comprehensive analysis:\n\n### The Bug (Issue #86)\n\n**Location:** `parser/parser.go` lines 1387 and 1482\n\n**Problem:** Both `parseVariantReceive()` and `parseVariantReceiveWithIndex()` call:\n```go\nvc.Body = p.parseStatement()  // Line 1387 and 1482\n```\n\nThis parses **only one statement** per variant case body, but case bodies should support **multiple statements** plus **scoped declarations** (like `INT x:` or `VAL y IS 42:` inside the case).\n\n---\n\n### How Other Constructs Handle Multi-Statement Bodies\n\nAll of these use the **same proven pattern**:\n\n#### 1. **IF Statement** (lines 2554-2644)\n- Line 2631: `choice.Body = p.parseBodyStatements()`\n- AST: `IfChoice.Body` is `[]Statement` (line 207 in ast.go)\n\n#### 2. **CASE Statement** (lines 2646-2733)\n- Line 2721: `choice.Body = p.parseBodyStatements()`\n- AST: `CaseChoice.Body` is `[]Statement` (line 226 in ast.go)\n\n#### 3. **ALT Cases** (lines 1722-1854)\n- Line 1851: `altCase.Body = p.parseBodyStatements()`\n- AST: `AltCase.Body` is `[]Statement` (line 403 in ast.go)\n- **BONUS:** ALT also supports **scoped declarations** before the channel input (lines 1725-1739):\n  ```go\n  for p.isAltDeclStart() {\n      stmt := p.parseStatement()\n      if stmt != nil {\n          altCase.Declarations = append(altCase.Declarations, stmt)\n      }\n      ...\n  }\n  ```\n\n---\n\n### The `parseBodyStatements()` Function (lines 1913-1965)\n\nThis is the **standard multi-statement block parser** used everywhere:\n\n```go\nfunc (p *Parser) parseBodyStatements() []ast.Statement {\n    var statements []ast.Statement\n    startLevel := p.indentLevel\n    \n    for !p.curTokenIs(lexer.EOF) {\n        // Skip newlines and handle DEDENT tokens\n        // Check for scope drops (indentLevel < startLevel)\n        // Parse each statement\n        stmt := p.parseStatement()\n        if stmt != nil {\n            statements = append(statements, stmt)\n        }\n        // Advance if needed\n        ...\n    }\n    return statements\n}\n```\n\nKey features:\n- Returns `[]ast.Statement` instead of single `Statement`\n- Tracks `startLevel` to detect when the indented block ends\n- Loops until DEDENT signals end of block\n- Handles both declarations and regular statements\n\n---\n\n### The AST Problem: `VariantCase.Body` is Single Statement\n\n**Location:** `ast/ast.go` lines 455-459\n\n```go\ntype VariantCase struct {\n    Tag       string    // variant tag name\n    Variables []string  // variables to bind payload fields\n    Body      Statement // ← BUG: Single Statement, should be []Statement\n}\n```\n\nCompare to working constructs:\n- `IfChoice.Body`: `[]Statement` (line 209)\n- `CaseChoice.Body`: `[]Statement` (line 226)\n- `AltCase.Body`: `[]Statement` (line 403)\n\n---\n\n### The Codegen Impact\n\n**Location:** `codegen/codegen.go` lines 1406-1426\n\nCurrent code treats `Body` as single statement:\n```go\nfunc (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {\n    ...\n    for _, vc := range vr.Cases {\n        ...\n        if vc.Body != nil {\n            g.generateStatement(vc.Body)  // ← Calls on single Statement\n        }\n        ...\n    }\n}\n```\n\nThis needs to change to loop over statements like IF/CASE do:\n```go\nfor _, s := range vc.Body {  // ← Loop over []Statement\n    g.generateStatement(s)\n}\n```\n\n---\n\n### Summary of Required Changes\n\n**Three files must change:**\n\n1. **`ast/ast.go`** (line 458):\n   - Change `Body Statement` → `Body []Statement`\n\n2. **`parser/parser.go`** (two functions):\n   - `parseVariantReceive()` line 1387: `vc.Body = p.parseBodyStatements()`\n   - `parseVariantReceiveWithIndex()` line 1482: `vc.Body = p.parseBodyStatements()`\n   - Optional: Add scoped declarations support (like ALT) if desired\n\n3. **`codegen/codegen.go`** (line 1420-1422):\n   ```go\n   for _, s := range vc.Body {\n       g.generateStatement(s)\n   }\n   ```\n\n---\n\n### Pattern Files to Reference\n\nWhen implementing the fix, refer to these working implementations:\n- **parseBodyStatements()**: `/home/david/projects/code-associates/occam2go/parser/parser.go` lines 1913-1965\n- **ALT pattern (scoped decls)**: `/home/david/projects/code-associates/occam2go/parser/parser.go` lines 1722-1854\n- **IF codegen**: `/home/david/projects/code-associates/occam2go/codegen/codegen.go` lines 2400-2409\n- **CASE codegen**: `/home/david/projects/code-associates/occam2go/codegen/codegen.go` lines 2574-2602\n\nAll three constructs follow the identical pattern for handling multi-statement bodies with proper indentation tracking and DEDENT detection."
  },
  {
    "type": "text",
    "text": "agentId: a11a84e1e4e4e4d4c (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 40803\ntool_uses: 18\nduration_ms: 33757</usage>"
  }
]
Thinking

The exploration is very thorough. Let me now read the key areas to verify and write the plan.

Let me verify the exact code at the key locations before writing the plan.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
  "offset": 450,
  "limit": 15
}
   450→	Channel        string
   451→	ChannelIndices []Expression // non-empty for cs[i] ? CASE ... or cs[i][j] ? CASE ...
   452→	Cases          []VariantCase
   453→}
   454→
   455→type VariantCase struct {
   456→	Tag       string    // variant tag name
   457→	Variables []string  // variables to bind payload fields
   458→	Body      Statement
   459→}
   460→
   461→func (vr *VariantReceive) statementNode()       {}
   462→func (vr *VariantReceive) TokenLiteral() string { return vr.Token.Literal }
   463→
   464→// RecordDecl represents a record type declaration: RECORD POINT { INT x: INT y: }
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1380,
  "limit": 20
}
  1380→		for p.peekTokenIs(lexer.NEWLINE) {
  1381→			p.nextToken()
  1382→		}
  1383→
  1384→		if p.peekTokenIs(lexer.INDENT) {
  1385→			p.nextToken() // consume INDENT
  1386→			p.nextToken() // move to body
  1387→			vc.Body = p.parseStatement()
  1388→
  1389→			// Advance past the last token of the statement if needed
  1390→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1391→				p.nextToken()
  1392→			}
  1393→		}
  1394→
  1395→		stmt.Cases = append(stmt.Cases, vc)
  1396→
  1397→		// No-progress guard: if we haven't moved, break to prevent infinite loop
  1398→		if p.curToken == prevToken && p.peekToken == prevPeek {
  1399→			p.nextToken() // force progress
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "offset": 1470,
  "limit": 25
}
  1470→				return stmt
  1471→			}
  1472→			vc.Variables = append(vc.Variables, p.curToken.Literal)
  1473→		}
  1474→
  1475→		for p.peekTokenIs(lexer.NEWLINE) {
  1476→			p.nextToken()
  1477→		}
  1478→
  1479→		if p.peekTokenIs(lexer.INDENT) {
  1480→			p.nextToken() // consume INDENT
  1481→			p.nextToken() // move to body
  1482→			vc.Body = p.parseStatement()
  1483→
  1484→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1485→				p.nextToken()
  1486→			}
  1487→		}
  1488→
  1489→		stmt.Cases = append(stmt.Cases, vc)
  1490→
  1491→		// No-progress guard: if we haven't moved, break to prevent infinite loop
  1492→		if p.curToken == prevToken && p.peekToken == prevPeek {
  1493→			p.nextToken() // force progress
  1494→			if p.curToken == prevToken {

Now let me check the codegen side.

Grep
{
  "pattern": "func.*generateVariantReceive",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
1406:func (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 1406,
  "limit": 50
}
  1406→func (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {
  1407→	protoName := g.chanProtocols[vr.Channel]
  1408→	gProtoName := goIdent(protoName)
  1409→	chanRef := goIdent(vr.Channel)
  1410→	if len(vr.ChannelIndices) > 0 {
  1411→		chanRef += g.generateIndicesStr(vr.ChannelIndices)
  1412→	}
  1413→	g.writeLine(fmt.Sprintf("switch _v := (<-%s).(type) {", chanRef))
  1414→	for _, vc := range vr.Cases {
  1415→		g.writeLine(fmt.Sprintf("case _proto_%s_%s:", gProtoName, goIdent(vc.Tag)))
  1416→		g.indent++
  1417→		for i, v := range vc.Variables {
  1418→			g.writeLine(fmt.Sprintf("%s = _v._%d", goIdent(v), i))
  1419→		}
  1420→		if vc.Body != nil {
  1421→			g.generateStatement(vc.Body)
  1422→		}
  1423→		g.indent--
  1424→	}
  1425→	g.writeLine("}")
  1426→}
  1427→
  1428→func (g *Generator) isVariantTag(protoName, tagName string) bool {
  1429→	proto := g.protocolDefs[protoName]
  1430→	if proto == nil {
  1431→		return false
  1432→	}
  1433→	for _, v := range proto.Variants {
  1434→		if v.Tag == tagName {
  1435→			return true
  1436→		}
  1437→	}
  1438→	return false
  1439→}
  1440→
  1441→func (g *Generator) collectChanProtocols(stmt ast.Statement) {
  1442→	switch s := stmt.(type) {
  1443→	case *ast.ChanDecl:
  1444→		if _, ok := g.protocolDefs[s.ElemType]; ok {
  1445→			for _, name := range s.Names {
  1446→				g.chanProtocols[name] = s.ElemType
  1447→			}
  1448→		}
  1449→	case *ast.SeqBlock:
  1450→		for _, inner := range s.Statements {
  1451→			g.collectChanProtocols(inner)
  1452→		}
  1453→	case *ast.ParBlock:
  1454→		for _, inner := range s.Statements {
  1455→			g.collectChanProtocols(inner)

Now let me also check if there's a containsVariantReceive scanner or similar that walks the AST (since it accesses vc.Body).

Grep
{
  "pattern": "VariantCase|VariantReceive|vc\\.Body",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true
}
567:	case *ast.VariantReceive:
640:	case *ast.VariantReceive:
716:	case *ast.VariantReceive:
789:	case *ast.VariantReceive:
910:	case *ast.VariantReceive:
1065:	case *ast.VariantReceive:
1066:		g.generateVariantReceive(s)
1406:func (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {
1420:		if vc.Body != nil {
1421:			g.generateStatement(vc.Body)
3085:	case *ast.VariantReceive:
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 564,
  "limit": 10
}
   564→				}
   565→			}
   566→		}
   567→	case *ast.VariantReceive:
   568→		for _, c := range s.Cases {
   569→			if c.Body != nil && g.containsPar(c.Body) {
   570→				return true
   571→			}
   572→		}
   573→	}
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 637,
  "limit": 10
}
   637→				}
   638→			}
   639→		}
   640→	case *ast.VariantReceive:
   641→		for _, c := range s.Cases {
   642→			if c.Body != nil && g.containsPrint(c.Body) {
   643→				return true
   644→			}
   645→		}
   646→	}
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 713,
  "limit": 10
}
   713→				}
   714→			}
   715→		}
   716→	case *ast.VariantReceive:
   717→		for _, c := range s.Cases {
   718→			if c.Body != nil && g.containsTimer(c.Body) {
   719→				return true
   720→			}
   721→		}
   722→	}
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 786,
  "limit": 10
}
   786→				}
   787→			}
   788→		}
   789→	case *ast.VariantReceive:
   790→		for _, c := range s.Cases {
   791→			if c.Body != nil && g.containsStop(c.Body) {
   792→				return true
   793→			}
   794→		}
   795→	}
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 907,
  "limit": 10
}
   907→				}
   908→			}
   909→		}
   910→	case *ast.VariantReceive:
   911→		for _, c := range s.Cases {
   912→			if c.Body != nil && g.containsMostExpr(c.Body) {
   913→				return true
   914→			}
   915→		}
   916→	}
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "offset": 3082,
  "limit": 10
}
  3082→				}
  3083→			}
  3084→		}
  3085→	case *ast.VariantReceive:
  3086→		for _, c := range s.Cases {
  3087→			if c.Body != nil && g.walkStatements(c.Body, fn) {
  3088→				return true
  3089→			}
  3090→		}
  3091→	}
Thinking

Now I have a complete picture. Let me also check how IF/CASE codegen handles their []Statement bodies to confirm the pattern.

Let me check how IF/CASE codegen iterates over their []Statement bodies for the pattern to follow.

Grep
{
  "pattern": "choice\\.Body",
  "path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
  "output_mode": "content",
  "-n": true,
  "-B": 1,
  "-A": 3
}
552-			}
553:			for _, inner := range choice.Body {
554-				if g.containsPar(inner) {
555-					return true
556-				}
--
560-		for _, choice := range s.Choices {
561:			for _, inner := range choice.Body {
562-				if g.containsPar(inner) {
563-					return true
564-				}
--
625-			}
626:			for _, inner := range choice.Body {
627-				if g.containsPrint(inner) {
628-					return true
629-				}
--
633-		for _, choice := range s.Choices {
634:			for _, inner := range choice.Body {
635-				if g.containsPrint(inner) {
636-					return true
637-				}
--
701-			}
702:			for _, inner := range choice.Body {
703-				if g.containsTimer(inner) {
704-					return true
705-				}
--
709-		for _, choice := range s.Choices {
710:			for _, inner := range choice.Body {
711-				if g.containsTimer(inner) {
712-					return true
713-				}
--
774-			}
775:			for _, inner := range choice.Body {
776-				if g.containsStop(inner) {
777-					return true
778-				}
--
782-		for _, choice := range s.Choices {
783:			for _, inner := range choice.Body {
784-				if g.containsStop(inner) {
785-					return true
786-				}
--
864-			}
865:			for _, inner := range choice.Body {
866-				if g.containsMostExpr(inner) {
867-					return true
868-				}
--
880-			}
881:			for _, inner := range choice.Body {
882-				if g.containsMostExpr(inner) {
883-					return true
884-				}
--
1481-			}
1482:			for _, inner := range choice.Body {
1483-				g.collectChanProtocols(inner)
1484-			}
1485-		}
--
1487-		for _, choice := range s.Choices {
1488:			for _, inner := range choice.Body {
1489-				g.collectChanProtocols(inner)
1490-			}
1491-		}
--
1533-			}
1534:			for _, inner := range choice.Body {
1535-				g.collectBoolVars(inner)
1536-			}
1537-		}
--
1539-		for _, choice := range s.Choices {
1540:			for _, inner := range choice.Body {
1541-				g.collectBoolVars(inner)
1542-			}
1543-		}
--
1586-			}
1587:			for _, inner := range choice.Body {
1588-				g.collectRecordVars(inner)
1589-			}
1590-		}
--
1592-		for _, choice := range s.Choices {
1593:			for _, inner := range choice.Body {
1594-				g.collectRecordVars(inner)
1595-			}
1596-		}
--
2466-
2467:		for _, s := range choice.Body {
2468-			g.generateStatement(s)
2469-		}
2470-		if withinFlag && len(flagName) > 0 {
--
2508-
2509:			for _, s := range choice.Body {
2510-				g.generateStatement(s)
2511-			}
2512-
--
2537-			g.indent++
2538:			for _, s := range choice.Body {
2539-				g.generateStatement(s)
2540-			}
2541-			g.indent--
--
2594-		g.indent++
2595:		for _, s := range choice.Body {
2596-			g.generateStatement(s)
2597-		}
2598-		g.indent--
--
2896-			}
2897:			for _, inner := range choice.Body {
2898-				if g.containsRetypes(inner) {
2899-					return true
2900-				}
--
2904-		for _, choice := range s.Choices {
2905:			for _, inner := range choice.Body {
2906-				if g.containsRetypes(inner) {
2907-					return true
2908-				}
--
2962-			}
2963:			for _, inner := range choice.Body {
2964-				if g.containsAltReplicator(inner) {
2965-					return true
2966-				}
--
2970-		for _, choice := range s.Choices {
2971:			for _, inner := range choice.Body {
2972-				if g.containsAltReplicator(inner) {
2973-					return true
2974-				}
--
3039-			}
3040:			for _, inner := range choice.Body {
3041-				if g.walkStatements(inner, fn) {
3042-					return true
3043-				}
--
3055-			}
3056:			for _, inner := range choice.Body {
3057-				if g.walkStatements(inner, fn) {
3058-					return true
3059-				}

I now have a complete understanding. The change is straightforward — it follows the exact same pattern used by IF, CASE, and ALT. Let me write the plan.

📝 Write vectorized-soaring-flurry.md
/home/david/.claude/plans/vectorized-soaring-flurry.md
# Fix #86: Support scoped declarations in variant receive case bodies

## Context

Variant protocol receive (`ch ? CASE`) case bodies only support a single statement. When a case body has a scoped declaration (e.g., `BOOL x :`) followed by a compound statement (`SEQ ...`), parsing fails. This blocks `historical-examples/life.occ` (issue #72).

The fix follows the identical pattern already used by IF, CASE, and ALT — change `Body` from a single `Statement` to `[]Statement` and use `parseBodyStatements()`.

## Changes

### 1. AST — `ast/ast.go` line 458

Change `VariantCase.Body` from single to multi-statement:

```
Body      Statement    →    Body      []Statement
```

### 2. Parser — `parser/parser.go`

**`parseVariantReceive()`** (lines 1384-1393): Replace single-statement parse with `parseBodyStatements()`:

```go
// Before:
if p.peekTokenIs(lexer.INDENT) {
    p.nextToken() // consume INDENT
    p.nextToken() // move to body
    vc.Body = p.parseStatement()
    if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
        p.nextToken()
    }
}

// After:
if p.peekTokenIs(lexer.INDENT) {
    p.nextToken() // consume INDENT
    vc.Body = p.parseBodyStatements()
}
```

**`parseVariantReceiveWithIndex()`** (lines 1479-1487): Same change.

Note: `parseBodyStatements()` handles `p.nextToken()` internally (line 1924: "move past INDENT") and manages its own DEDENT/NEWLINE tracking, so the manual advance code is removed.

### 3. Codegen — `codegen/codegen.go`

**`generateVariantReceive()`** (lines 1420-1422): Loop over body statements:

```go
// Before:
if vc.Body != nil {
    g.generateStatement(vc.Body)
}

// After:
for _, s := range vc.Body {
    g.generateStatement(s)
}
```

**All `containsX()` scanner functions** that walk `vc.Body` — 6 occurrences (containsPar, containsPrint, containsTimer, containsStop, containsMostExpr, walkStatements) plus collectChanProtocols, collectBoolVars, collectRecordVars, containsRetypes, containsAltReplicator. Change from:

```go
if c.Body != nil && g.containsX(c.Body) {
```

to:

```go
for _, inner := range c.Body {
    if g.containsX(inner) {
        return true
    }
}
```

This matches the pattern used by IF/CASE `choice.Body` iteration throughout the file.

### 4. Tests

- Add a parser test in `parser/parser_test.go` for variant receive with scoped declarations
- Add an e2e test in `codegen/e2e_test.go` that transpiles, compiles, and runs a program using variant receive with declarations in case bodies

## Verification

```bash
go test ./...                    # all tests pass
# Specific test for the new feature:
cat > /tmp/test_variant_decl.occ << 'EOF'
PROTOCOL CMD
  CASE
    set.state; BOOL
    evolve
    terminate
:
PROC test(CHAN OF CMD control)
  BOOL state, not.finished :
  SEQ
    state := FALSE
    not.finished := TRUE
    WHILE not.finished
      control ? CASE
        set.state; state
          SKIP
        evolve
          BOOL next.state :
          SEQ
            next.state := TRUE
            state := next.state
        terminate
          not.finished := FALSE
:
CHAN OF CMD ch :
PAR
  test(ch)
  SEQ
    ch ! set.state; TRUE
    ch ! evolve
    ch ! terminate
EOF
./occam2go -o /tmp/test_variant_decl.go /tmp/test_variant_decl.occ
go run /tmp/test_variant_decl.go
```
File created successfully at: /home/david/.claude/plans/vectorized-soaring-flurry.md
ExitPlanMode
{
  "allowedPrompts": [
    {
      "tool": "Bash",
      "prompt": "run tests"
    }
  ],
  "plan": "# Fix #86: Support scoped declarations in variant receive case bodies\n\n## Context\n\nVariant protocol receive (`ch ? CASE`) case bodies only support a single statement. When a case body has a scoped declaration (e.g., `BOOL x :`) followed by a compound statement (`SEQ ...`), parsing fails. This blocks `historical-examples/life.occ` (issue #72).\n\nThe fix follows the identical pattern already used by IF, CASE, and ALT — change `Body` from a single `Statement` to `[]Statement` and use `parseBodyStatements()`.\n\n## Changes\n\n### 1. AST — `ast/ast.go` line 458\n\nChange `VariantCase.Body` from single to multi-statement:\n\n```\nBody      Statement    →    Body      []Statement\n```\n\n### 2. Parser — `parser/parser.go`\n\n**`parseVariantReceive()`** (lines 1384-1393): Replace single-statement parse with `parseBodyStatements()`:\n\n```go\n// Before:\nif p.peekTokenIs(lexer.INDENT) {\n    p.nextToken() // consume INDENT\n    p.nextToken() // move to body\n    vc.Body = p.parseStatement()\n    if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {\n        p.nextToken()\n    }\n}\n\n// After:\nif p.peekTokenIs(lexer.INDENT) {\n    p.nextToken() // consume INDENT\n    vc.Body = p.parseBodyStatements()\n}\n```\n\n**`parseVariantReceiveWithIndex()`** (lines 1479-1487): Same change.\n\nNote: `parseBodyStatements()` handles `p.nextToken()` internally (line 1924: \"move past INDENT\") and manages its own DEDENT/NEWLINE tracking, so the manual advance code is removed.\n\n### 3. Codegen — `codegen/codegen.go`\n\n**`generateVariantReceive()`** (lines 1420-1422): Loop over body statements:\n\n```go\n// Before:\nif vc.Body != nil {\n    g.generateStatement(vc.Body)\n}\n\n// After:\nfor _, s := range vc.Body {\n    g.generateStatement(s)\n}\n```\n\n**All `containsX()` scanner functions** that walk `vc.Body` — 6 occurrences (containsPar, containsPrint, containsTimer, containsStop, containsMostExpr, walkStatements) plus collectChanProtocols, collectBoolVars, collectRecordVars, containsRetypes, containsAltReplicator. Change from:\n\n```go\nif c.Body != nil && g.containsX(c.Body) {\n```\n\nto:\n\n```go\nfor _, inner := range c.Body {\n    if g.containsX(inner) {\n        return true\n    }\n}\n```\n\nThis matches the pattern used by IF/CASE `choice.Body` iteration throughout the file.\n\n### 4. Tests\n\n- Add a parser test in `parser/parser_test.go` for variant receive with scoped declarations\n- Add an e2e test in `codegen/e2e_test.go` that transpiles, compiles, and runs a program using variant receive with declarations in case bodies\n\n## Verification\n\n```bash\ngo test ./...                    # all tests pass\n# Specific test for the new feature:\ncat > /tmp/test_variant_decl.occ << 'EOF'\nPROTOCOL CMD\n  CASE\n    set.state; BOOL\n    evolve\n    terminate\n:\nPROC test(CHAN OF CMD control)\n  BOOL state, not.finished :\n  SEQ\n    state := FALSE\n    not.finished := TRUE\n    WHILE not.finished\n      control ? CASE\n        set.state; state\n          SKIP\n        evolve\n          BOOL next.state :\n          SEQ\n            next.state := TRUE\n            state := next.state\n        terminate\n          not.finished := FALSE\n:\nCHAN OF CMD ch :\nPAR\n  test(ch)\n  SEQ\n    ch ! set.state; TRUE\n    ch ! evolve\n    ch ! terminate\nEOF\n./occam2go -o /tmp/test_variant_decl.go /tmp/test_variant_decl.occ\ngo run /tmp/test_variant_decl.go\n```\n"
}
The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.

[Request interrupted by user for tool use]