title: Support multi-dimensional arrays (declaration, parameters, indexing)
state: OPEN
author: associate-1
labels:
comments: 0
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.Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach. In plan mode, you should: 1. Thoroughly explore the codebase to understand existing patterns 2. Identify similar features and architectural approaches 3. Consider multiple approaches and their trade-offs 4. Use AskUserQuestion if you need to clarify the approach 5. Design a concrete implementation strategy 6. When ready, use ExitPlanMode to present your plan for approval Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.
Let me explore the codebase to understand the current array handling across all relevant packages.
{
"subagent_type": "Explore",
"prompt": "Explore the occam2go transpiler codebase to understand how single-dimensional arrays are currently handled in the parser and AST. I need to understand:\n\n1. In `ast/ast.go`: Find all array-related AST nodes - especially `ArrayDecl`, `ChanArrayDecl`, any array type representation, and how dimensions are stored (single Size field vs list).\n\n2. In `parser/parser.go`: Find how arrays are parsed - look for:\n - Array declaration parsing (e.g. `[5]INT`, `[n]CHAN OF TYPE`)\n - Open array params in proc signatures (e.g. `[]INT`, `[]CHAN OF TYPE`)\n - Fixed-size array params\n - Array indexing in expressions\n - How `[` is handled at the start of declarations\n\n3. Look at the life.occ example file to see all multi-dimensional array usage patterns.\n\nBe very thorough - show me the relevant code sections with line numbers."
}{
"subagent_type": "Explore",
"prompt": "Explore the occam2go transpiler codebase to understand how single-dimensional arrays are currently handled in code generation and tests. I need to understand:\n\n1. In `codegen/codegen.go`: Find all array-related code generation:\n - How `ArrayDecl` / `ChanArrayDecl` are generated (the make() calls, init loops for channels)\n - How array indexing works in expressions (IndexExpression generation)\n - How array proc params are generated (open arrays, fixed-size arrays, channel arrays)\n - How array send/receive on indexed channels works\n\n2. In `codegen/codegen_test.go` and `codegen/e2e_test.go`: Find existing array tests\n\n3. In `parser/parser_test.go`: Find existing array parsing tests\n\nBe very thorough - show me the relevant code sections with line numbers."
}[
{
"type": "text",
"text": "Perfect! Now I have all the information I need. Let me compile the comprehensive report.\n\n## Comprehensive Array Handling Analysis for occam2go Transpiler\n\nBased on my thorough exploration of the codebase, here's the complete picture of how single-dimensional (and currently, multi-dimensional) arrays are handled:\n\n### 1. AST NODES (ast/ast.go)\n\n**Array-Related AST Nodes:**\n\n#### `ArrayDecl` (lines 46-55)\n```go\ntype ArrayDecl struct {\n Token lexer.Token // the [ token\n Size Expression // array size (single Expression, not a list!)\n Type string // element type (\"INT\", \"BYTE\", \"BOOL\", etc.)\n Names []string // variable names\n}\n```\n- **Key Point**: `Size` is a **single `Expression`**, not a list of dimensions\n- Used for declarations like: `[5]INT arr:` or `[n]CHAR buf:`\n- Currently **does NOT support multi-dimensional arrays as a single construct**\n- Lines 36-45 also show `VarDecl` for scalar variables for comparison\n\n#### `ChanDecl` (lines 348-358)\n```go\ntype ChanDecl struct {\n Token lexer.Token // the CHAN token\n ElemType string // the element type (INT, BYTE, etc.)\n Names []string // channel names\n IsArray bool // true for [n]CHAN OF TYPE\n Size Expression // array size when IsArray\n}\n```\n- Similar to `ArrayDecl`: `Size` is a **single Expression**\n- Used for: `[5]CHAN OF INT cs:` or `[]CHAN OF INT cs` (open channel array param)\n\n#### `ProcParam` (lines 143-154)\n```go\ntype ProcParam struct {\n IsVal bool // VAL parameter (pass by value)\n Type string // INT, BYTE, BOOL, etc.\n Name string\n IsChan bool // true if this is a CHAN OF <type> parameter\n IsChanArray bool // true for []CHAN OF TYPE params\n IsOpenArray bool // true for []TYPE params (open array)\n ChanElemType string // element type when IsChan (e.g., \"INT\")\n ChanDir string // \"?\" for input, \"!\" for output, \"\" for bidirectional\n ArraySize string // non-empty for fixed-size array params like [2]INT\n}\n```\n- **Three distinct array representation modes**:\n - `IsChanArray`: Open channel array `[]CHAN OF TYPE`\n - `IsOpenArray`: Open array `[]TYPE`\n - `ArraySize`: Fixed-size array `[2]INT` (stored as STRING, not Expression!)\n\n#### `Abbreviation` (lines 489-501)\n```go\ntype Abbreviation struct {\n Token lexer.Token // VAL, INITIAL, or type token\n IsVal bool // true for VAL abbreviations\n IsInitial bool // true for INITIAL declarations\n IsOpenArray bool // true for []TYPE abbreviations (e.g. VAL []BYTE)\n Type string // \"INT\", \"BYTE\", \"BOOL\", etc.\n Name string // variable name\n Value Expression // the expression\n}\n```\n- Used for: `VAL []BYTE str IS \"hello\":` (open array abbreviation)\n- Open arrays are marked with `IsOpenArray`, not a size expression\n\n#### `SliceExpr` (lines 478-487)\n```go\ntype SliceExpr struct {\n Token lexer.Token // the [ token\n Array Expression // the array being sliced\n Start Expression // start index\n Length Expression // number of elements\n}\n```\n- Used for: `[arr FROM n FOR m]` or `[arr FOR m]` (shorthand, start=0)\n\n#### `IndexExpr` (lines 338-346)\n```go\ntype IndexExpr struct {\n Token lexer.Token // the [ token\n Left Expression // the array expression\n Index Expression // the index expression\n}\n```\n- Single index only: `arr[i]`\n- **NOT used for multi-dimensional indexing** - instead chains of `IndexExpr` nodes are created for `arr[x][y][z]`\n\n#### `ArrayLiteral` (lines 503-510)\n```go\ntype ArrayLiteral struct {\n Token lexer.Token // the [ token\n Elements []Expression // the elements\n}\n```\n- Used for: `[1, 2, 3]` or `VAL x IS [1, 2, 3] :`\n\n#### `RetypesDecl` (lines 512-525)\n```go\ntype RetypesDecl struct {\n Token lexer.Token // the VAL token\n IsVal bool // always true for now\n TargetType string // \"INT\", \"REAL32\", etc.\n IsArray bool // true for [n]TYPE\n ArraySize Expression // array size when IsArray\n Name string // target variable name\n Source string // source variable name\n}\n```\n- Used for: `VAL INT X RETYPES Y :` or `VAL [2]INT X RETYPES Y :`\n\n---\n\n### 2. PARSER (parser/parser.go)\n\n#### **Array Declaration Parsing** (lines 530-628)\n\n`parseArrayDecl()` function:\n\n```go\nfunc (p *Parser) parseArrayDecl() ast.Statement {\n lbracketToken := p.curToken\n \n // Parse size expression after [\n p.nextToken()\n size := p.parseExpression(LOWEST) // Single size expression!\n \n // Check if this is a slice assignment: [arr FROM start FOR length]\n if p.peekTokenIs(lexer.FROM) || p.peekTokenIs(lexer.FOR) {\n return p.parseSliceAssignment(lbracketToken, size)\n }\n \n // Expect ]\n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n \n // Check if this is a channel array: [n]CHAN OF TYPE\n if p.peekTokenIs(lexer.CHAN) {\n // ... create ChanDecl with IsArray=true and Size\n }\n \n // Regular array declaration: [n]TYPE name:\n decl := &ast.ArrayDecl{Token: lbracketToken, Size: size}\n // ... expect type, parse names\n}\n```\n\n**Key insight:** Arrays are parsed with a **single size expression** only. Multi-dimensional arrays like `[5][3]INT arr:` are **NOT parsed as a single statement** but rather handled differently.\n\n#### **Multi-Dimensional Array Handling** (NOT directly supported)\n\nLooking at the life.occ example at lines 400-402:\n```occam\n[array.width][array.height][neighbours]CHAN OF STATE link :\n[array.width][array.height]CHAN OF COMMAND control :\n[array.width][array.height]CHAN OF RESPONSE sense :\n```\n\n**These are NOT parsed as `ArrayDecl` with multiple dimensions**. Instead:\n1. The parser sees `[array.width]` and creates a `ChanDecl` with `IsArray=true, Size=array.width`\n2. The next `[array.height]` is seen as **a separate operation** during code generation\n3. This happens because the transpiler creates **arrays of arrays** in Go, which is a different data structure\n\n#### **Fixed-Size Array Parameters** (lines 2070-2123)\n\nIn `parseProcParams()`:\n\n```go\n// Check for []CHAN OF <type>, []TYPE (open array), or [n]TYPE (fixed-size array)\nif p.curTokenIs(lexer.LBRACKET) {\n if p.peekTokenIs(lexer.RBRACKET) {\n // Open array: []CHAN OF TYPE or []TYPE\n // ... sets IsChanArray or IsOpenArray\n } else {\n // Fixed-size array: [n]TYPE\n p.nextToken() // move past [\n if !p.curTokenIs(lexer.INT) {\n p.addError(fmt.Sprintf(\"expected array size, got %s\", p.curToken.Type))\n return params\n }\n param.ArraySize = p.curToken.Literal // STORED AS STRING!\n // ... expect ], then type\n }\n}\n```\n\n**Critical difference**: `ArraySize` in `ProcParam` is stored as a **STRING** (line 153 in ast.go), not as an `Expression`.\n\n#### **Array Indexing in Expressions** (lines 2753-2808)\n\nIn `parsePrimaryExpression()`:\n\n```go\ncase lexer.LBRACKET:\n // Could be: [arr FROM start FOR length], [arr FOR length], or [expr, expr, ...] array literal\n lbracket := p.curToken\n p.nextToken() // move past [\n firstExpr := p.parseExpression(LOWEST)\n \n if p.peekTokenIs(lexer.COMMA) {\n // Array literal: [expr, expr, ...]\n elements := []ast.Expression{firstExpr}\n // ... parse remaining elements\n left = &ast.ArrayLiteral{Token: lbracket, Elements: elements}\n } else if p.peekTokenIs(lexer.RBRACKET) {\n // Single-element array literal: [expr]\n left = &ast.ArrayLiteral{Token: lbracket, Elements: []ast.Expression{firstExpr}}\n } else {\n // Slice expression: [arr FROM start FOR length] or [arr FOR length]\n // ... parse slice with start and length\n left = &ast.SliceExpr{...}\n }\n```\n\n#### **Array Indexing in Infix (lines 2865-2867)**\n\n```go\ncase lexer.LBRACKET:\n p.nextToken()\n left = p.parseIndexExpression(left)\n```\n\n**This creates chained `IndexExpr` nodes** for multi-dimensional access:\n- `arr[i]` → `IndexExpr{Left: arr, Index: i}`\n- `arr[i][j]` → `IndexExpr{Left: IndexExpr{Left: arr, Index: i}, Index: j}`\n- `arr[i][j][k]` → `IndexExpr{Left: IndexExpr{Left: IndexExpr{Left: arr, Index: i}, Index: j}, Index: k}`\n\n#### **Index Expression Parsing** (lines 786-800)\n\n```go\nfunc (p *Parser) parseIndexExpression(left ast.Expression) *ast.IndexExpr {\n expr := &ast.IndexExpr{\n Token: p.curToken,\n Left: left,\n }\n \n p.nextToken() // move past [\n expr.Index = p.parseExpression(LOWEST) // Single index only!\n \n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n \n return expr\n}\n```\n\n**Single index per call** - multi-dimensional indexing is built via nesting.\n\n---\n\n### 3. CODEGEN (codegen/codegen.go)\n\n#### **Array Declaration Code Generation** (lines 1146-1155)\n\n```go\nfunc (g *Generator) generateArrayDecl(decl *ast.ArrayDecl) {\n goType := g.occamTypeToGo(decl.Type)\n for _, name := range decl.Names {\n n := goIdent(name)\n g.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n g.write(fmt.Sprintf(\"%s := make([]%s, \", n, goType))\n g.generateExpression(decl.Size)\n g.write(\")\\n\")\n }\n}\n```\n\n**Output for `[5]INT arr:`**:\n```go\narr := make([]int, 5)\n```\n\n**NOT generating Go slices for multi-dimensional arrays - they're treated as channels/arrays of channels**\n\n#### **Channel Array Code Generation** (lines 551-554 in parser context)\n\nFor `[n]CHAN OF TYPE`:\n- Creates `ChanDecl` with `IsArray=true`\n- Codegen in `generateStatement()` case line 1020-1021 calls `generateChanDecl()`\n\n---\n\n### 4. REAL-WORLD EXAMPLES\n\n#### **From life.occ** (Historical example)\n\n**Lines 43, 49, 90**: Proc parameters with multi-dimensional channel arrays:\n```occam\nPROC broadcast.present.state([][][]CHAN OF STATE link, ...)\nPROC calculate.next.state([][][]CHAN OF STATE link, ...)\nPROC cell([][][]CHAN OF STATE link, ...)\n```\n\n**Lines 400-402**: Multi-dimensional channel array declarations:\n```occam\n[array.width][array.height][neighbours]CHAN OF STATE link :\n[array.width][array.height]CHAN OF COMMAND control :\n[array.width][array.height]CHAN OF RESPONSE sense :\n```\n\n**Lines 46, 57**: Multi-dimensional indexing:\n```occam\nlink[x][y][d] ! state\nlink[nx[d]][ny[d]][d] ? state.of.neighbour[d]\n```\n\n**How this is parsed:**\n1. `[array.width]` → Parsed as size expression\n2. `[array.height]` → Parsed as ANOTHER size (not as a second dimension in the AST, but used differently during codegen)\n3. `[neighbours]CHAN OF STATE` → Completes the full declaration\n\n#### **From examples/array.occ** (Lines 3-10)\n\nSimple single-dimensional array:\n```occam\n[5]INT arr:\n...\nSEQ i = 0 FOR 5\n arr[i] := (i + 1) * 10\n```\n\n**Parsed as:**\n- `ArrayDecl{Token: [, Size: IntegerLiteral(5), Type: \"INT\", Names: [\"arr\"]}`\n- Assignment with `Name: \"arr\"` and `Index: IntegerLiteral(2)` for `arr[i] := ...`\n\n---\n\n### 5. TEST CASES (parser/parser_test.go)\n\n#### **TestArrayDecl** (lines 585-617)\n```go\ninput := `[5]INT arr:`\n// Expects: ArrayDecl with Size = IntegerLiteral(5), Type = \"INT\", Names = [\"arr\"]\n```\n\n#### **TestArrayDeclMultipleNames** (lines 619-649)\n```go\ninput := `[10]INT a, b:`\n// Expects: ArrayDecl with Size = IntegerLiteral(10), Type = \"INT\", Names = [\"a\", \"b\"]\n```\n\n#### **TestArrayLiteral** (lines 3121-3160)\n```go\ninput := `VAL x IS [1, 2, 3] :`\n// Expects: Abbreviation with Value = ArrayLiteral{Elements: [IntegerLiteral(1), IntegerLiteral(2), IntegerLiteral(3)]}\n```\n\n---\n\n### 6. KEY FINDINGS - SINGLE vs MULTI-DIMENSIONAL\n\n**Current Implementation:**\n\n1. **Single-Dimensional Arrays**: Fully supported\n - `ArrayDecl.Size` is a single `Expression`\n - Codegen produces: `var_name := make([]type, size)`\n\n2. **Multi-Dimensional Arrays**: NOT supported as a single AST construct\n - Currently handled as **channel arrays** (special case for CHAN)\n - Declaration syntax `[n][m]CHAN OF TYPE` is parsed sequentially\n - Indexing is done via **chained `IndexExpr` nodes** (recursive structure)\n - Codegen must recursively handle nested indexing\n\n3. **Array Parameters**:\n - `IsOpenArray`: Open array `[]TYPE` (variable-length)\n - `IsChanArray`: Open channel array `[]CHAN OF TYPE`\n - `ArraySize`: Fixed-size `[n]TYPE` (stored as string, not expression!)\n\n4. **No Dimension List**:\n - `ArrayDecl` has `Size` (singular), not `Sizes` (plural)\n - `ChanDecl` has `Size` (singular), not `Sizes` (plural)\n - Multi-dimensions are represented through nesting/chaining\n\n---\n\n### File References for Your Implementation\n\n1. **Array AST definitions**: `/home/david/projects/code-associates/occam2go/ast/ast.go` lines 46-55, 348-358, 143-154\n2. **Array parsing**: `/home/david/projects/code-associates/occam2go/parser/parser.go` lines 530-628, 2753-2808, 786-800\n3. **Index expression parsing**: `/home/david/projects/code-associates/occam2go/parser/parser.go` lines 2865-2867 (infix), 786-800 (parseIndexExpression)\n4. **Codegen**: `/home/david/projects/code-associates/occam2go/codegen/codegen.go` lines 1146-1155\n5. **Test examples**: `/home/david/projects/code-associates/occam2go/parser/parser_test.go` lines 585-649, 3121-3160\n6. **Real-world example**: `/home/david/projects/code-associates/occam2go/historical-examples/life.occ` lines 43, 49, 90, 400-402, 46, 57"
},
{
"type": "text",
"text": "agentId: ae44ffec7464099ab (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 66092\ntool_uses: 34\nduration_ms: 72019</usage>"
}
][
{
"type": "text",
"text": "Perfect! Now I have all the information I need. Let me create a comprehensive report:\n\n## Comprehensive Array Handling Analysis - occam2go Transpiler\n\nI've completed a thorough exploration of the occam2go codebase's array handling. Here's a detailed breakdown organized by the three key areas you requested:\n\n---\n\n## 1. CODEGEN ARRAY GENERATION (`codegen/codegen.go`)\n\n### 1.1 ArrayDecl Generation (Lines 1146-1155)\n```go\nfunc (g *Generator) generateArrayDecl(decl *ast.ArrayDecl) {\n\tgoType := g.occamTypeToGo(decl.Type)\n\tfor _, name := range decl.Names {\n\t\tn := goIdent(name)\n\t\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\t\tg.write(fmt.Sprintf(\"%s := make([]%s, \", n, goType))\n\t\tg.generateExpression(decl.Size)\n\t\tg.write(\")\\n\")\n\t}\n}\n```\n- **Location**: Lines 1146-1155\n- **Behavior**: Generates Go slice allocation `name := make([]type, size)` for occam arrays\n- **Key points**: \n - Always creates slices (not fixed arrays)\n - Supports multiple names in one declaration\n - Size can be any expression\n\n### 1.2 ChanArrayDecl Generation (Lines 1117-1134)\n```go\nfunc (g *Generator) generateChanDecl(decl *ast.ChanDecl) {\n\tgoType := g.occamTypeToGo(decl.ElemType)\n\tif decl.IsArray {\n\t\tfor _, name := range decl.Names {\n\t\t\tn := goIdent(name)\n\t\t\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\t\t\tg.write(fmt.Sprintf(\"%s := make([]chan %s, \", n, goType))\n\t\t\tg.generateExpression(decl.Size)\n\t\t\tg.write(\")\\n\")\n\t\t\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\t\t\tg.write(fmt.Sprintf(\"for _i := range %s { %s[_i] = make(chan %s) }\\n\", n, n, goType))\n\t\t}\n\t} else {\n\t\tfor _, name := range decl.Names {\n\t\t\tg.writeLine(fmt.Sprintf(\"%s := make(chan %s)\", goIdent(name), goType))\n\t\t}\n\t}\n}\n```\n- **Location**: Lines 1117-1134\n- **Behavior**: Two-phase initialization for channel arrays:\n 1. Allocates slice of channels: `cs := make([]chan int, 5)`\n 2. Initializes each channel: `for _i := range cs { cs[_i] = make(chan int) }`\n- **Key points**:\n - `decl.IsArray` distinguishes between `[n]CHAN OF T` and `CHAN OF T`\n - Uses `decl.Size` for array size\n - Uses `decl.ElemType` for element type\n - Singular channels don't need the init loop\n\n### 1.3 Array Indexing Expression Generation (Lines 2521-2536)\n```go\ncase *ast.IndexExpr:\n\t// Check if this is a record field access\n\tif ident, ok := e.Left.(*ast.Identifier); ok {\n\t\tif _, ok := g.recordVars[ident.Value]; ok {\n\t\t\tif field, ok := e.Index.(*ast.Identifier); ok {\n\t\t\t\tg.generateExpression(e.Left)\n\t\t\t\tg.write(\".\")\n\t\t\t\tg.write(goIdent(field.Value))\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tg.generateExpression(e.Left)\n\tg.write(\"[\")\n\tg.generateExpression(e.Index)\n\tg.write(\"]\")\n```\n- **Location**: Lines 2521-2536\n- **Behavior**: Handles both array and record field access\n - **Record fields**: `p[x]` → `p.x` (when `x` is in a record)\n - **Array indexing**: `arr[i]` → `arr[i]` (straight pass-through)\n- **Key points**:\n - Uses `recordVars` map to distinguish records from arrays\n - Checks if Left is an Identifier and if it's in recordVars\n - Field names are converted with `goIdent()` (handles reserved words)\n\n### 1.4 Array Indexing in Send/Receive (Lines 1157-1216)\n\n**Send with Index** (Lines 1157-1202):\n```go\nfunc (g *Generator) generateSend(send *ast.Send) {\n\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\tg.write(goIdent(send.Channel))\n\tif send.ChannelIndex != nil {\n\t\tg.write(\"[\")\n\t\tg.generateExpression(send.ChannelIndex)\n\t\tg.write(\"]\")\n\t}\n\tg.write(\" <- \")\n\t// ... protocol handling ...\n\tg.generateExpression(send.Value)\n\tg.write(\"\\n\")\n}\n```\n- **Location**: Lines 1157-1202\n- **Key points**:\n - `send.ChannelIndex` being non-nil signals `cs[i]` syntax\n - Outputs `cs[i] <- value`\n - Works with simple, sequential, and variant protocols\n\n**Receive with Index** (Lines 1204-1216):\n```go\nfunc (g *Generator) generateReceive(recv *ast.Receive) {\n\tchanRef := goIdent(recv.Channel)\n\tif recv.ChannelIndex != nil {\n\t\tvar buf strings.Builder\n\t\tbuf.WriteString(goIdent(recv.Channel))\n\t\tbuf.WriteString(\"[\")\n\t\t// Generate the index expression into a temporary buffer\n\t\toldBuilder := g.builder\n\t\tg.builder = strings.Builder{}\n\t\tg.generateExpression(recv.ChannelIndex)\n\t\tbuf.WriteString(g.builder.String())\n\t\tg.builder = oldBuilder\n\t\tbuf.WriteString(\"]\")\n\t\t// ... rest of generation ...\n```\n- **Location**: Lines 1204-1216+\n- **Key points**:\n - Handles `recv.ChannelIndex` to generate `cs[i] ? x` syntax\n - Uses temporary buffer for index expression generation\n\n### 1.5 Array Assignment Generation (Lines 1582-1634)\n```go\nfunc (g *Generator) generateAssignment(assign *ast.Assignment) {\n\t// ... preamble ...\n\t\n\tif assign.SliceTarget != nil {\n\t\t// Slice assignment: [arr FROM start FOR length] := value\n\t\t// Maps to: copy(arr[start : start + length], value)\n\t\tg.write(\"copy(\")\n\t\tg.generateExpression(assign.SliceTarget.Array)\n\t\tg.write(\"[\")\n\t\tg.generateExpression(assign.SliceTarget.Start)\n\t\tg.write(\" : \")\n\t\tg.generateExpression(assign.SliceTarget.Start)\n\t\tg.write(\" + \")\n\t\tg.generateExpression(assign.SliceTarget.Length)\n\t\tg.write(\"], \")\n\t\tg.generateExpression(assign.Value)\n\t\tg.write(\")\\n\")\n\t\treturn\n\t}\n\n\tif assign.Index != nil {\n\t\t// Check if this is a record field access\n\t\tif _, ok := g.recordVars[assign.Name]; ok {\n\t\t\tif ident, ok := assign.Index.(*ast.Identifier); ok {\n\t\t\t\t// Record field: p.x = value\n\t\t\t\tg.write(goIdent(assign.Name))\n\t\t\t\tg.write(\".\")\n\t\t\t\tg.write(goIdent(ident.Value))\n\t\t\t\tg.write(\" = \")\n\t\t\t\tg.generateExpression(assign.Value)\n\t\t\t\tg.write(\"\\n\")\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\t// Array index: dereference if ref param\n\t\tif g.refParams[assign.Name] {\n\t\t\tg.write(\"*\")\n\t\t}\n\t\tg.write(goIdent(assign.Name))\n\t\tg.write(\"[\")\n\t\tg.generateExpression(assign.Index)\n\t\tg.write(\"]\")\n\t} else {\n\t\t// Simple assignment: dereference if ref param\n\t\tif g.refParams[assign.Name] {\n\t\t\tg.write(\"*\")\n\t\t}\n\t\tg.write(goIdent(assign.Name))\n\t}\n\tg.write(\" = \")\n\tg.generateExpression(assign.Value)\n\tg.write(\"\\n\")\n}\n```\n- **Location**: Lines 1582-1634\n- **Three pathways**:\n 1. **Slice assignment** (lines 1585-1599): `[arr FROM n FOR m] := src` → `copy(arr[n:n+m], src)`\n 2. **Record field assignment** (lines 1603-1614): `p[x] := v` → `p.x = v`\n 3. **Array indexed assignment** (lines 1616-1633): `arr[i] := v` → `arr[i] = v` (with ref param dereference)\n- **Key tracking**:\n - `g.recordVars` distinguishes record fields from array indices\n - `g.refParams` tracks which variables are reference parameters (need dereference)\n\n### 1.6 Multi-Assignment with Array Indices (Lines 2210-2251)\n```go\nfunc (g *Generator) generateMultiAssignment(stmt *ast.MultiAssignment) {\n\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\tfor i, target := range stmt.Targets {\n\t\tif i > 0 {\n\t\t\tg.write(\", \")\n\t\t}\n\t\tif target.Index != nil {\n\t\t\t// Check if this is a record field access\n\t\t\tif _, ok := g.recordVars[target.Name]; ok {\n\t\t\t\tif ident, ok := target.Index.(*ast.Identifier); ok {\n\t\t\t\t\tg.write(goIdent(target.Name))\n\t\t\t\t\tg.write(\".\")\n\t\t\t\t\tg.write(goIdent(ident.Value))\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t\tif g.refParams[target.Name] {\n\t\t\t\tg.write(\"(*\")\n\t\t\t\tg.write(goIdent(target.Name))\n\t\t\t\tg.write(\")\")\n\t\t\t} else {\n\t\t\t\tg.write(goIdent(target.Name))\n\t\t\t}\n\t\t\tg.write(\"[\")\n\t\t\tg.generateExpression(target.Index)\n\t\t\tg.write(\"]\")\n\t\t} else {\n\t\t\tif g.refParams[target.Name] {\n\t\t\t\tg.write(\"*\")\n\t\t\t}\n\t\t\tg.write(goIdent(target.Name))\n\t\t}\n\t}\n\tg.write(\" = \")\n\tfor i, val := range stmt.Values {\n\t\tif i > 0 {\n\t\t\tg.write(\", \")\n\t\t}\n\t\tg.generateExpression(val)\n\t}\n\tg.write(\"\\n\")\n}\n```\n- **Location**: Lines 2210-2251\n- **Supports**:\n - `a, b := 1, 2` (simple vars)\n - `x[0], x[1] := x[1], x[0]` (indexed targets)\n - `a, x[0] := 1, 2` (mixed)\n - Record field targets: `r.x, r.y := ...`\n- **Key points**:\n - `MultiAssignTarget` has optional `Index` field for indexed targets\n - Handles both record and array indices via `recordVars` check\n - Reference params require special handling: `(*arr)[i] = val`\n\n### 1.7 Procedure Parameter Generation (Lines 2036-2064)\n```go\nfunc (g *Generator) generateProcParams(params []ast.ProcParam) string {\n\tvar parts []string\n\tfor _, p := range params {\n\t\tvar goType string\n\t\tif p.IsChanArray {\n\t\t\tgoType = \"[]chan \" + g.occamTypeToGo(p.ChanElemType)\n\t\t} else if p.IsChan {\n\t\t\tgoType = chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)\n\t\t} else if p.IsOpenArray {\n\t\t\tgoType = \"[]\" + g.occamTypeToGo(p.Type)\n\t\t} else if p.ArraySize != \"\" {\n\t\t\t// Fixed-size array parameter: use slice for Go compatibility\n\t\t\t// (occam [n]TYPE and []TYPE both map to Go slices)\n\t\t\tgoType = \"[]\" + g.occamTypeToGo(p.Type)\n\t\t} else {\n\t\t\tgoType = g.occamTypeToGo(p.Type)\n\t\t\tif !p.IsVal {\n\t\t\t\t// Non-VAL parameters are pass by reference in Occam\n\t\t\t\tgoType = \"*\" + goType\n\t\t\t}\n\t\t}\n\t\tpName := goIdent(p.Name)\n\t\tif renamed, ok := g.retypesRenames[p.Name]; ok {\n\t\t\tpName = renamed\n\t\t}\n\t\tparts = append(parts, fmt.Sprintf(\"%s %s\", pName, goType))\n\t}\n\treturn strings.Join(parts, \", \")\n}\n```\n- **Location**: Lines 2036-2064\n- **Mapping array parameter types**:\n - `[]CHAN OF INT` → `[]chan int` (IsChanArray=true)\n - `CHAN OF INT c?` → `<-chan int` (IsChan=true, ChanDir=\"?\")\n - `[]INT` (open array) → `[]int` (IsOpenArray=true)\n - `[2]INT` (fixed-size) → `[]int` (ArraySize=\"2\", maps to slice)\n - Plain `INT` → `int` (VAL) or `*int` (non-VAL)\n\n### 1.8 Slice Expression Generation (Lines 2537-2545)\n```go\ncase *ast.SliceExpr:\n\tg.generateExpression(e.Array)\n\tg.write(\"[\")\n\tg.generateExpression(e.Start)\n\tg.write(\" : \")\n\tg.generateExpression(e.Start)\n\tg.write(\" + \")\n\tg.generateExpression(e.Length)\n\tg.write(\"]\")\n```\n- **Location**: Lines 2537-2545\n- **Behavior**: `[arr FROM n FOR m]` → `arr[n : n+m]`\n- **Key points**:\n - Uses `SliceExpr` AST node with `Array`, `Start`, `Length`\n - Generates Go slice syntax\n\n### 1.9 Array Literal Generation (Lines 2647-2657)\n```go\nfunc (g *Generator) generateArrayLiteral(al *ast.ArrayLiteral) {\n\tg.write(\"[]int{\")\n\tfor i, elem := range al.Elements {\n\t\tif i > 0 {\n\t\t\tg.write(\", \")\n\t\t}\n\t\tg.generateExpression(elem)\n\t}\n\tg.write(\"}\")\n}\n```\n- **Location**: Lines 2647-2657\n- **Behavior**: `[1, 2, 3]` → `[]int{1, 2, 3}`\n- **Key points**:\n - Hardcoded to `[]int{}` (assumes integer literals in array literals)\n - Used in abbreviations: `VAL x IS [1, 2, 3] :`\n\n---\n\n## 2. CODEGEN TESTS (`codegen/codegen_test.go` and `codegen/e2e_array_test.go`)\n\n### 2.1 Unit Tests (codegen_test.go)\n\n**TestArrayDecl** (Lines 184-192):\n```go\nfunc TestArrayDecl(t *testing.T) {\n\tinput := `[5]INT arr:\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"arr := make([]int, 5)\") {\n\t\tt.Errorf(\"expected 'arr := make([]int, 5)' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: Basic array declaration generates `make([]int, 5)`\n\n**TestIndexedAssignment** (Lines 194-202):\n```go\nfunc TestIndexedAssignment(t *testing.T) {\n\tinput := `arr[2] := 10\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"arr[2] = 10\") {\n\t\tt.Errorf(\"expected 'arr[2] = 10' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: Array element assignment\n\n**TestChanArrayDeclGen** (Lines 582-593):\n```go\nfunc TestChanArrayDeclGen(t *testing.T) {\n\tinput := `[5]CHAN OF INT cs:\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"cs := make([]chan int, 5)\") {\n\t\tt.Errorf(\"expected 'cs := make([]chan int, 5)' in output, got:\\n%s\", output)\n\t}\n\tif !strings.Contains(output, \"for _i := range cs { cs[_i] = make(chan int) }\") {\n\t\tt.Errorf(\"expected init loop in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: \n - Channel array allocation\n - Individual channel initialization loop\n\n**TestIndexedSendGen** (Lines 595-603):\n```go\nfunc TestIndexedSendGen(t *testing.T) {\n\tinput := `cs[0] ! 42\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"cs[0] <- 42\") {\n\t\tt.Errorf(\"expected 'cs[0] <- 42' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: `cs[0] ! 42` → `cs[0] <- 42`\n\n**TestIndexedReceiveGen** (Lines 605-613):\n```go\nfunc TestIndexedReceiveGen(t *testing.T) {\n\tinput := `cs[0] ? x\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"x = <-cs[0]\") {\n\t\tt.Errorf(\"expected 'x = <-cs[0]' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: `cs[0] ? x` → `x = <-cs[0]`\n\n**TestChanArrayParamGen** (Lines 615-624):\n```go\nfunc TestChanArrayParamGen(t *testing.T) {\n\tinput := `PROC worker([]CHAN OF INT cs)\n SKIP\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"func worker(cs []chan int)\") {\n\t\tt.Errorf(\"expected 'func worker(cs []chan int)' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: `[]CHAN OF INT cs` parameter → `cs []chan int`\n\n**TestOpenArrayParamGen** (Lines 687-696):\n```go\nfunc TestOpenArrayParamGen(t *testing.T) {\n\tinput := `PROC worker(VAL []INT arr, []BYTE data)\n SKIP\n`\n\toutput := transpile(t, input)\n\n\tif !strings.Contains(output, \"func worker(arr []int, data []byte)\") {\n\t\tt.Errorf(\"expected 'func worker(arr []int, data []byte)' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: Open array params → slices\n\n**TestSizeOperator** (Lines 670-685):\n```go\nfunc TestSizeOperator(t *testing.T) {\n\ttests := []struct {\n\t\tinput string\n\t\texpected string\n\t}{\n\t\t{\"x := SIZE arr\\n\", \"x = len(arr)\"},\n\t\t{\"x := SIZE arr + 1\\n\", \"x = (len(arr) + 1)\"},\n\t}\n\t// ... loop ...\n}\n```\n- **Checks**: `SIZE arr` → `len(arr)`\n\n**TestArrayLiteralCodegen** (Lines 748-755):\n```go\nfunc TestArrayLiteralCodegen(t *testing.T) {\n\tinput := `VAL x IS [10, 20, 30] :\n`\n\toutput := transpile(t, input)\n\tif !strings.Contains(output, \"[]int{10, 20, 30}\") {\n\t\tt.Errorf(\"expected '[]int{10, 20, 30}' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: Array literal → `[]int{...}`\n\n**TestMultiAssignmentIndexed** (Lines 730-737):\n```go\nfunc TestMultiAssignmentIndexed(t *testing.T) {\n\tinput := `x[0], x[1] := x[1], x[0]\n`\n\toutput := transpile(t, output)\n\tif !strings.Contains(output, \"x[0], x[1] = x[1], x[0]\") {\n\t\tt.Errorf(\"expected 'x[0], x[1] = x[1], x[0]' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: Indexed multi-assignment swap\n\n**TestMultiAssignmentMixed** (Lines 739-746):\n```go\nfunc TestMultiAssignmentMixed(t *testing.T) {\n\tinput := `a, x[0] := 1, 2\n`\n\toutput := transpile(t, input)\n\tif !strings.Contains(output, \"a, x[0] = 1, 2\") {\n\t\tt.Errorf(\"expected 'a, x[0] = 1, 2' in output, got:\\n%s\", output)\n\t}\n}\n```\n- **Checks**: Mixed simple and indexed targets\n\n### 2.2 End-to-End Tests (codegen/e2e_array_test.go)\n\n**TestE2E_ArrayBasic** (Lines 5-17):\n- Declares array, stores value, loads and prints it\n- **Input**: 5-element array, set `arr[0] := 42`, print\n- **Expected**: `42\\n`\n\n**TestE2E_ArrayWithLoop** (Lines 19-33):\n- Uses replicated SEQ to fill array, then print all elements\n- **Input**: `arr[i] := i * 10` in loop, then print all\n- **Expected**: `0\\n10\\n20\\n30\\n40\\n`\n\n**TestE2E_ArraySum** (Lines 35-54):\n- Initialize array elements, compute sum with loop\n- **Expected**: `100\\n`\n\n**TestE2E_ArrayExpressionIndex** (Lines 56-73):\n- Array index with variable and expression: `arr[idx]`, `arr[idx + 1]`\n- **Expected**: `200\\n300\\n`\n\n**TestE2E_ChanArrayBasic** (Lines 75-95):\n- Declare `[3]CHAN OF INT cs`\n- Replicated PAR: senders do `cs[i] ! value`, receiver does `cs[i] ? x`\n- **Input**: Sum 3 channel values\n- **Expected**: `60\\n`\n\n**TestE2E_ChanArrayWithProc** (Lines 97-120):\n- Pass channel array to PROC with param `[]CHAN OF INT cs`\n- PROC uses `cs[i] ! value` in indexed loop\n- **Expected**: `600\\n`\n\n**TestE2E_ChanArrayAlt** (Lines 122-141):\n- Use `cs[0]` and `cs[1]` in ALT cases\n- **Expected**: `42\\n`\n\n**TestE2E_SizeArray** (Lines 143-155):\n- `n := SIZE arr` where arr is 5 elements\n- **Expected**: `5\\n`\n\n**TestE2E_SizeString** (Lines 157-168):\n- `n := SIZE \"hello\"`\n- **Expected**: `5\\n`\n\n**TestE2E_SliceAsArg** (Lines 170-186):\n- Pass slice `[nums FROM 1 FOR 3]` to PROC expecting `VAL []INT arr`\n- **Expected**: `20\\n30\\n40\\n`\n\n**TestE2E_SliceAssignment** (Lines 188-203):\n- `[arr FROM 3 FOR 3] := [arr FROM 0 FOR 3]` (copy first 3 to last 3)\n- **Expected**: `1\\n2\\n3\\n1\\n2\\n3\\n`\n\n**TestE2E_SliceSize** (Lines 205-218):\n- `n := SIZE [arr FROM 2 FOR 5]`\n- **Expected**: `5\\n`\n\n**TestE2E_SliceFromZero** (Lines 220-240):\n- Slice starting at 0: `[arr FROM 0 FOR 3]`\n- **Expected**: `6\\n` (sum of 1+2+3)\n\n**TestE2E_OpenArrayParam** (Lines 242-259):\n- Pass array to `PROC printarray(VAL []INT arr)`\n- **Expected**: `10\\n20\\n30\\n`\n\n**TestE2E_MultiAssignmentSwap** (Lines 261-276):\n- `arr[0], arr[1] := arr[1], arr[0]`\n- **Expected**: `20\\n10\\n`\n\n**TestE2E_MultiAssignmentMixed** (Lines 278-293):\n- `a, arr[1] := arr[0], 42`\n- **Expected**: `99\\n42\\n`\n\n**TestE2E_MultiAssignmentValues** (Lines 295-308):\n- `a, b, c := 10, 20, 30`\n- **Expected**: `10\\n20\\n30\\n`\n\n**TestE2E_ChanArrayDirParam** (Lines 310-335):\n- Channel array with direction annotations: `[]CHAN OF INT out!`\n- **Expected**: `0\\n1\\n2\\n`\n\n---\n\n## 3. PARSER TESTS (`parser/parser_test.go`)\n\n### 3.1 Array Declaration Tests\n\n**TestArrayDecl** (Lines 585-617):\n```go\nfunc TestArrayDecl(t *testing.T) {\n\tinput := `[5]INT arr:\n`\n\t// ... parse ...\n\tdecl, ok := program.Statements[0].(*ast.ArrayDecl)\n\tif !ok {\n\t\tt.Fatalf(\"expected ArrayDecl, got %T\", program.Statements[0])\n\t}\n\tif decl.Type != \"INT\" {\n\t\tt.Errorf(\"expected type INT, got %s\", decl.Type)\n\t}\n\tsizeLit, ok := decl.Size.(*ast.IntegerLiteral)\n\tif sizeLit.Value != 5 {\n\t\tt.Errorf(\"expected size 5, got %d\", sizeLit.Value)\n\t}\n\tif len(decl.Names) != 1 || decl.Names[0] != \"arr\" {\n\t\tt.Errorf(\"expected name 'arr', got %v\", decl.Names)\n\t}\n}\n```\n- **Checks**: \n - Parses `[5]INT arr:` into `ArrayDecl`\n - Verifies `Type`, `Size`, `Names`\n\n**TestArrayDeclMultipleNames** (Lines 619-649):\n```go\nfunc TestArrayDeclMultipleNames(t *testing.T) {\n\tinput := `[10]INT a, b:\n`\n\t// ... parse ...\n\texpected := []string{\"a\", \"b\"}\n\tif len(decl.Names) != len(expected) {\n\t\tt.Fatalf(\"expected %d names, got %d\", len(expected), len(decl.Names))\n\t}\n}\n```\n- **Checks**: Multiple array names in one declaration\n\n### 3.2 Array Indexing Tests\n\n**TestIndexedAssignment** (Lines 651-691):\n```go\nfunc TestIndexedAssignment(t *testing.T) {\n\tinput := `arr[2] := 10\n`\n\t// ... parse ...\n\tassign, ok := program.Statements[0].(*ast.Assignment)\n\tif assign.Name != \"arr\" {\n\t\tt.Errorf(\"expected name 'arr', got %s\", assign.Name)\n\t}\n\tif assign.Index == nil {\n\t\tt.Fatal(\"expected index expression, got nil\")\n\t}\n\tindexLit, ok := assign.Index.(*ast.IntegerLiteral)\n\tif indexLit.Value != 2 {\n\t\tt.Errorf(\"expected index 2, got %d\", indexLit.Value)\n\t}\n}\n```\n- **Checks**: \n - `Assignment.Index` is non-nil for indexed assignment\n - Index is an expression\n\n**TestIndexExpression** (Lines 693-740):\n```go\nfunc TestIndexExpression(t *testing.T) {\n\tinput := `x := arr[0] + 1\n`\n\t// ... parse ...\n\tbinExpr, ok := assign.Value.(*ast.BinaryExpr)\n\tindexExpr, ok := binExpr.Left.(*ast.IndexExpr)\n\tident, ok := indexExpr.Left.(*ast.Identifier)\n\tif ident.Value != \"arr\" {\n\t\tt.Errorf(\"expected 'arr', got %s\", ident.Value)\n\t}\n\tidxLit, ok := indexExpr.Index.(*ast.IntegerLiteral)\n\tif idxLit.Value != 0 {\n\t\tt.Errorf(\"expected index 0, got %d\", idxLit.Value)\n\t}\n}\n```\n- **Checks**: \n - `IndexExpr` in expressions\n - `IndexExpr.Left` is the array (Identifier)\n - `IndexExpr.Index` is the index expression\n\n### 3.3 Channel Array Tests\n\n**TestChanArrayDecl** (Lines 1991-2027):\n```go\nfunc TestChanArrayDecl(t *testing.T) {\n\tinput := `[5]CHAN OF INT cs:\n`\n\t// ... parse ...\n\tdecl, ok := program.Statements[0].(*ast.ChanDecl)\n\tif !decl.IsArray {\n\t\tt.Error(\"expected IsArray=true\")\n\t}\n\tsizeLit, ok := decl.Size.(*ast.IntegerLiteral)\n\tif sizeLit.Value != 5 {\n\t\tt.Errorf(\"expected size 5, got %d\", sizeLit.Value)\n\t}\n\tif decl.ElemType != \"INT\" {\n\t\tt.Errorf(\"expected element type INT, got %s\", decl.ElemType)\n\t}\n}\n```\n- **Checks**: \n - `ChanDecl.IsArray = true` for `[n]CHAN OF TYPE`\n - `ChanDecl.Size` and `ChanDecl.ElemType`\n\n**TestIndexedSend** (Lines 2029-2069):\n```go\nfunc TestIndexedSend(t *testing.T) {\n\tinput := `cs[0] ! 42\n`\n\t// ... parse ...\n\tsend, ok := program.Statements[0].(*ast.Send)\n\tif send.Channel != \"cs\" {\n\t\tt.Errorf(\"expected channel 'cs', got %s\", send.Channel)\n\t}\n\tif send.ChannelIndex == nil {\n\t\tt.Fatal(\"expected ChannelIndex, got nil\")\n\t}\n\tidxLit, ok := send.ChannelIndex.(*ast.IntegerLiteral)\n\tif idxLit.Value != 0 {\n\t\tt.Errorf(\"expected index 0, got %d\", idxLit.Value)\n\t}\n}\n```\n- **Checks**: \n - `Send.ChannelIndex` is non-nil for `cs[i]` syntax\n - `Send.Channel` is the base channel name\n\n**TestChanArrayParam** (Lines 2109-2152):\n```go\nfunc TestChanArrayParam(t *testing.T) {\n\tinput := `PROC worker([]CHAN OF INT cs, VAL INT n)\n SKIP\n`\n\t// ... parse ...\n\tp0 := proc.Params[0]\n\tif !p0.IsChan {\n\t\tt.Error(\"param 0: expected IsChan=true\")\n\t}\n\tif !p0.IsChanArray {\n\t\tt.Error(\"param 0: expected IsChanArray=true\")\n\t}\n\tif p0.ChanElemType != \"INT\" {\n\t\tt.Errorf(\"param 0: expected ChanElemType=INT, got %s\", p0.ChanElemType)\n\t}\n}\n```\n- **Checks**: \n - `ProcParam.IsChanArray = true`\n - `ProcParam.IsChan = true`\n - `ProcParam.ChanElemType` is set\n\n**TestChanDirParam** (Lines 2154-2188):\n```go\nfunc TestChanDirParam(t *testing.T) {\n\tinput := `PROC worker(CHAN OF INT input?, CHAN OF INT output!)\n ...\n`\n\t// ... parse ...\n\tp0 := proc.Params[0]\n\tif !p0.IsChan {\n\t\tt.Error(\"param 0: expected IsChan=true\")\n\t}\n\tif p0.ChanDir != \"?\" {\n\t\tt.Errorf(\"param 0: expected ChanDir=?, got %q\", p0.ChanDir)\n\t}\n}\n```\n- **Checks**: \n - `ProcParam.ChanDir` for `?` (input) and `!` (output)\n\n**TestChanArrayDeclShorthand** (Lines 2658-?):\n- Tests shorthand channel declaration syntax\n\n### 3.4 Open Array Parameter Tests\n\n**TestOpenArrayParam** (Lines 2584-2630):\n```go\nfunc TestOpenArrayParam(t *testing.T) {\n\tinput := `PROC sum.array(VAL []INT arr, INT result)\n SKIP\n`\n\t// ... parse ...\n\tp0 := proc.Params[0]\n\tif !p0.IsVal {\n\t\tt.Error(\"param 0: expected IsVal=true\")\n\t}\n\tif !p0.IsOpenArray {\n\t\tt.Error(\"param 0: expected IsOpenArray=true\")\n\t}\n\tif p0.Type != \"INT\" {\n\t\tt.Errorf(\"param 0: expected Type=INT, got %s\", p0.Type)\n\t}\n}\n```\n- **Checks**: \n - `ProcParam.IsOpenArray = true` for `[]TYPE`\n - `ProcParam.Type` contains element type\n\n### 3.5 Array Literal Tests\n\n**TestArrayLiteral** (Lines 3121-3160):\n```go\nfunc TestArrayLiteral(t *testing.T) {\n\tinput := `VAL x IS [1, 2, 3] :\n`\n\t// ... parse ...\n\tarr, ok := abbr.Value.(*ast.ArrayLiteral)\n\tif len(arr.Elements) != 3 {\n\t\tt.Fatalf(\"expected 3 elements, got %d\", len(arr.Elements))\n\t}\n\tfor i, expected := range []int64{1, 2, 3} {\n\t\tlit, ok := arr.Elements[i].(*ast.IntegerLiteral)\n\t\tif lit.Value != expected {\n\t\t\tt.Errorf(\"element %d: expected %d, got %d\", i, expected, lit.Value)\n\t\t}\n\t}\n}\n```\n- **Checks**: \n - `ArrayLiteral` AST node with `Elements` slice\n - Each element is an expression\n\n---\n\n## Summary Table: Array Feature Coverage\n\n| Feature | AST Type | Codegen Function | Codegen Lines | Unit Test | E2E Test |\n|---------|----------|------------------|---------------|-----------|----------|\n| Array declaration | `ArrayDecl` | `generateArrayDecl` | 1146-1155 | TestArrayDecl (184) | TestE2E_ArrayBasic (5) |\n| Multi-name arrays | `ArrayDecl.Names[]` | `generateArrayDecl` | 1146-1155 | TestArrayDeclMultipleNames (619) | N/A |\n| Array indexing | `IndexExpr` | `generateExpression` | 2521-2536 | TestIndexExpression (693) | TestE2E_ArrayWithLoop (19) |\n| Indexed assignment | `Assignment.Index` | `generateAssignment` | 1602-1633 | TestIndexedAssignment (194) | TestE2E_ArrayBasic (5) |\n| Channel array decl | `ChanDecl.IsArray` | `generateChanDecl` | 1117-1134 | TestChanArrayDeclGen (582) | TestE2E_ChanArrayBasic (75) |\n| Indexed send | `Send.ChannelIndex` | `generateSend` | 1157-1202 | TestIndexedSendGen (595) | TestE2E_ChanArrayBasic (75) |\n| Indexed receive | `Receive.ChannelIndex` | `generateReceive` | 1204-1216 | TestIndexedReceiveGen (605) | TestE2E_ChanArrayBasic (75) |\n| Open array params | `ProcParam.IsOpenArray` | `generateProcParams` | 2044-2045 | TestOpenArrayParam (687) | TestE2E_OpenArrayParam (242) |\n| Chan array params | `ProcParam.IsChanArray` | `generateProcParams` | 2040-2041 | TestChanArrayParamGen (615) | TestE2E_ChanArrayWithProc (97) |\n| Fixed-size params | `ProcParam.ArraySize` | `generateProcParams` | 2046-2049 | N/A | N/A |\n| Slice expressions | `SliceExpr` | `generateExpression` | 2537-2545 | N/A | TestE2E_SliceAsArg (170) |\n| Slice assignment | `Assignment.SliceTarget` | `generateAssignment` | 1585-1599 | N/A | TestE2E_SliceAssignment (188) |\n| Array literals | `ArrayLiteral` | `generateArrayLiteral` | 2647-2657 | TestArrayLiteralCodegen (748) | N/A |\n| SIZE operator | `SizeExpr` | `generateExpression` | 2513-2516 | TestSizeOperator (670) | TestE2E_SizeArray (143) |\n| Multi-assignment | `MultiAssignment` | `generateMultiAssignment` | 2210-2251 | TestMultiAssignmentIndexed (730) | TestE2E_MultiAssignmentSwap (261) |\n| Record field access | `IndexExpr` (record) | `generateExpression` | 2523-2529 | N/A | N/A |\n\n---\n\n## Key Architectural Patterns\n\n1. **IndexExpr dual purpose**: Used for both array indexing (`arr[i]`) and record field access (`p[x]` → `p.x`). Disambiguation via `recordVars` map.\n\n2. **Channel array two-phase init**: Slice allocation followed by individual channel creation in loop.\n\n3. **Fixed-size arrays → slices**: `[n]TYPE` proc params map to `[]TYPE` slices for Go compatibility (line 2046-2049).\n\n4. **Reference parameter dereference**: When `refParams[name]` is true, assignments need `*` or `(*arr)[i]` for dereferencing (lines 1617-1627, 2226-2239).\n\n5. **Slice mapping**: Occam `[arr FROM n FOR m]` → Go `arr[n : n+m]` syntax (lines 2537-2545, 1585-1599).\n\nAll absolute file paths:\n- `/home/david/projects/code-associates/occam2go/codegen/codegen.go`\n- `/home/david/projects/code-associates/occam2go/codegen/codegen_test.go`\n- `/home/david/projects/code-associates/occam2go/codegen/e2e_array_test.go`\n- `/home/david/projects/code-associates/occam2go/parser/parser_test.go`\n- `/home/david/projects/code-associates/occam2go/ast/ast.go`"
},
{
"type": "text",
"text": "agentId: af8e2690b2842ea02 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 75305\ntool_uses: 39\nduration_ms: 110038</usage>"
}
]Excellent! Now I have a comprehensive understanding of the codebase. Let me also look at the life.occ file and the specific areas that need changes. Let me also look at how Send and Receive nodes handle channel indexing, since multi-dimensional channel arrays will need multi-level indexing for send/receive.
Now let me look at the specific structures that need to change — the Send/Receive AST nodes and how assignments with multi-dimensional indexing work.
{
"file_path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"offset": 1,
"limit": 100
} 1→package ast
2→
3→import (
4→ "github.com/codeassociates/occam2go/lexer"
5→)
6→
7→// Node is the base interface for all AST nodes
8→type Node interface {
9→ TokenLiteral() string
10→}
11→
12→// Statement represents a statement node
13→type Statement interface {
14→ Node
15→ statementNode()
16→}
17→
18→// Expression represents an expression node
19→type Expression interface {
20→ Node
21→ expressionNode()
22→}
23→
24→// Program is the root node of every AST
25→type Program struct {
26→ Statements []Statement
27→}
28→
29→func (p *Program) TokenLiteral() string {
30→ if len(p.Statements) > 0 {
31→ return p.Statements[0].TokenLiteral()
32→ }
33→ return ""
34→}
35→
36→// VarDecl represents a variable declaration: INT x:
37→type VarDecl struct {
38→ Token lexer.Token // the type token (INT, BYTE, BOOL)
39→ Type string // "INT", "BYTE", "BOOL", etc.
40→ Names []string // variable names (can declare multiple: INT x, y, z:)
41→}
42→
43→func (v *VarDecl) statementNode() {}
44→func (v *VarDecl) TokenLiteral() string { return v.Token.Literal }
45→
46→// ArrayDecl represents an array declaration: [5]INT arr:
47→type ArrayDecl struct {
48→ Token lexer.Token // the [ token
49→ Size Expression // array size
50→ Type string // element type ("INT", "BYTE", "BOOL", etc.)
51→ Names []string // variable names
52→}
53→
54→func (a *ArrayDecl) statementNode() {}
55→func (a *ArrayDecl) TokenLiteral() string { return a.Token.Literal }
56→
57→// Assignment represents an assignment: x := 5 or arr[i] := 5 or [arr FROM n FOR m] := value
58→type Assignment struct {
59→ Token lexer.Token // the := token
60→ Name string // variable name
61→ Index Expression // optional: index expression for arr[i] := x (nil for simple assignments)
62→ SliceTarget *SliceExpr // optional: slice target for [arr FROM n FOR m] := value
63→ Value Expression // the value being assigned
64→}
65→
66→func (a *Assignment) statementNode() {}
67→func (a *Assignment) TokenLiteral() string { return a.Token.Literal }
68→
69→// MultiAssignTarget represents one target in a multi-assignment.
70→// Name is always set. Index is non-nil for indexed targets like arr[i].
71→type MultiAssignTarget struct {
72→ Name string // variable name
73→ Index Expression // optional: index expression for arr[i] (nil for simple ident)
74→}
75→
76→// MultiAssignment represents a multi-target assignment: a, b := func(x)
77→type MultiAssignment struct {
78→ Token lexer.Token // the := token
79→ Targets []MultiAssignTarget // targets on the left side
80→ Values []Expression // expressions on the right side
81→}
82→
83→func (m *MultiAssignment) statementNode() {}
84→func (m *MultiAssignment) TokenLiteral() string { return m.Token.Literal }
85→
86→// SeqBlock represents a SEQ block (sequential execution)
87→// If Replicator is non-nil, this is a replicated SEQ (SEQ i = 0 FOR n)
88→type SeqBlock struct {
89→ Token lexer.Token // the SEQ token
90→ Statements []Statement
91→ Replicator *Replicator // optional replicator
92→}
93→
94→func (s *SeqBlock) statementNode() {}
95→func (s *SeqBlock) TokenLiteral() string { return s.Token.Literal }
96→
97→// ParBlock represents a PAR block (parallel execution)
98→// If Replicator is non-nil, this is a replicated PAR (PAR i = 0 FOR n)
99→type ParBlock struct {
100→ Token lexer.Token // the PAR token{
"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→Now let me look at the Send and Receive AST nodes, and the parsing of send/receive with channel indexing, to understand how multi-dimensional indexing needs to be handled there.
{
"pattern": "type Send struct",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-A": 15
}361:type Send struct {
362- Token lexer.Token // the ! token
363- Channel string // channel name
364- ChannelIndex Expression // non-nil for cs[i] ! value
365- Value Expression // value to send (simple send, backward compat)
366- Values []Expression // additional values for sequential sends (c ! x ; y)
367- VariantTag string // variant tag name for variant sends (c ! tag ; x)
368-}
369-
370-func (s *Send) statementNode() {}
371-func (s *Send) TokenLiteral() string { return s.Token.Literal }
372-
373-/ Receive represents a channel receive: c ? x or c ? x ; y
374-type Receive struct {
375- Token lexer.Token // the ? token
376- Channel string // channel name{
"pattern": "type Receive struct",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-A": 20
}374:type Receive struct {
375- Token lexer.Token // the ? token
376- Channel string // channel name
377- ChannelIndex Expression // non-nil for cs[i] ? 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-
385-// AltBlock represents an ALT block (alternation/select)
386-// If Replicator is non-nil, this is a replicated ALT (ALT i = 0 FOR n)
387-type AltBlock struct {
388- Token lexer.Token // the ALT token
389- Cases []AltCase
390- Replicator *Replicator // optional replicator
391-}
392-
393-func (a *AltBlock) statementNode() {}
394-func (a *AltBlock) TokenLiteral() string { return a.Token.Literal }{
"pattern": "func.*parseSend|func.*parseReceive|ChannelIndex",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true
}716: ChannelIndex: index,
761: ChannelIndex: index,
1189:func (p *Parser) parseSend() *ast.Send {
1249:func (p *Parser) parseReceive() ast.Statement {
1388: ChannelIndex: channelIndex,
1739: altCase.ChannelIndex = p.parseExpression(LOWEST)
1777: altCase.ChannelIndex = p.parseExpression(LOWEST)Let me look at the core area where assignments with indexing are parsed — specifically the IDENT case in parseStatement.
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 680,
"limit": 120
} 680→ p.nextToken() // move past [
681→ index := p.parseExpression(LOWEST)
682→
683→ // Expect ]
684→ if !p.expectPeek(lexer.RBRACKET) {
685→ return nil
686→ }
687→
688→ // Check what follows ]
689→ if p.peekTokenIs(lexer.COMMA) {
690→ // Multi-assignment starting with indexed target: name[index], ... := ...
691→ firstTarget := ast.MultiAssignTarget{Name: name, Index: index}
692→ return p.parseMultiAssignmentFrom(firstTarget)
693→ }
694→ if p.peekTokenIs(lexer.ASSIGN) {
695→ // Indexed assignment: name[index] := value
696→ p.nextToken() // move to :=
697→ stmt := &ast.Assignment{
698→ Name: name,
699→ Token: p.curToken,
700→ Index: index,
701→ }
702→ p.nextToken() // move past :=
703→ stmt.Value = p.parseExpression(LOWEST)
704→ return stmt
705→ }
706→
707→ if p.peekTokenIs(lexer.SEND) {
708→ // Indexed channel send: cs[i] ! value
709→ p.nextToken() // move to !
710→ sendToken := p.curToken
711→ p.nextToken() // move past !
712→
713→ stmt := &ast.Send{
714→ Token: sendToken,
715→ Channel: name,
716→ ChannelIndex: index,
717→ }
718→
719→ // Check if this is a variant send: first token is an identifier that is a variant tag
720→ if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.SEMICOLON) {
721→ possibleTag := p.curToken.Literal
722→ if p.isVariantTag(possibleTag) {
723→ stmt.VariantTag = possibleTag
724→ p.nextToken() // move to ;
725→ for p.curTokenIs(lexer.SEMICOLON) {
726→ p.nextToken() // move past ;
727→ val := p.parseExpression(LOWEST)
728→ stmt.Values = append(stmt.Values, val)
729→ }
730→ return stmt
731→ }
732→ }
733→
734→ stmt.Value = p.parseExpression(LOWEST)
735→
736→ // Check for sequential send
737→ for p.peekTokenIs(lexer.SEMICOLON) {
738→ p.nextToken() // move to ;
739→ p.nextToken() // move past ;
740→ val := p.parseExpression(LOWEST)
741→ stmt.Values = append(stmt.Values, val)
742→ }
743→
744→ return stmt
745→ }
746→
747→ if p.peekTokenIs(lexer.RECEIVE) {
748→ // Indexed channel receive: cs[i] ? x or cs[i] ? CASE ...
749→ p.nextToken() // move to ?
750→ recvToken := p.curToken
751→
752→ // Check for variant receive: cs[i] ? CASE
753→ if p.peekTokenIs(lexer.CASE) {
754→ p.nextToken() // move to CASE
755→ return p.parseVariantReceiveWithIndex(name, index, recvToken)
756→ }
757→
758→ stmt := &ast.Receive{
759→ Token: recvToken,
760→ Channel: name,
761→ ChannelIndex: index,
762→ }
763→
764→ if !p.expectPeek(lexer.IDENT) {
765→ return nil
766→ }
767→ stmt.Variable = p.curToken.Literal
768→
769→ // Check for sequential receive
770→ for p.peekTokenIs(lexer.SEMICOLON) {
771→ p.nextToken() // move to ;
772→ if !p.expectPeek(lexer.IDENT) {
773→ return nil
774→ }
775→ stmt.Variables = append(stmt.Variables, p.curToken.Literal)
776→ }
777→
778→ return stmt
779→ }
780→
781→ // Default: treat as indexed assignment (shouldn't reach here normally)
782→ p.addError(fmt.Sprintf("expected :=, !, or ? after %s[...], got %s", name, p.peekToken.Type))
783→ return nil
784→}
785→
786→func (p *Parser) parseIndexExpression(left ast.Expression) *ast.IndexExpr {
787→ expr := &ast.IndexExpr{
788→ Token: p.curToken,
789→ Left: left,
790→ }
791→
792→ p.nextToken() // move past [
793→ expr.Index = p.parseExpression(LOWEST)
794→
795→ if !p.expectPeek(lexer.RBRACKET) {
796→ return nil
797→ }
798→
799→ return expr{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 530,
"limit": 100
} 530→func (p *Parser) parseArrayDecl() ast.Statement {
531→ lbracketToken := p.curToken
532→
533→ // Parse size expression after [
534→ p.nextToken()
535→ size := p.parseExpression(LOWEST)
536→
537→ // Check if this is a slice assignment: [arr FROM start FOR length] := value
538→ // Also handles [arr FOR length] shorthand (FROM 0)
539→ if p.peekTokenIs(lexer.FROM) || p.peekTokenIs(lexer.FOR) {
540→ return p.parseSliceAssignment(lbracketToken, size)
541→ }
542→
543→ // Expect ]
544→ if !p.expectPeek(lexer.RBRACKET) {
545→ return nil
546→ }
547→
548→ // Check if this is a channel array: [n]CHAN OF TYPE
549→ if p.peekTokenIs(lexer.CHAN) {
550→ p.nextToken() // move to CHAN
551→ chanDecl := &ast.ChanDecl{
552→ Token: p.curToken,
553→ IsArray: true,
554→ Size: size,
555→ }
556→
557→ // Expect OF (optional — CHAN BYTE is shorthand for CHAN OF BYTE)
558→ if p.peekTokenIs(lexer.OF) {
559→ p.nextToken() // consume OF
560→ }
561→
562→ // Expect type (INT, BYTE, BOOL, etc.) or protocol name (IDENT)
563→ p.nextToken()
564→ if p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
565→ p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
566→ p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
567→ chanDecl.ElemType = p.curToken.Literal
568→ } else if p.curTokenIs(lexer.IDENT) {
569→ chanDecl.ElemType = p.curToken.Literal
570→ } else {
571→ p.addError(fmt.Sprintf("expected type after CHAN, got %s", p.curToken.Type))
572→ return nil
573→ }
574→
575→ // Parse channel names
576→ for {
577→ if !p.expectPeek(lexer.IDENT) {
578→ return nil
579→ }
580→ chanDecl.Names = append(chanDecl.Names, p.curToken.Literal)
581→
582→ if p.peekTokenIs(lexer.COMMA) {
583→ p.nextToken() // consume comma
584→ } else {
585→ break
586→ }
587→ }
588→
589→ if !p.expectPeek(lexer.COLON) {
590→ return nil
591→ }
592→
593→ return chanDecl
594→ }
595→
596→ // Regular array declaration
597→ decl := &ast.ArrayDecl{Token: lbracketToken, Size: size}
598→
599→ // Expect type (INT, BYTE, BOOL, REAL, REAL32, REAL64)
600→ p.nextToken()
601→ if !p.curTokenIs(lexer.INT_TYPE) && !p.curTokenIs(lexer.BYTE_TYPE) &&
602→ !p.curTokenIs(lexer.BOOL_TYPE) && !p.curTokenIs(lexer.REAL_TYPE) &&
603→ !p.curTokenIs(lexer.REAL32_TYPE) && !p.curTokenIs(lexer.REAL64_TYPE) {
604→ p.addError(fmt.Sprintf("expected type after array size, got %s", p.curToken.Type))
605→ return nil
606→ }
607→ decl.Type = p.curToken.Literal
608→
609→ // Parse variable names
610→ for {
611→ if !p.expectPeek(lexer.IDENT) {
612→ return nil
613→ }
614→ decl.Names = append(decl.Names, p.curToken.Literal)
615→
616→ if p.peekTokenIs(lexer.COMMA) {
617→ p.nextToken() // consume comma
618→ } else {
619→ break
620→ }
621→ }
622→
623→ if !p.expectPeek(lexer.COLON) {
624→ return nil
625→ }
626→
627→ return decl
628→}
629→Now let me look at how proc params are parsed, especially the []CHAN OF TYPE and []TYPE patterns, to understand how to extend for multi-dimensional params.
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 2035,
"limit": 140
} 2035→ param.IsOpenArray = prevParam.IsOpenArray
2036→ param.ChanElemType = prevParam.ChanElemType
2037→ param.ArraySize = prevParam.ArraySize
2038→ param.Name = p.curToken.Literal
2039→
2040→ // Check for channel direction marker (? or !)
2041→ if (param.IsChan || param.IsChanArray) && (p.peekTokenIs(lexer.RECEIVE) || p.peekTokenIs(lexer.SEND)) {
2042→ p.nextToken()
2043→ param.ChanDir = p.curToken.Literal
2044→ }
2045→
2046→ params = append(params, param)
2047→ prevParam = ¶ms[len(params)-1]
2048→
2049→ if !p.peekTokenIs(lexer.COMMA) {
2050→ break
2051→ }
2052→ p.nextToken() // consume comma
2053→ p.nextToken() // move to next param
2054→ continue
2055→ }
2056→
2057→ // Check for VAL keyword
2058→ if p.curTokenIs(lexer.VAL) {
2059→ param.IsVal = true
2060→ p.nextToken()
2061→ }
2062→
2063→ // Check for RESULT keyword (output-only parameter — maps to pointer like non-VAL)
2064→ if p.curTokenIs(lexer.RESULT) {
2065→ // RESULT is semantically like non-VAL (pointer param), just skip it
2066→ p.nextToken()
2067→ }
2068→
2069→ // Check for []CHAN OF <type>, []TYPE (open array), or [n]TYPE (fixed-size array)
2070→ if p.curTokenIs(lexer.LBRACKET) {
2071→ if p.peekTokenIs(lexer.RBRACKET) {
2072→ // Open array: []CHAN OF TYPE or []TYPE
2073→ p.nextToken() // consume ]
2074→ p.nextToken() // move past ]
2075→ if p.curTokenIs(lexer.CHAN) {
2076→ // []CHAN OF <type> or []CHAN <type> (channel array parameter)
2077→ param.IsChan = true
2078→ param.IsChanArray = true
2079→ if p.peekTokenIs(lexer.OF) {
2080→ p.nextToken() // consume OF
2081→ }
2082→ p.nextToken() // move to element type
2083→ if isTypeToken(p.curToken.Type) || p.curTokenIs(lexer.IDENT) {
2084→ param.ChanElemType = p.curToken.Literal
2085→ } else {
2086→ p.addError(fmt.Sprintf("expected type after []CHAN, got %s", p.curToken.Type))
2087→ return params
2088→ }
2089→ p.nextToken()
2090→ } else if isTypeToken(p.curToken.Type) {
2091→ param.IsOpenArray = true
2092→ param.Type = p.curToken.Literal
2093→ p.nextToken()
2094→ } else if p.curTokenIs(lexer.IDENT) && p.recordNames[p.curToken.Literal] {
2095→ param.IsOpenArray = true
2096→ param.Type = p.curToken.Literal
2097→ p.nextToken()
2098→ } else {
2099→ p.addError(fmt.Sprintf("expected type after [], got %s", p.curToken.Type))
2100→ return params
2101→ }
2102→ } else {
2103→ // Fixed-size array: [n]TYPE
2104→ p.nextToken() // move past [
2105→ if !p.curTokenIs(lexer.INT) {
2106→ p.addError(fmt.Sprintf("expected array size, got %s", p.curToken.Type))
2107→ return params
2108→ }
2109→ param.ArraySize = p.curToken.Literal
2110→ if !p.expectPeek(lexer.RBRACKET) {
2111→ return params
2112→ }
2113→ p.nextToken() // move to type
2114→ if isTypeToken(p.curToken.Type) {
2115→ param.Type = p.curToken.Literal
2116→ } else if p.curTokenIs(lexer.IDENT) && p.recordNames[p.curToken.Literal] {
2117→ param.Type = p.curToken.Literal
2118→ } else {
2119→ p.addError(fmt.Sprintf("expected type after [%s], got %s", param.ArraySize, p.curToken.Type))
2120→ return params
2121→ }
2122→ p.nextToken()
2123→ }
2124→ } else if p.curTokenIs(lexer.CHAN) {
2125→ // Check for CHAN OF <type> or CHAN <type>
2126→ param.IsChan = true
2127→ if p.peekTokenIs(lexer.OF) {
2128→ p.nextToken() // consume OF
2129→ }
2130→ p.nextToken() // move to element type
2131→ if isTypeToken(p.curToken.Type) || p.curTokenIs(lexer.IDENT) {
2132→ param.ChanElemType = p.curToken.Literal
2133→ } else {
2134→ p.addError(fmt.Sprintf("expected type after CHAN, got %s", p.curToken.Type))
2135→ return params
2136→ }
2137→ p.nextToken()
2138→ } else if p.curTokenIs(lexer.IDENT) && p.recordNames[p.curToken.Literal] {
2139→ // Record type parameter
2140→ param.Type = p.curToken.Literal
2141→ p.nextToken()
2142→ } else {
2143→ // Expect scalar type
2144→ if !isTypeToken(p.curToken.Type) {
2145→ p.addError(fmt.Sprintf("expected type in parameter, got %s", p.curToken.Type))
2146→ return params
2147→ }
2148→ param.Type = p.curToken.Literal
2149→ p.nextToken()
2150→ }
2151→
2152→ // Expect identifier
2153→ if !p.curTokenIs(lexer.IDENT) {
2154→ p.addError(fmt.Sprintf("expected parameter name, got %s", p.curToken.Type))
2155→ return params
2156→ }
2157→ param.Name = p.curToken.Literal
2158→
2159→ // Check for channel direction marker (? or !)
2160→ if (param.IsChan || param.IsChanArray) && (p.peekTokenIs(lexer.RECEIVE) || p.peekTokenIs(lexer.SEND)) {
2161→ p.nextToken()
2162→ param.ChanDir = p.curToken.Literal
2163→ }
2164→
2165→ params = append(params, param)
2166→ prevParam = ¶ms[len(params)-1]
2167→
2168→ if !p.peekTokenIs(lexer.COMMA) {
2169→ break
2170→ }
2171→ p.nextToken() // consume comma
2172→ p.nextToken() // move to next param
2173→ }
2174→Now let me look at the IDENT case in parseStatement to understand the full flow when we see name[index][index2]....
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 635,
"limit": 50
} 635→ var startExpr ast.Expression
636→ if p.peekTokenIs(lexer.FOR) {
637→ // [arr FOR length] shorthand — start is 0
638→ startExpr = &ast.IntegerLiteral{Token: lexer.Token{Type: lexer.INT, Literal: "0"}, Value: 0}
639→ } else {
640→ p.nextToken() // consume FROM
641→ p.nextToken() // move to start expression
642→ startExpr = p.parseExpression(LOWEST)
643→ }
644→
645→ if !p.expectPeek(lexer.FOR) {
646→ return nil
647→ }
648→ p.nextToken() // move to length expression
649→ lengthExpr := p.parseExpression(LOWEST)
650→
651→ if !p.expectPeek(lexer.RBRACKET) {
652→ return nil
653→ }
654→
655→ if !p.expectPeek(lexer.ASSIGN) {
656→ return nil
657→ }
658→
659→ assignToken := p.curToken
660→ p.nextToken() // move past :=
661→
662→ value := p.parseExpression(LOWEST)
663→
664→ return &ast.Assignment{
665→ Token: assignToken,
666→ SliceTarget: &ast.SliceExpr{
667→ Token: lbracketToken,
668→ Array: arrayExpr,
669→ Start: startExpr,
670→ Length: lengthExpr,
671→ },
672→ Value: value,
673→ }
674→}
675→
676→func (p *Parser) parseIndexedOperation() ast.Statement {
677→ name := p.curToken.Literal
678→
679→ p.nextToken() // move to [
680→ p.nextToken() // move past [
681→ index := p.parseExpression(LOWEST)
682→
683→ // Expect ]
684→ if !p.expectPeek(lexer.RBRACKET) {Now I understand the full picture. The key issue is:
parseArrayDecl() only parses a single [size] before the type - it doesn't handle [s1][s2]TYPE or [s1][s2]CHAN OF TYPEparseIndexedOperation() only parses a single name[index] before checking for :=, !, or ? - it doesn't handle name[i][j] ProcParam only supports single-dimensional open arrays []TYPE and []CHAN OF TYPE, not [][]TYPE or [][][]CHAN OF TYPE
To fix this, I need to refactor the AST nodes to support multiple dimensions: ArrayDecl and ChanDecl should store lists of sizes instead of single sizes, ProcParam needs a dimensions field to track nesting levels for open arrays, and both Send and Receive operations need to handle multiple indices instead of just one.
But I'm second-guessing myself on the best approach for Send, Receive, and Assignment — I could either extend them to use []Expression for indices, or leverage the existing IndexExpr chain pattern that already handles nested expressions well.
Looking at the current structure more carefully, Send and Receive store the channel name separately from a single index, so extending to multiple indices with ChannelIndices []Expression seems like the natural fit, though Assignment needs similar treatment for its name-index relationship. For assignment statements, I could either store a chain of IndexExpr objects or add an Indices field to hold multiple expressions. The cleanest solution is probably to leverage the fact that IndexExpr already chains naturally—I could use that same chaining pattern consistently across assignment, send, and receive operations rather than introducing separate fields for each construct.
The real parsing issue is that parseIndexedOperation() stops after parsing name[i] and immediately checks for !, ?, or :=, so it fails when it encounters another [. I need to loop after the first index to collect any additional bracketed indices before checking for the operation token.
For the AST changes, I should update the struct fields to hold multiple indices: Send and Receive get ChannelIndices []Expression, Assignment gets Indices []Expression, and AltCase gets ChannelIndices []Expression as well. Continuing with the AST node updates, I need to change the index fields across several more structures - AltCase, Assignment, and MultiAssignTarget all get converted to plural indices arrays. For code generation, I'll need to loop through all indices when emitting bracket notation. Then for declarations, both ArrayDecl and ChanDecl need their size fields converted to arrays to support multiple dimensions. For procedure parameters with multi-dimensional open arrays, I'm tracking the nesting level with an ArrayDimensions field to know how many bracket pairs are involved.
Looking at the actual life.occ examples, I can see parameters like [][][]CHAN OF STATE with three open dimensions and [][]CHAN OF COMMAND with two - these need to map correctly to Go's nested slice and channel types. The current IsChanArray boolean isn't enough; I need to track the actual number of nesting levels, so I'll convert that to an integer field instead.
Regular multi-dimensional arrays also need support beyond just channels, so I should add an OpenArrayDims field to handle cases like [][]INT arr. For the code generation side, I'm working through how to properly initialize these nested channel arrays with the right loop structure.
Generating nested initialization code... I'm realizing the real issue is at the statement level in parseIndexedOperation() — when it encounters name[i][j] ! value, it successfully parses name[i] but then sees another [ instead of the ! operator, so it fails. The expression parser already handles chained indexing correctly by creating nested IndexExpr nodes, but the statement parser needs to be fixed to support multi-dimensional array assignments. I should also check how ChannelIndex is being used in the codegen for Send and Receive operations.
Updating the codegen to handle multiple indices...
The expression parser already handles chained indexing correctly since it treats [ as an infix operator that can be applied repeatedly, so control[x][y] naturally parses into nested IndexExpr nodes without any changes needed.
The real problem is at the statement level—parseIndexedOperation, parseArrayDecl, and parseProcParams don't support chaining. Let me trace through how parseIndexedOperation gets triggered when the parser encounters an identifier followed by [ in a statement context, and then check what the ALT case looks like to understand the full scope of what needs fixing.
Let me check how ALT case handles channel indices and how parseStatement dispatches to parseIndexedOperation.
{
"pattern": "ChannelIndex",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-n": true
}364: ChannelIndex Expression // non-nil for cs[i] ! value 377: ChannelIndex Expression // non-nil for cs[i] ? x 400: ChannelIndex Expression // non-nil for cs[i] ? x in ALT 450: ChannelIndex Expression // non-nil for cs[i] ? CASE ...
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1720,
"limit": 80
} 1720→ return nil
1721→ }
1722→ p.nextToken() // move past AFTER
1723→ altCase.Deadline = p.parseExpression(LOWEST)
1724→ } else {
1725→ // Simple case: channel ? var
1726→ altCase.Channel = name
1727→ p.nextToken() // move to ?
1728→ if !p.expectPeek(lexer.IDENT) {
1729→ return nil
1730→ }
1731→ altCase.Variable = p.curToken.Literal
1732→ }
1733→ } else if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.LBRACKET) {
1734→ // Indexed channel case: cs[i] ? var
1735→ name := p.curToken.Literal
1736→ altCase.Channel = name
1737→ p.nextToken() // move to [
1738→ p.nextToken() // move past [
1739→ altCase.ChannelIndex = p.parseExpression(LOWEST)
1740→ if !p.expectPeek(lexer.RBRACKET) {
1741→ return nil
1742→ }
1743→ if !p.expectPeek(lexer.RECEIVE) {
1744→ return nil
1745→ }
1746→ if !p.expectPeek(lexer.IDENT) {
1747→ return nil
1748→ }
1749→ altCase.Variable = p.curToken.Literal
1750→ } else {
1751→ // Guard followed by & channel ? var, or guard & SKIP
1752→ guard := p.parseExpression(LOWEST)
1753→ altCase.Guard = guard
1754→
1755→ // Expect &
1756→ if !p.peekTokenIs(lexer.AMPERSAND) {
1757→ p.addError("expected & after guard in ALT case")
1758→ return nil
1759→ }
1760→ p.nextToken() // move to &
1761→ p.nextToken() // move past &
1762→
1763→ if p.curTokenIs(lexer.SKIP) {
1764→ // Guarded SKIP: guard & SKIP
1765→ altCase.IsSkip = true
1766→ } else if !p.curTokenIs(lexer.IDENT) {
1767→ p.addError(fmt.Sprintf("expected channel name or SKIP after guard, got %s", p.curToken.Type))
1768→ return nil
1769→ } else {
1770→ // Channel operation after guard
1771→ altCase.Channel = p.curToken.Literal
1772→
1773→ if p.peekTokenIs(lexer.LBRACKET) {
1774→ // Indexed channel with guard: guard & cs[i] ? var
1775→ p.nextToken() // move to [
1776→ p.nextToken() // move past [
1777→ altCase.ChannelIndex = p.parseExpression(LOWEST)
1778→ if !p.expectPeek(lexer.RBRACKET) {
1779→ return nil
1780→ }
1781→ }
1782→
1783→ if !p.expectPeek(lexer.RECEIVE) {
1784→ return nil
1785→ }
1786→ if !p.expectPeek(lexer.IDENT) {
1787→ return nil
1788→ }
1789→ altCase.Variable = p.curToken.Literal
1790→ }
1791→ }
1792→
1793→ // Skip to next line for the body
1794→ for p.peekTokenIs(lexer.NEWLINE) {
1795→ p.nextToken()
1796→ }
1797→
1798→ // Expect INDENT for body
1799→ if !p.peekTokenIs(lexer.INDENT) {Now let me look at how ChannelIndex is used in codegen for all four AST types (Send, Receive, AltCase, VariantReceive).
{
"pattern": "ChannelIndex",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true,
"-C": 3
}1157-func (g *Generator) generateSend(send *ast.Send) {
1158- g.builder.WriteString(strings.Repeat("\t", g.indent))
1159- g.write(goIdent(send.Channel))
1160: if send.ChannelIndex != nil {
1161- g.write("[")
1162: g.generateExpression(send.ChannelIndex)
1163- g.write("]")
1164- }
1165- g.write(" <- ")
--
1203-
1204-func (g *Generator) generateReceive(recv *ast.Receive) {
1205- chanRef := goIdent(recv.Channel)
1206: if recv.ChannelIndex != nil {
1207- var buf strings.Builder
1208- buf.WriteString(goIdent(recv.Channel))
1209- buf.WriteString("[")
1210- // Generate the index expression into a temporary buffer
1211- oldBuilder := g.builder
1212- g.builder = strings.Builder{}
1213: g.generateExpression(recv.ChannelIndex)
1214- buf.WriteString(g.builder.String())
1215- g.builder = oldBuilder
1216- buf.WriteString("]")
--
1294- protoName := g.chanProtocols[vr.Channel]
1295- gProtoName := goIdent(protoName)
1296- chanRef := goIdent(vr.Channel)
1297: if vr.ChannelIndex != nil {
1298- var buf strings.Builder
1299- buf.WriteString(goIdent(vr.Channel))
1300- buf.WriteString("[")
1301- oldBuilder := g.builder
1302- g.builder = strings.Builder{}
1303: g.generateExpression(vr.ChannelIndex)
1304- buf.WriteString(g.builder.String())
1305- g.builder = oldBuilder
1306- buf.WriteString("]")
--
1790- g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
1791- } else if c.Guard != nil {
1792- g.write(fmt.Sprintf("case %s = <-_alt%d:\n", goIdent(c.Variable), i))
1793: } else if c.ChannelIndex != nil {
1794- g.write(fmt.Sprintf("case %s = <-%s[", goIdent(c.Variable), goIdent(c.Channel)))
1795: g.generateExpression(c.ChannelIndex)
1796- g.write("]:\n")
1797- } else {
1798- g.write(fmt.Sprintf("case %s = <-%s:\n", goIdent(c.Variable), goIdent(c.Channel)))
--
1870- // Build select case entry
1871- g.builder.WriteString(strings.Repeat("\t", g.indent))
1872- g.write("_altCases[_altI] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(")
1873: if c.ChannelIndex != nil {
1874- g.write(goIdent(c.Channel) + "[")
1875: g.generateExpression(c.ChannelIndex)
1876- g.write("]")
1877- } else {
1878- g.write(goIdent(c.Channel)){
"pattern": "type VariantReceive struct",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-A": 10
}447:type VariantReceive struct {
448- Token lexer.Token // the ? token
449- Channel string
450- ChannelIndex Expression // non-nil for cs[i] ? CASE ...
451- Cases []VariantCase
452-}
453-
454-type VariantCase struct {
455- Tag string // variant tag name
456- Variables []string // variables to bind payload fields
457- Body Statement{
"pattern": "type AltCase struct",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-A": 20
}397:type AltCase struct {
398- Guard Expression // optional guard condition (nil if no guard)
399- Channel string // channel name
400- ChannelIndex Expression // non-nil for cs[i] ? x in ALT
401- Variable string // variable to receive into
402- Body []Statement // the body to execute
403- IsTimer bool // true if this is a timer AFTER case
404- IsSkip bool // true if this is a guarded SKIP case (guard & SKIP)
405- Timer string // timer name (when IsTimer)
406- Deadline Expression // AFTER deadline expression (when IsTimer)
407- Declarations []Statement / scoped declarations before channel input (e.g., BYTE ch:)
408-}
409-
410-/ TimerDecl represents a timer declaration: TIMER tim:
411-type TimerDecl struct {
412- Token lexer.Token // the TIMER token
413- Names []string // timer variable names
414-}
415-
416-func (td *TimerDecl) statementNode() {}
417-func (td *TimerDecl) TokenLiteral() string { return td.Token.Literal }Now let me also look at the generateChanDecl and generateArrayDecl in codegen, and how proc params are generated, to understand the full codegen picture.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 2036,
"limit": 35
} 2036→func (g *Generator) generateProcParams(params []ast.ProcParam) string {
2037→ var parts []string
2038→ for _, p := range params {
2039→ var goType string
2040→ if p.IsChanArray {
2041→ goType = "[]chan " + g.occamTypeToGo(p.ChanElemType)
2042→ } else if p.IsChan {
2043→ goType = chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)
2044→ } else if p.IsOpenArray {
2045→ goType = "[]" + g.occamTypeToGo(p.Type)
2046→ } else if p.ArraySize != "" {
2047→ // Fixed-size array parameter: use slice for Go compatibility
2048→ // (occam [n]TYPE and []TYPE both map to Go slices)
2049→ goType = "[]" + g.occamTypeToGo(p.Type)
2050→ } else {
2051→ goType = g.occamTypeToGo(p.Type)
2052→ if !p.IsVal {
2053→ // Non-VAL parameters are pass by reference in Occam
2054→ goType = "*" + goType
2055→ }
2056→ }
2057→ pName := goIdent(p.Name)
2058→ if renamed, ok := g.retypesRenames[p.Name]; ok {
2059→ pName = renamed
2060→ }
2061→ parts = append(parts, fmt.Sprintf("%s %s", pName, goType))
2062→ }
2063→ return strings.Join(parts, ", ")
2064→}
2065→
2066→func chanDirPrefix(dir string) string {
2067→ switch dir {
2068→ case "?":
2069→ return "<-chan " // input/receive-only
2070→ case "!":Good. Now I have a complete understanding. Let me now launch a Plan agent to design the implementation.
{
"subagent_type": "Plan",
"prompt": "I need to implement multi-dimensional array support for an occam-to-Go transpiler (issue #74). Based on thorough exploration, here's the current state and what needs to change:\n\n## What needs to work (from life.occ)\n\n**Multi-dim channel array declarations:**\n```occam\n[array.width][array.height][neighbours]CHAN OF STATE link :\n[array.width][array.height]CHAN OF COMMAND control :\n```\n→ Go: nested make() with init loops\n\n**Multi-dim regular array declarations:**\n```occam\n[5][3]INT arr :\n```\n→ Go: nested make() loops\n\n**Multi-dim open array proc params:**\n```occam\nPROC broadcast.present.state([][][]CHAN OF STATE link, ...)\nPROC generation([][]CHAN OF COMMAND control, ...)\n```\n→ Go: `func broadcast_present_state(link [][][]chan bool, ...)`\n\n**Multi-dim indexed send/receive/assignment:**\n```occam\nlink[x][y][d] ! state\nlink[nx[d]][ny[d]][d] ? state.of.neighbour[d]\ncontrol[x][y] ! evolve\nsense[x][y] ? changed; next.state\ncontrol[x][y] ! set.state; state\n```\n\n**Multi-dim indexed in proc call args:**\n```occam\ncell(link, x, y, nx, ny, control[x][y], sense[x][y])\n```\n→ This already works via chained IndexExpr in expression parsing.\n\n## Current AST (what needs changing)\n\n1. **`ArrayDecl`** has `Size Expression` (single) - needs `Sizes []Expression`\n2. **`ChanDecl`** has `Size Expression` (single) + `IsArray bool` - needs `Sizes []Expression` (len > 0 means array)\n3. **`Send`** has `ChannelIndex Expression` (single) - needs `ChannelIndices []Expression`\n4. **`Receive`** has `ChannelIndex Expression` (single) - needs `ChannelIndices []Expression`\n5. **`AltCase`** has `ChannelIndex Expression` (single) - needs `ChannelIndices []Expression`\n6. **`VariantReceive`** has `ChannelIndex Expression` (single) - needs `ChannelIndices []Expression`\n7. **`Assignment`** has `Index Expression` (single) - needs to support multi-dim. Could change to `Indices []Expression`\n8. **`MultiAssignTarget`** has `Index Expression` (single) - needs `Indices []Expression`\n9. **`ProcParam`** has `IsChanArray bool` and `IsOpenArray bool` - needs a dimension count: `ChanArrayDims int` / `OpenArrayDims int`\n\n## Current Parser (what needs changing)\n\n1. **`parseArrayDecl()`** (parser.go ~530): Parses `[size]` then checks for type/CHAN. Need to loop: parse `[size]`, check if next is `[`, if so parse another dimension, etc.\n\n2. **`parseIndexedOperation()`** (parser.go ~676): Parses `name[index]` then checks for `:=`, `!`, `?`. Need to loop collecting indices when seeing `[` after `]`.\n\n3. **`parseProcParams()`** (parser.go ~2069): Handles `[]CHAN OF TYPE` and `[]TYPE`. Need to count consecutive `[]` pairs for multi-dim open arrays.\n\n4. **`parseAltCases()`** (parser.go ~1733-1781): Parses single `cs[i]`. Need to loop for multi-dim.\n\n## Current Codegen (what needs changing)\n\n1. **`generateArrayDecl()`** (~1146): Generates `make([]type, size)`. Need nested make + init loops.\n2. **`generateChanDecl()`** (~1117): Generates single-level make + init loop. Need nested loops.\n3. **`generateSend()`** (~1157): Emits single `[index]`. Need loop for all indices.\n4. **`generateReceive()`** (~1204): Same.\n5. **`generateProcParams()`** (~2036): `[]chan type` for IsChanArray. Need `[][][]chan type` for multi-dim.\n6. **`generateAssignment()`** (~1582): Single `[index]`. Need multiple.\n7. **`generateMultiAssignment()`** (~2210): Single `[index]`. Need multiple.\n8. ALT codegen (~1790, ~1870): Single index. Need multiple.\n9. Variant receive codegen (~1294): Single index. Need multiple.\n\n## Key patterns to preserve\n\n- Expression-level chained IndexExpr already works for multi-dim in expressions (e.g. `control[x][y]` as a proc call arg)\n- `goIdent()` transforms names\n- `occamTypeToGo()` converts types\n- `refParams` and `recordVars` maps affect codegen\n- Existing single-dim tests must all still pass\n\n## Questions for the plan\n\nPlease design a complete implementation plan with:\n1. Exact AST field changes\n2. Parser changes (with pseudocode for the new loops) \n3. Codegen changes (with pseudocode for nested init loops)\n4. Test strategy\n\nPrioritize minimal, focused changes that don't break existing single-dim functionality. The existing field names should be renamed to plural forms where sensible."
}[
{
"type": "text",
"text": "Now I have all the information needed. Let me design the complete implementation plan.\n\n---\n\n## Multi-Dimensional Array Implementation Plan (Issue #74)\n\nThis plan covers all changes needed across 4 files (`ast/ast.go`, `parser/parser.go`, `codegen/codegen.go`, and test files) to support multi-dimensional arrays and channel arrays in the occam2go transpiler.\n\n---\n\n### Phase 1: AST Field Changes (`/home/david/projects/code-associates/occam2go/ast/ast.go`)\n\nThe strategy is to rename single-value fields to plural slice forms. For backward compatibility with single-dim code, `len(Sizes) == 1` replaces the old `Size != nil` / `IsArray` patterns.\n\n**1.1 ArrayDecl (line 47)**\n\n```go\n// Before:\ntype ArrayDecl struct {\n Token lexer.Token\n Size Expression // single\n Type string\n Names []string\n}\n\n// After:\ntype ArrayDecl struct {\n Token lexer.Token\n Sizes []Expression // one per dimension: [5][3]INT -> Sizes=[5, 3]\n Type string\n Names []string\n}\n```\n\n**1.2 ChanDecl (line 349)**\n\n```go\n// Before:\ntype ChanDecl struct {\n Token lexer.Token\n ElemType string\n Names []string\n IsArray bool\n Size Expression\n}\n\n// After:\ntype ChanDecl struct {\n Token lexer.Token\n ElemType string\n Names []string\n Sizes []Expression // empty = scalar CHAN; len>0 = array (one entry per dim)\n}\n```\n\nThe `IsArray` field is removed; callers check `len(decl.Sizes) > 0` instead.\n\n**1.3 Send (line 361)**\n\n```go\n// Before:\n ChannelIndex Expression // non-nil for cs[i] ! value\n\n// After:\n ChannelIndices []Expression // one per dimension: cs[x][y] ! val -> [x, y]\n```\n\n**1.4 Receive (line 373)**\n\n```go\n// Before:\n ChannelIndex Expression\n\n// After:\n ChannelIndices []Expression\n```\n\n**1.5 AltCase (line 397)**\n\n```go\n// Before:\n ChannelIndex Expression\n\n// After:\n ChannelIndices []Expression\n```\n\n**1.6 VariantReceive (line 448)**\n\n```go\n// Before:\n ChannelIndex Expression\n\n// After:\n ChannelIndices []Expression\n```\n\n**1.7 Assignment (line 58)**\n\n```go\n// Before:\n Index Expression\n\n// After:\n Indices []Expression // nil/empty for simple, one per dim for arr[i][j] := val\n```\n\n**1.8 MultiAssignTarget (line 71)**\n\n```go\n// Before:\n Index Expression\n\n// After:\n Indices []Expression\n```\n\n**1.9 ProcParam (line 144)**\n\n```go\n// Before:\n IsChanArray bool\n IsOpenArray bool\n\n// After:\n ChanArrayDims int // 0 = scalar CHAN, 1 = []CHAN, 2 = [][]CHAN, etc.\n OpenArrayDims int // 0 = scalar, 1 = []TYPE, 2 = [][]TYPE, etc.\n```\n\nRemove `IsChanArray` and `IsOpenArray`. Old code checked `p.IsChanArray` -- now check `p.ChanArrayDims > 0`. Old code checked `p.IsOpenArray` -- now check `p.OpenArrayDims > 0`.\n\n---\n\n### Phase 2: Parser Changes (`/home/david/projects/code-associates/occam2go/parser/parser.go`)\n\n**2.1 parseArrayDecl() (line 530)**\n\nCurrent: Parses `[size]` then dispatches to CHAN or regular array. \n\nNew: Loop to collect multiple `[size]` dimensions before dispatching.\n\nPseudocode:\n```\nfunc parseArrayDecl():\n lbracketToken = curToken\n nextToken()\n firstSize = parseExpression(LOWEST)\n \n // Check for slice assignment (only valid for 1st dim)\n if peekTokenIs(FROM) || peekTokenIs(FOR):\n return parseSliceAssignment(lbracketToken, firstSize)\n \n expectPeek(RBRACKET)\n \n sizes = [firstSize]\n \n // Loop: collect additional [size] dimensions\n while peekTokenIs(LBRACKET):\n nextToken() // move to [\n nextToken() // move past [\n size = parseExpression(LOWEST)\n expectPeek(RBRACKET)\n sizes = append(sizes, size)\n \n // Check if channel array: [w][h]CHAN OF TYPE\n if peekTokenIs(CHAN):\n nextToken() // move to CHAN\n chanDecl = &ChanDecl{Token: curToken, Sizes: sizes}\n // ... parse OF, type, names, colon (same as before)\n return chanDecl\n \n // Regular array: [w][h]INT arr:\n decl = &ArrayDecl{Token: lbracketToken, Sizes: sizes}\n // ... parse type, names, colon (same as before)\n return decl\n```\n\n**2.2 parseIndexedOperation() (line 676)**\n\nCurrent: Parses `name[index]` then checks for `:=`, `!`, `?`, `,`. \n\nNew: After parsing `name[index]`, loop to collect more `[index]` if peek is `[`, then dispatch with the full indices list.\n\nPseudocode:\n```\nfunc parseIndexedOperation():\n name = curToken.Literal\n nextToken() // move to [\n nextToken() // move past [\n index = parseExpression(LOWEST)\n expectPeek(RBRACKET)\n \n indices = [index]\n \n // Loop: collect additional [index] dimensions\n while peekTokenIs(LBRACKET):\n nextToken() // move to [\n nextToken() // move past [\n idx = parseExpression(LOWEST)\n expectPeek(RBRACKET)\n indices = append(indices, idx)\n \n // Check what follows the last ]\n if peekTokenIs(COMMA):\n firstTarget = MultiAssignTarget{Name: name, Indices: indices}\n return parseMultiAssignmentFrom(firstTarget)\n \n if peekTokenIs(ASSIGN):\n nextToken()\n stmt = &Assignment{Name: name, Token: curToken, Indices: indices}\n nextToken()\n stmt.Value = parseExpression(LOWEST)\n return stmt\n \n if peekTokenIs(SEND):\n // ... same as before but use ChannelIndices: indices\n \n if peekTokenIs(RECEIVE):\n // ... same as before but use ChannelIndices: indices\n```\n\n**2.3 parseProcParams() (line 2005)**\n\nFor `[]CHAN` and `[]TYPE`, need to count consecutive `[]` pairs.\n\nChange the `if p.curTokenIs(lexer.LBRACKET)` block (line 2070):\n\n```\nif curTokenIs(LBRACKET):\n if peekTokenIs(RBRACKET):\n // Open array: could be [][]...CHAN OF TYPE or [][]...TYPE\n dims = 0\n while curTokenIs(LBRACKET) && peekTokenIs(RBRACKET):\n dims++\n nextToken() // consume ]\n nextToken() // move past ] to next [ or type\n \n if curTokenIs(CHAN):\n param.IsChan = true\n param.ChanArrayDims = dims\n // ... parse OF, type (same as before)\n else if isTypeToken(...):\n param.OpenArrayDims = dims\n param.Type = curToken.Literal\n nextToken()\n // ... (record type case, error case)\n else:\n // Fixed-size array: [n]TYPE (unchanged)\n```\n\nFor the **shared-type parameter** block (line 2029), copy `ChanArrayDims` and `OpenArrayDims` instead of `IsChanArray` and `IsOpenArray`.\n\n**2.4 parseAltCase() (line 1679)**\n\nAt line 1733 (indexed channel in ALT: `cs[i] ? var`), and line 1773 (indexed channel with guard), need to loop collecting multiple indices:\n\n```\n// At line 1733 branch: \"cs[i][j] ? var\"\nname = curToken.Literal\naltCase.Channel = name\nwhile peekTokenIs(LBRACKET):\n nextToken() // move to [\n nextToken() // move past [\n idx = parseExpression(LOWEST)\n expectPeek(RBRACKET)\n altCase.ChannelIndices = append(altCase.ChannelIndices, idx)\nexpectPeek(RECEIVE)\nexpectPeek(IDENT)\naltCase.Variable = curToken.Literal\n```\n\nSame pattern at line 1773 for the guarded case.\n\n**2.5 parseMultiAssignmentFrom() (line 493)**\n\nThe inner loop at line 503 that parses subsequent targets (`target.Index = parseExpression(...)`) also needs multi-index support:\n\n```\nif peekTokenIs(LBRACKET):\n while peekTokenIs(LBRACKET):\n nextToken() // move to [\n nextToken() // move past [\n target.Indices = append(target.Indices, parseExpression(LOWEST))\n expectPeek(RBRACKET)\n```\n\n---\n\n### Phase 3: Codegen Changes (`/home/david/projects/code-associates/occam2go/codegen/codegen.go`)\n\n**3.1 Helper: generateIndices()**\n\nAdd a small helper used by send, receive, assignment, alt, etc.:\n\n```go\nfunc (g *Generator) generateIndices(indices []Expression) {\n for _, idx := range indices {\n g.write(\"[\")\n g.generateExpression(idx)\n g.write(\"]\")\n }\n}\n```\n\n**3.2 generateArrayDecl() (line 1146)**\n\nCurrent: `make([]type, size)`. New: nested `make` + init loops for multi-dim.\n\nFor `[5][3]INT arr:` (Sizes=[5, 3]):\n```go\narr := make([][]int, 5)\nfor _i0 := range arr { arr[_i0] = make([]int, 3) }\n```\n\nFor 3 dims `[a][b][c]INT arr:` (Sizes=[a, b, c]):\n```go\narr := make([][][]int, a)\nfor _i0 := range arr {\n arr[_i0] = make([][]int, b)\n for _i1 := range arr[_i0] {\n arr[_i0][_i1] = make([]int, c)\n }\n}\n```\n\nPseudocode:\n```\nfunc generateArrayDecl(decl):\n goType = occamTypeToGo(decl.Type)\n for each name in decl.Names:\n n = goIdent(name)\n dims = len(decl.Sizes)\n \n // Build the outermost type: [][]...type\n slicePrefix = strings.Repeat(\"[]\", dims)\n write(n + \" := make(\" + slicePrefix + goType + \", \")\n generateExpression(decl.Sizes[0])\n write(\")\\n\")\n \n // Nested init loops for dims > 1\n if dims > 1:\n generateNestedArrayInit(n, decl.Sizes, goType, 0)\n\nfunc generateNestedArrayInit(base, sizes, goType, depth):\n // depth ranges from 0 to len(sizes)-2\n loopVar = \"_i\" + strconv.Itoa(depth)\n accessor = base + \"[\" + loopVar + \"]\"\n \n remaining = len(sizes) - depth - 1\n slicePrefix = strings.Repeat(\"[]\", remaining)\n \n writeLine(\"for \" + loopVar + \" := range \" + base + \" {\")\n indent++\n \n write(accessor + \" = make(\" + slicePrefix + goType + \", \")\n generateExpression(sizes[depth+1])\n write(\")\\n\")\n \n if depth + 2 < len(sizes):\n generateNestedArrayInit(accessor, sizes, goType, depth+1)\n \n indent--\n writeLine(\"}\")\n```\n\n**3.3 generateChanDecl() (line 1117)**\n\nSame pattern as arrays but innermost init creates `make(chan type)`.\n\nFor `[w][h]CHAN OF INT cs:` (Sizes=[w, h]):\n```go\ncs := make([][]chan int, w)\nfor _i0 := range cs {\n cs[_i0] = make([]chan int, h)\n for _i1 := range cs[_i0] {\n cs[_i0][_i1] = make(chan int)\n }\n}\n```\n\nFor `[w][h][n]CHAN OF STATE link:` (Sizes=[w, h, n]):\n```go\nlink := make([][][]chan _proto_STATE, w)\nfor _i0 := range link {\n link[_i0] = make([][]chan _proto_STATE, h)\n for _i1 := range link[_i0] {\n link[_i0][_i1] = make([]chan _proto_STATE, n)\n for _i2 := range link[_i0][_i1] {\n link[_i0][_i1][_i2] = make(chan _proto_STATE)\n }\n }\n}\n```\n\nPseudocode:\n```\nfunc generateChanDecl(decl):\n goType = occamTypeToGo(decl.ElemType)\n if len(decl.Sizes) == 0:\n // Scalar channel (no array), same as before\n for name in decl.Names:\n writeLine(goIdent(name) + \" := make(chan \" + goType + \")\")\n return\n \n for name in decl.Names:\n n = goIdent(name)\n dims = len(decl.Sizes)\n slicePrefix = strings.Repeat(\"[]\", dims)\n write(n + \" := make(\" + slicePrefix + \"chan \" + goType + \", \")\n generateExpression(decl.Sizes[0])\n write(\")\\n\")\n generateNestedChanInit(n, decl.Sizes, goType, 0)\n```\n\nThe nested init is similar to arrays but the innermost level creates `make(chan type)` instead of `make([]type, size)`.\n\n**3.4 generateSend() (line 1157)**\n\nReplace:\n```go\nif send.ChannelIndex != nil {\n g.write(\"[\")\n g.generateExpression(send.ChannelIndex)\n g.write(\"]\")\n}\n```\nWith:\n```go\ng.generateIndices(send.ChannelIndices)\n```\n\n**3.5 generateReceive() (line 1204)**\n\nReplace the `ChannelIndex` block (lines 1206-1217) to loop over `ChannelIndices`:\n```go\nif len(recv.ChannelIndices) > 0 {\n var buf strings.Builder\n buf.WriteString(goIdent(recv.Channel))\n for _, idx := range recv.ChannelIndices {\n buf.WriteString(\"[\")\n oldBuilder := g.builder\n g.builder = strings.Builder{}\n g.generateExpression(idx)\n buf.WriteString(g.builder.String())\n g.builder = oldBuilder\n buf.WriteString(\"]\")\n }\n chanRef = buf.String()\n}\n```\n\n**3.6 generateVariantReceive() (line 1293)**\n\nSame pattern as receive -- loop over `ChannelIndices`.\n\n**3.7 generateAssignment() (line 1582)**\n\nReplace the `assign.Index != nil` check (line 1602) with `len(assign.Indices) > 0`:\n\n```go\nif len(assign.Indices) > 0 {\n // Check if first index is record field access\n if _, ok := g.recordVars[assign.Name]; ok && len(assign.Indices) == 1 {\n if ident, ok := assign.Indices[0].(*ast.Identifier); ok {\n // Record field: p.x = value\n ...\n return\n }\n }\n // Array index: dereference if ref param\n if g.refParams[assign.Name] {\n g.write(\"*\")\n }\n g.write(goIdent(assign.Name))\n g.generateIndices(assign.Indices)\n}\n```\n\n**3.8 generateMultiAssignment() (line 2210)**\n\nReplace `target.Index != nil` (line 2216) with `len(target.Indices) > 0`. For record field access, check `len(target.Indices) == 1` and first index is Identifier. For array indices, loop:\n\n```go\nif len(target.Indices) > 0 {\n // record field check (same pattern, only for len==1)\n ...\n // array indices\n g.write(goIdent(target.Name))\n g.generateIndices(target.Indices)\n}\n```\n\n**3.9 generateProcParams() (line 2036)**\n\nReplace `IsChanArray` / `IsOpenArray` checks:\n\n```go\nif p.ChanArrayDims > 0 {\n goType = strings.Repeat(\"[]\", p.ChanArrayDims) + \"chan \" + g.occamTypeToGo(p.ChanElemType)\n} else if p.IsChan {\n goType = chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)\n} else if p.OpenArrayDims > 0 {\n goType = strings.Repeat(\"[]\", p.OpenArrayDims) + g.occamTypeToGo(p.Type)\n} else if p.ArraySize != \"\" {\n goType = \"[]\" + g.occamTypeToGo(p.Type)\n} else {\n // scalar\n}\n```\n\n**3.10 generateAltBlock() (line 1746)**\n\nAt line 1793 (non-guarded indexed channel):\n```go\n} else if len(c.ChannelIndices) > 0 {\n g.write(fmt.Sprintf(\"case %s = <-%s\", goIdent(c.Variable), goIdent(c.Channel)))\n // generate all indices\n for _, idx := range c.ChannelIndices {\n g.write(\"[\")\n g.generateExpression(idx)\n g.write(\"]\")\n }\n g.write(\":\\n\")\n```\n\nAt line 1777 (guarded channel, already assigns `_altN` -- the guard pattern might need adjustment for multi-dim indexed channels).\n\n**3.11 generateReplicatedAlt() (line 1809)**\n\nAt line 1873 (building reflect.SelectCase):\n```go\nif len(c.ChannelIndices) > 0 {\n g.write(goIdent(c.Channel))\n for _, idx := range c.ChannelIndices {\n g.write(\"[\")\n g.generateExpression(idx)\n g.write(\"]\")\n }\n}\n```\n\n**3.12 generateProcCall() (line 2077)**\n\nUpdate the address-of check at line 2103 to use the new field names:\n```go\nif i < len(params) && !params[i].IsVal && !params[i].IsChan && params[i].ChanArrayDims == 0 && params[i].OpenArrayDims == 0 && params[i].ArraySize == \"\" {\n g.write(\"&\")\n}\n```\n\nAnd the string-to-byte-slice wrapping at line 2107:\n```go\nif _, isStr := arg.(*ast.StringLiteral); isStr && i < len(params) && params[i].OpenArrayDims > 0 && params[i].Type == \"BYTE\" {\n```\n\n**3.13 generateProcDecl() (line 1929)**\n\nAt line 1946, update the refParams check:\n```go\nif !p.IsVal && !p.IsChan && p.ChanArrayDims == 0 && p.OpenArrayDims == 0 && p.ArraySize == \"\" {\n newRefParams[p.Name] = true\n}\n```\n\nAt line 1953, update BOOL tracking:\n```go\nif p.Type == \"BOOL\" && !p.IsChan && p.ChanArrayDims == 0 {\n```\n\nAt line 1959, update protocol registration:\n```go\nif p.IsChan || p.ChanArrayDims > 0 {\n```\n\n**3.14 collectChanProtocols() (line 1337)**\n\nAt line 1356, update channel param check:\n```go\nif p.IsChan || p.ChanArrayDims > 0 {\n```\n\n**3.15 containsXxx scanner functions**\n\nThese walk the AST to check import needs. `ChanDecl` checks like `containsPar` walk through `*ast.ChanDecl` and already handle it as a statement type. No changes needed for the scanner logic itself, since the AST node types remain the same.\n\nHowever, any scanner that inspects `ChanDecl.IsArray` will need to change to `len(s.Sizes) > 0`. Search for all references to `IsArray`, `ChannelIndex`, `IsChanArray`, `IsOpenArray`, `Index` (on Assignment/MultiAssignTarget), and `Size` (on ArrayDecl/ChanDecl) across the codebase.\n\n---\n\n### Phase 4: Systematic Find-and-Replace for Field Renames\n\nAfter the structural changes above, a search-and-replace pass is needed across all files that reference the old field names. Key patterns:\n\n| Old field/check | New field/check |\n|---|---|\n| `decl.Size` (ArrayDecl) | `decl.Sizes[0]` (single-dim) or loop over `decl.Sizes` |\n| `decl.IsArray` (ChanDecl) | `len(decl.Sizes) > 0` |\n| `decl.Size` (ChanDecl) | `decl.Sizes[0]` (single-dim) or loop |\n| `send.ChannelIndex` | `send.ChannelIndices` |\n| `recv.ChannelIndex` | `recv.ChannelIndices` |\n| `vr.ChannelIndex` | `vr.ChannelIndices` |\n| `altCase.ChannelIndex` | `altCase.ChannelIndices` |\n| `assign.Index` | `assign.Indices` |\n| `target.Index` | `target.Indices` |\n| `p.IsChanArray` | `p.ChanArrayDims > 0` |\n| `p.IsOpenArray` | `p.OpenArrayDims > 0` |\n\nAll existing single-dimension tests continue to pass because the slices simply have length 1.\n\n---\n\n### Phase 5: Test Strategy\n\n**5.1 Parser unit tests** (`parser/parser_test.go`)\n\nAdd tests:\n\n- `TestMultiDimArrayDecl`: Parse `[5][3]INT arr:`, assert `Sizes` has 2 entries (5, 3).\n- `TestMultiDimChanArrayDecl`: Parse `[4][3]CHAN OF INT cs:`, assert `Sizes` has 2 entries.\n- `TestThreeDimChanArrayDecl`: Parse `[w][h][n]CHAN OF STATE link:` (with abbreviations for w, h, n).\n- `TestMultiDimIndexedSend`: Parse `cs[x][y] ! 42`, assert `ChannelIndices` has 2 entries.\n- `TestMultiDimIndexedReceive`: Parse `cs[x][y] ? v`, assert `ChannelIndices` has 2 entries.\n- `TestMultiDimIndexedAssignment`: Parse `arr[i][j] := 42`, assert `Indices` has 2 entries.\n- `TestMultiDimOpenArrayParam`: Parse `PROC f([][]INT arr)`, assert `OpenArrayDims == 2`.\n- `TestMultiDimChanArrayParam`: Parse `PROC f([][][]CHAN OF INT c)`, assert `ChanArrayDims == 3`.\n- `TestMultiDimAltCase`: Parse ALT with `cs[x][y] ? v`.\n\nAlso update existing single-dim tests that reference `decl.Size` to use `decl.Sizes[0]`.\n\n**5.2 Codegen unit tests** (`codegen/codegen_test.go`)\n\nAdd tests:\n\n- `TestMultiDimArrayDeclCodegen`: Input `[3][2]INT arr:`, check output contains nested `make` and init loop.\n- `TestMultiDimChanDeclCodegen`: Input `[3][2]CHAN OF INT cs:`, check output contains nested init.\n- `TestMultiDimSendCodegen`: Input `cs[0][1] ! 42`, check output `cs[0][1] <- 42`.\n- `TestMultiDimReceiveCodegen`: Input `cs[0][1] ? x`, check output `x = <-cs[0][1]`.\n- `TestMultiDimProcParamCodegen`: Input proc with `[][]CHAN OF INT`, check Go signature `[][]chan int`.\n\n**5.3 E2E tests** (`codegen/e2e_array_test.go`)\n\nAdd tests:\n\n- `TestE2E_MultiDimArray`: Declare `[3][2]INT`, fill with nested SEQ loops, read back and print. Verify output.\n- `TestE2E_MultiDimChanArray`: Declare `[2][3]CHAN OF INT`, use in PAR with indexed send/receive, verify values.\n- `TestE2E_MultiDimChanArrayWithProc`: Pass `[][]CHAN OF INT` to a proc, send/receive through it.\n- `TestE2E_ThreeDimChanArray`: A simplified version of the life pattern with 3-dim channel arrays.\n- `TestE2E_MultiDimAssignment`: `arr[i][j] := val` and read back.\n\n**5.4 Regression**: Run `go test ./...` to ensure all existing tests pass after the field renames.\n\n---\n\n### Phase 6: Implementation Order\n\n1. **AST changes** -- modify `ast/ast.go` (all field renames at once)\n2. **Fix compilation errors** -- immediately update all references in parser, codegen, and tests to use the new field names (this is mechanical)\n3. **Parser multi-dim loops** -- add the `while peekTokenIs(LBRACKET)` loops in `parseArrayDecl`, `parseIndexedOperation`, `parseProcParams`, `parseAltCase`, `parseMultiAssignmentFrom`\n4. **Codegen multi-dim generation** -- add `generateIndices` helper, nested init loops for `generateArrayDecl` and `generateChanDecl`, update all consumers to loop over slices\n5. **Tests** -- add new parser, codegen, and e2e tests\n6. **Verify** -- run `go test ./...` to confirm all tests pass\n7. **Update documentation** -- update `CLAUDE.md` and `TODO.md`\n\n---\n\n### Potential Challenges\n\n1. **Record field access disambiguation**: Currently `assign.Index` being an `*ast.Identifier` means record field access. With `Indices`, we check `len(Indices) == 1 && Indices[0] is Identifier`. This is safe because record fields never have multi-dim access.\n\n2. **Guard handling in ALT**: The guarded ALT case with indexed channels (line 1770-1781) sets a `_altN` variable to `nil` or the channel. For multi-dim indexed channels with guards, the pattern `_altN = cs[x][y]` works fine since the indices are evaluated at the point of guard setup.\n\n3. **Replicated ALT with multi-dim**: The `reflect.Select` builder at line 1872 builds `reflect.ValueOf(cs[i])`. For multi-dim this becomes `reflect.ValueOf(cs[x][y])` -- just needs to loop over indices.\n\n4. **Shared-type params**: When copying type info from `prevParam` for shared-type params (line 2029), the new `ChanArrayDims` and `OpenArrayDims` must be copied in addition to other fields.\n\n5. **String-to-byte-slice wrapping**: The check `params[i].IsOpenArray && params[i].Type == \"BYTE\"` in `generateProcCall` (line 2107) and `generateAbbreviation` (line 1103) needs to use `params[i].OpenArrayDims > 0`.\n\n---\n\n### Critical Files for Implementation\n- `/home/david/projects/code-associates/occam2go/ast/ast.go` - All AST struct definitions that need field changes (ArrayDecl, ChanDecl, Send, Receive, AltCase, VariantReceive, Assignment, MultiAssignTarget, ProcParam)\n- `/home/david/projects/code-associates/occam2go/parser/parser.go` - Parser functions that need multi-dim loops: parseArrayDecl, parseIndexedOperation, parseProcParams, parseAltCase, parseMultiAssignmentFrom\n- `/home/david/projects/code-associates/occam2go/codegen/codegen.go` - Code generator functions for nested make/init loops and multi-index emission: generateArrayDecl, generateChanDecl, generateSend, generateReceive, generateAssignment, generateProcParams, generateAltBlock, generateProcCall, generateProcDecl\n- `/home/david/projects/code-associates/occam2go/codegen/e2e_array_test.go` - Existing array E2E tests to update for field renames and new multi-dim E2E tests to add\n- `/home/david/projects/code-associates/occam2go/parser/parser_test.go` - Existing parser tests to update for field renames and new multi-dim parser tests to add"
},
{
"type": "text",
"text": "agentId: a4ad8954441baad9d (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 92184\ntool_uses: 53\nduration_ms: 269795</usage>"
}
]Excellent plan. Let me now verify a few details and write the final plan. Let me also check the parseVariantReceiveWithIndex and other places where ChannelIndex is set in the parser.
{
"pattern": "parseVariantReceiveWithIndex",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true,
"-A": 15
}755: return p.parseVariantReceiveWithIndex(name, index, recvToken)
756- }
757-
758- stmt := &ast.Receive{
759- Token: recvToken,
760- Channel: name,
761- ChannelIndex: index,
762- }
763-
764- if !p.expectPeek(lexer.IDENT) {
765- return nil
766- }
767- stmt.Variable = p.curToken.Literal
768-
769- // Check for sequential receive
770- for p.peekTokenIs(lexer.SEMICOLON) {
--
1384:func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndex ast.Expression, token lexer.Token) *ast.VariantReceive {
1385- stmt := &ast.VariantReceive{
1386- Token: token,
1387- Channel: channel,
1388- ChannelIndex: channelIndex,
1389- }
1390-
1391- // Skip to next line
1392- for p.peekTokenIs(lexer.NEWLINE) {
1393- p.nextToken()
1394- }
1395-
1396- // Expect INDENT
1397- if !p.peekTokenIs(lexer.INDENT) {
1398- p.addError("expected indented block after ? CASE")
1399- return stmt{
"pattern": "parseMultiAssignmentFrom",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true,
"-A": 40
}487: return p.parseMultiAssignmentFrom(firstTarget)
488-}
489-
490:// parseMultiAssignmentFrom parses a multi-assignment given the first target already parsed.
491-// The current token should be on the first target's last token (ident or ']').
492-// Peek token should be COMMA.
493:func (p *Parser) parseMultiAssignmentFrom(firstTarget ast.MultiAssignTarget) *ast.MultiAssignment {
494- stmt := &ast.MultiAssignment{
495- Targets: []ast.MultiAssignTarget{firstTarget},
496- }
497-
498- / Parse comma-separated targets: a, b[i], c
499- for p.peekTokenIs(lexer.COMMA) {
500- p.nextToken() // consume comma
501- p.nextToken() // move to next target
502- target := ast.MultiAssignTarget{Name: p.curToken.Literal}
503- if p.peekTokenIs(lexer.LBRACKET) {
504- p.nextToken() // move to [
505- p.nextToken() // move past [
506- target.Index = p.parseExpression(LOWEST)
507- if !p.expectPeek(lexer.RBRACKET) {
508- return nil
509- }
510- }
511- stmt.Targets = append(stmt.Targets, target)
512- }
513-
514- p.nextToken() / move to :=
515- stmt.Token = p.curToken
516-
517- p.nextToken() / move past :=
518-
519- // Parse comma-separated values
520- stmt.Values = []ast.Expression{p.parseExpression(LOWEST)}
521- for p.peekTokenIs(lexer.COMMA) {
522- p.nextToken() // consume comma
523- p.nextToken() // move to next expression
524- stmt.Values = append(stmt.Values, p.parseExpression(LOWEST))
525- }
526-
527- return stmt
528-}
529-
530-func (p *Parser) parseArrayDecl() ast.Statement {
531- lbracketToken := p.curToken
532-
533- // Parse size expression after [
--
692: return p.parseMultiAssignmentFrom(firstTarget)
693- }
694- if p.peekTokenIs(lexer.ASSIGN) {
695- / Indexed assignment: name[index] := value
696- p.nextToken() / move to :=
697- stmt := &ast.Assignment{
698- Name: name,
699- Token: p.curToken,
700- Index: index,
701- }
702- p.nextToken() / move past :=
703- stmt.Value = p.parseExpression(LOWEST)
704- return stmt
705- }
706-
707- if p.peekTokenIs(lexer.SEND) {
708- / Indexed channel send: cs[i] ! value
709- p.nextToken() // move to !
710- sendToken := p.curToken
711- p.nextToken() // move past !
712-
713- stmt := &ast.Send{
714- Token: sendToken,
715- Channel: name,
716- ChannelIndex: index,
717- }
718-
719- / Check if this is a variant send: first token is an identifier that is a variant tag
720- if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.SEMICOLON) {
721- possibleTag := p.curToken.Literal
722- if p.isVariantTag(possibleTag) {
723- stmt.VariantTag = possibleTag
724- p.nextToken() // move to ;
725- for p.curTokenIs(lexer.SEMICOLON) {
726- p.nextToken() // move past ;
727- val := p.parseExpression(LOWEST)
728- stmt.Values = append(stmt.Values, val)
729- }
730- return stmt
731- }
732- }Let me also check all the places in codegen that reference IsChanArray, IsOpenArray, and IsArray to make sure the plan covers everything.
{
"pattern": "IsChanArray|IsOpenArray|\\.IsArray",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true,
"-C": 2
}280- } else {
281- goType := g.occamTypeToGo(abbr.Type)
282: if abbr.IsOpenArray {
283- goType = "[]" + goType
284- }
--
286- g.write(fmt.Sprintf("%s %s = ", goIdent(abbr.Name), goType))
287- // Wrap string literals with []byte() when assigned to []byte variables
288: if _, isStr := abbr.Value.(*ast.StringLiteral); isStr && abbr.IsOpenArray && abbr.Type == "BYTE" {
289- g.write("[]byte(")
290- g.generateExpression(abbr.Value)
--
1093- if abbr.Type != "" {
1094- goType := g.occamTypeToGo(abbr.Type)
1095: if abbr.IsOpenArray {
1096- goType = "[]" + goType
1097- }
--
1101- }
1102- // Wrap string literals with []byte() when assigned to []byte variables
1103: if _, isStr := abbr.Value.(*ast.StringLiteral); isStr && abbr.IsOpenArray && abbr.Type == "BYTE" {
1104- g.write("[]byte(")
1105- g.generateExpression(abbr.Value)
--
1117-func (g *Generator) generateChanDecl(decl *ast.ChanDecl) {
1118- goType := g.occamTypeToGo(decl.ElemType)
1119: if decl.IsArray {
1120- for _, name := range decl.Names {
1121- n := goIdent(name)
--
1354- // Register PROC param channels (including channel array params)
1355- for _, p := range s.Params {
1356: if p.IsChan || p.IsChanArray {
1357- if _, ok := g.protocolDefs[p.ChanElemType]; ok {
1358- g.chanProtocols[p.Name] = p.ChanElemType
--
1944- }
1945- for _, p := range proc.Params {
1946: if !p.IsVal && !p.IsChan && !p.IsChanArray && !p.IsOpenArray && p.ArraySize == "" {
1947- newRefParams[p.Name] = true
1948- } else {
--
1951- }
1952- // Track BOOL params; delete non-BOOL params that shadow inherited names
1953: if p.Type == "BOOL" && !p.IsChan && !p.IsChanArray {
1954- newBoolVars[p.Name] = true
1955- } else {
--
1957- }
1958- // Register chan params with protocol mappings
1959: if p.IsChan || p.IsChanArray {
1960- if _, ok := g.protocolDefs[p.ChanElemType]; ok {
1961- g.chanProtocols[p.Name] = p.ChanElemType
--
2038- for _, p := range params {
2039- var goType string
2040: if p.IsChanArray {
2041- goType = "[]chan " + g.occamTypeToGo(p.ChanElemType)
2042- } else if p.IsChan {
2043- goType = chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)
2044: } else if p.IsOpenArray {
2045- goType = "[]" + g.occamTypeToGo(p.Type)
2046- } else if p.ArraySize != "" {
--
2101- // If this parameter is not VAL (i.e., pass by reference), take address
2102- // Channels, channel arrays, open arrays, and fixed-size arrays (mapped to slices) are already reference types
2103: if i < len(params) && !params[i].IsVal && !params[i].IsChan && !params[i].IsChanArray && !params[i].IsOpenArray && params[i].ArraySize == "" {
2104- g.write("&")
2105- }
2106- // Wrap string literals with []byte() when passed to []BYTE parameters
2107: if _, isStr := arg.(*ast.StringLiteral); isStr && i < len(params) && params[i].IsOpenArray && params[i].Type == "BYTE" {
2108- g.write("[]byte(")
2109- g.generateExpression(arg)
--
2141- }
2142- for _, p := range fn.Params {
2143: if p.Type == "BOOL" && !p.IsChan && !p.IsChanArray {
2144- newBoolVars[p.Name] = true
2145- } else {
--
2197- }
2198- // Wrap string literals with []byte() when passed to []BYTE parameters
2199: if _, isStr := arg.(*ast.StringLiteral); isStr && i < len(params) && params[i].IsOpenArray && params[i].Type == "BYTE" {
2200- g.write("[]byte(")
2201- g.generateExpression(arg)
--
2669- gSource = renamed
2670- }
2671: if r.IsArray {
2672- / VAL [2]INT X RETYPES X : — split float64 into two int32 words
2673- tmpVar := fmt.Sprintf("_retmp%d", g.tmpCounter){
"pattern": "\\.Index[^eE]",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}802: return g.exprNeedsMath(s.Value) || g.exprNeedsMath(s.Index)
805: if g.exprNeedsMath(t.Index) {
936: return g.exprNeedsMath(e.Left) || g.exprNeedsMath(e.Index)
1602: if assign.Index != nil {
1605: if ident, ok := assign.Index.(*ast.Identifier); ok {
1622: g.generateExpression(assign.Index)
2216: if target.Index != nil {
2219: if ident, ok := target.Index.(*ast.Identifier); ok {
2234: g.generateExpression(target.Index)
2525: if field, ok := e.Index.(*ast.Identifier); ok {
2535: g.generateExpression(e.Index)
2871: return g.walkExpr(s.Value, fn) || g.walkExpr(s.Index, fn)
2996: return g.walkExpr(e.Left, fn) || g.walkExpr(e.Index, fn){
"pattern": "IsChanArray|IsOpenArray|\\.IsArray",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true
}420: IsOpenArray: isOpenArray,
2034: param.IsChanArray = prevParam.IsChanArray
2035: param.IsOpenArray = prevParam.IsOpenArray
2041: if (param.IsChan || param.IsChanArray) && (p.peekTokenIs(lexer.RECEIVE) || p.peekTokenIs(lexer.SEND)) {
2078: param.IsChanArray = true
2091: param.IsOpenArray = true
2095: param.IsOpenArray = true
2160: if (param.IsChan || param.IsChanArray) && (p.peekTokenIs(lexer.RECEIVE) || p.peekTokenIs(lexer.SEND)) {{
"pattern": "IsOpenArray",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-n": true
}150: IsOpenArray bool // true for []TYPE params (open array) 494: IsOpenArray bool // true for []TYPE abbreviations (e.g. VAL []BYTE)
OK so Abbreviation.IsOpenArray is separate from ProcParam.IsOpenArray. The Abbreviation one only handles single-dim (VAL []BYTE s IS "hi":), and that doesn't need multi-dim support for the life.occ use case. I'll leave Abbreviation.IsOpenArray unchanged.
Now I also need to check the exprNeedsMath and walkExpr functions that reference .Index to see if those need changes too.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 798,
"limit": 12
} 798→
799→func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
800→ switch s := stmt.(type) {
801→ case *ast.Assignment:
802→ return g.exprNeedsMath(s.Value) || g.exprNeedsMath(s.Index)
803→ case *ast.MultiAssignment:
804→ for _, t := range s.Targets {
805→ if g.exprNeedsMath(t.Index) {
806→ return true
807→ }
808→ }
809→ for _, v := range s.Values {{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 868,
"limit": 8
} 868→ }
869→ for _, choice := range s.Choices {
870→ for _, v := range choice.Values {
871→ if g.exprNeedsMath(v) {
872→ return true
873→ }
874→ }
875→ for _, inner := range choice.Body {{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 2865,
"limit": 15
} 2865→
2866→// walkStatements recursively walks a statement tree, applying fn to all expressions.
2867→// Returns true if fn returns true for any expression.
2868→func (g *Generator) walkStatements(stmt ast.Statement, fn func(ast.Expression) bool) bool {
2869→ switch s := stmt.(type) {
2870→ case *ast.Assignment:
2871→ return g.walkExpr(s.Value, fn) || g.walkExpr(s.Index, fn)
2872→ case *ast.MultiAssignment:
2873→ for _, v := range s.Values {
2874→ if g.walkExpr(v, fn) {
2875→ return true
2876→ }
2877→ }
2878→ case *ast.Abbreviation:
2879→ return g.walkExpr(s.Value, fn){
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 2990,
"limit": 10
} 2990→ return g.walkExpr(e.Expr, fn)
2991→ case *ast.TypeConversion:
2992→ return g.walkExpr(e.Expr, fn)
2993→ case *ast.SizeExpr:
2994→ return g.walkExpr(e.Expr, fn)
2995→ case *ast.IndexExpr:
2996→ return g.walkExpr(e.Left, fn) || g.walkExpr(e.Index, fn)
2997→ case *ast.FuncCall:
2998→ for _, arg := range e.Args {
2999→ if g.walkExpr(arg, fn) {Good — IndexExpr.Index (line 2996) is part of the expression AST, not the assignment field, so that stays. But s.Index at line 2871 and t.Index at line 805 are Assignment.Index and MultiAssignTarget.Index respectively, which need to change.
Now I have everything I need. Let me write the final plan.
# Plan: Multi-Dimensional Array Support (Issue #74)
## Context
The transpiler only supports single-dimensional arrays. The Life example (`historical-examples/life.occ`) requires multi-dimensional arrays for its cell grid and channel network. This is the single most pervasive error source in that file, appearing at 15+ lines. The fix requires changes across AST, parser, codegen, and tests.
## Files to Modify
- `ast/ast.go` — struct field changes
- `parser/parser.go` — multi-dim parsing loops
- `codegen/codegen.go` — nested init loops, multi-index emission
- `parser/parser_test.go` — update existing + add new tests
- `codegen/codegen_test.go` — update existing + add new tests
- `codegen/e2e_array_test.go` — add multi-dim e2e tests
- `CLAUDE.md` / `TODO.md` — documentation updates
## Step 1: AST Changes (`ast/ast.go`)
Rename single-value fields to plural slices. Existing single-dim code uses `len(Xs) == 1`.
| Struct | Old Field | New Field |
|--------|-----------|-----------|
| `ArrayDecl` | `Size Expression` | `Sizes []Expression` |
| `ChanDecl` | `IsArray bool` + `Size Expression` | `Sizes []Expression` (remove `IsArray`; `len(Sizes) > 0` = array) |
| `Send` | `ChannelIndex Expression` | `ChannelIndices []Expression` |
| `Receive` | `ChannelIndex Expression` | `ChannelIndices []Expression` |
| `AltCase` | `ChannelIndex Expression` | `ChannelIndices []Expression` |
| `VariantReceive` | `ChannelIndex Expression` | `ChannelIndices []Expression` |
| `Assignment` | `Index Expression` | `Indices []Expression` |
| `MultiAssignTarget` | `Index Expression` | `Indices []Expression` |
| `ProcParam` | `IsChanArray bool` + `IsOpenArray bool` | `ChanArrayDims int` + `OpenArrayDims int` (0=not array, 1=[], 2=[][], etc.) |
**Not changed**: `Abbreviation.IsOpenArray` (only single-dim `VAL []BYTE` needed), `IndexExpr.Index` (expression-level chaining already works).
## Step 2: Fix All Compilation Errors (Mechanical Renames)
After AST changes, update every reference across parser, codegen, and tests:
| Old Pattern | New Pattern |
|-------------|-------------|
| `decl.Size` (ArrayDecl) | `decl.Sizes[0]` or loop `decl.Sizes` |
| `decl.IsArray` / `decl.Size` (ChanDecl) | `len(decl.Sizes) > 0` / `decl.Sizes` |
| `send.ChannelIndex` | `send.ChannelIndices` |
| `recv.ChannelIndex` | `recv.ChannelIndices` |
| `vr.ChannelIndex` | `vr.ChannelIndices` |
| `c.ChannelIndex` (AltCase) | `c.ChannelIndices` |
| `assign.Index` | `assign.Indices` |
| `target.Index` | `target.Indices` |
| `p.IsChanArray` | `p.ChanArrayDims > 0` |
| `p.IsOpenArray` | `p.OpenArrayDims > 0` |
Key locations in **codegen.go** to update:
- `containsMostExpr()` (line 802): `s.Index` → loop `s.Indices`
- `containsMostExpr()` (line 805): `t.Index` → loop `t.Indices`
- `walkStatements()` (line 2871): `s.Index` → loop `s.Indices`
- `generateChanDecl()` (line 1119): `decl.IsArray` → `len(decl.Sizes) > 0`
- `generateSend()` (line 1160): single index → loop
- `generateReceive()` (line 1206): single index → loop
- `generateVariantReceive()` (line 1297): single index → loop
- `generateAssignment()` (line 1602): single index → loop
- `generateMultiAssignment()` (line 2216): single index → loop
- `generateProcParams()` (line 2040): `IsChanArray` → `ChanArrayDims`
- `generateProcCall()` (line 2103): `IsChanArray`/`IsOpenArray` → dims > 0
- `generateProcDecl()` (line 1946): `IsChanArray`/`IsOpenArray` → dims > 0
- `collectChanProtocols()` (line 1356): `IsChanArray` → `ChanArrayDims > 0`
- ALT codegen (lines 1793, 1873): single index → loop
- `generateFuncDecl()` (line 2143): `IsChanArray` → `ChanArrayDims > 0`
- `generateFuncCallExpr()` (line 2199): `IsOpenArray` → `OpenArrayDims > 0`
- `generateRetypesDecl()` (line 2671): `r.IsArray` → `len(r.Sizes) > 0` — wait, this is RetypesDecl, keep as-is
Key locations in **parser.go** to update:
- `parseProcParams()` shared-type copy (line 2034-2035): copy `ChanArrayDims`/`OpenArrayDims`
- `parseProcParams()` direction check (lines 2041, 2160): `IsChanArray` → `ChanArrayDims > 0`
- `parseAbbreviation()` (line 420): `IsOpenArray` stays (on Abbreviation, not ProcParam)
- All `ChannelIndex:` assignments become `ChannelIndices: []Expression{...}` or append
## Step 3: Parser Multi-Dim Loops
### 3a. `parseArrayDecl()` (line 530)
After parsing first `[size]` and `]`, loop while `peekTokenIs(LBRACKET)` to collect additional sizes:
```
sizes := []Expression{firstSize}
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken(); p.nextToken() // move past [
size := p.parseExpression(LOWEST)
p.expectPeek(lexer.RBRACKET)
sizes = append(sizes, size)
}
```
Then create `ChanDecl{Sizes: sizes}` or `ArrayDecl{Sizes: sizes}`.
### 3b. `parseIndexedOperation()` (line 676)
After parsing first `name[index]`, loop while `peekTokenIs(LBRACKET)`:
```
indices := []Expression{index}
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken(); p.nextToken()
idx := p.parseExpression(LOWEST)
p.expectPeek(lexer.RBRACKET)
indices = append(indices, idx)
}
```
Then use `Indices: indices` / `ChannelIndices: indices` in all branches.
### 3c. `parseProcParams()` (line 2070)
Count consecutive `[]` pairs for open array params:
```
dims := 0
for p.curTokenIs(LBRACKET) && p.peekTokenIs(RBRACKET) {
dims++
p.nextToken(); p.nextToken() // consume []
}
```
Then set `param.ChanArrayDims = dims` or `param.OpenArrayDims = dims`.
### 3d. `parseAltCase()` (lines 1733, 1773)
After parsing channel name, loop while `peekTokenIs(LBRACKET)`:
```
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken(); p.nextToken()
idx := p.parseExpression(LOWEST)
p.expectPeek(lexer.RBRACKET)
altCase.ChannelIndices = append(altCase.ChannelIndices, idx)
}
```
### 3e. `parseMultiAssignmentFrom()` (line 503)
When parsing subsequent targets with `[`, loop for multiple indices.
### 3f. `parseVariantReceiveWithIndex()` (line 1384)
Signature changes from `(channel string, channelIndex Expression, ...)` to `(channel string, channelIndices []Expression, ...)`.
## Step 4: Codegen Multi-Dim Generation
### 4a. Add `generateIndices()` helper
```go
func (g *Generator) generateIndices(indices []ast.Expression) {
for _, idx := range indices {
g.write("["); g.generateExpression(idx); g.write("]")
}
}
```
Use this in `generateSend`, `generateAssignment`, `generateMultiAssignment`, ALT codegen.
### 4b. `generateArrayDecl()` — nested make + init loops
For `[5][3]INT arr:` (Sizes=[5,3]):
```go
arr := make([][]int, 5)
for _i0 := range arr { arr[_i0] = make([]int, 3) }
```
Implement as recursive nested loop generator for arbitrary depth.
### 4c. `generateChanDecl()` — nested make + init loops + innermost channel init
For `[w][h][n]CHAN OF STATE link:` (Sizes=[w,h,n]):
```go
link := make([][][]chan bool, w)
for _i0 := range link {
link[_i0] = make([][]chan bool, h)
for _i1 := range link[_i0] {
link[_i0][_i1] = make([]chan bool, n)
for _i2 := range link[_i0][_i1] { link[_i0][_i1][_i2] = make(chan bool) }
}
}
```
### 4d. `generateProcParams()` — multi-dim type strings
```go
if p.ChanArrayDims > 0 {
goType = strings.Repeat("[]", p.ChanArrayDims) + "chan " + ...
} else if p.OpenArrayDims > 0 {
goType = strings.Repeat("[]", p.OpenArrayDims) + ...
}
```
### 4e. Update receive/variant-receive codegen
Loop over `ChannelIndices` building `chanRef` string with nested `[idx]`.
## Step 5: Tests
### Parser tests (`parser/parser_test.go`)
- Update existing tests: `decl.Size` → `decl.Sizes[0]`, etc.
- Add: `TestMultiDimArrayDecl`, `TestMultiDimChanArrayDecl`, `TestMultiDimIndexedSend`, `TestMultiDimIndexedReceive`, `TestMultiDimIndexedAssignment`, `TestMultiDimOpenArrayParam`, `TestMultiDimChanArrayParam`
### Codegen unit tests (`codegen/codegen_test.go`)
- Update existing tests referencing old field names
- Add: `TestMultiDimArrayDeclCodegen`, `TestMultiDimChanDeclCodegen`, `TestMultiDimSendCodegen`, `TestMultiDimReceiveCodegen`, `TestMultiDimProcParamCodegen`
### E2E tests (`codegen/e2e_array_test.go`)
- Add: `TestE2E_MultiDimArray` (2D array, fill + read), `TestE2E_MultiDimChanArray` (2D channel array with send/receive), `TestE2E_MultiDimChanArrayWithProc` (pass `[][]CHAN` to proc)
## Step 6: Documentation
- Update `CLAUDE.md` mapping table to show multi-dim arrays
- Update `TODO.md` to mark multi-dim arrays as implemented
## Verification
```bash
go build ./... # must compile
go test ./... # all tests pass (existing + new)
# Smoke test with life.occ patterns:
./occam2go -I examples -o /tmp/multidim_test.go examples/multidim_test.occ # if we create a test file
```
{
"allowedPrompts": [
{
"tool": "Bash",
"prompt": "run tests"
},
{
"tool": "Bash",
"prompt": "build project"
},
{
"tool": "Bash",
"prompt": "run transpiler"
}
],
"plan": "# Plan: Multi-Dimensional Array Support (Issue #74)\n\n## Context\n\nThe transpiler only supports single-dimensional arrays. The Life example (`historical-examples/life.occ`) requires multi-dimensional arrays for its cell grid and channel network. This is the single most pervasive error source in that file, appearing at 15+ lines. The fix requires changes across AST, parser, codegen, and tests.\n\n## Files to Modify\n\n- `ast/ast.go` — struct field changes\n- `parser/parser.go` — multi-dim parsing loops\n- `codegen/codegen.go` — nested init loops, multi-index emission\n- `parser/parser_test.go` — update existing + add new tests\n- `codegen/codegen_test.go` — update existing + add new tests\n- `codegen/e2e_array_test.go` — add multi-dim e2e tests\n- `CLAUDE.md` / `TODO.md` — documentation updates\n\n## Step 1: AST Changes (`ast/ast.go`)\n\nRename single-value fields to plural slices. Existing single-dim code uses `len(Xs) == 1`.\n\n| Struct | Old Field | New Field |\n|--------|-----------|-----------|\n| `ArrayDecl` | `Size Expression` | `Sizes []Expression` |\n| `ChanDecl` | `IsArray bool` + `Size Expression` | `Sizes []Expression` (remove `IsArray`; `len(Sizes) > 0` = array) |\n| `Send` | `ChannelIndex Expression` | `ChannelIndices []Expression` |\n| `Receive` | `ChannelIndex Expression` | `ChannelIndices []Expression` |\n| `AltCase` | `ChannelIndex Expression` | `ChannelIndices []Expression` |\n| `VariantReceive` | `ChannelIndex Expression` | `ChannelIndices []Expression` |\n| `Assignment` | `Index Expression` | `Indices []Expression` |\n| `MultiAssignTarget` | `Index Expression` | `Indices []Expression` |\n| `ProcParam` | `IsChanArray bool` + `IsOpenArray bool` | `ChanArrayDims int` + `OpenArrayDims int` (0=not array, 1=[], 2=[][], etc.) |\n\n**Not changed**: `Abbreviation.IsOpenArray` (only single-dim `VAL []BYTE` needed), `IndexExpr.Index` (expression-level chaining already works).\n\n## Step 2: Fix All Compilation Errors (Mechanical Renames)\n\nAfter AST changes, update every reference across parser, codegen, and tests:\n\n| Old Pattern | New Pattern |\n|-------------|-------------|\n| `decl.Size` (ArrayDecl) | `decl.Sizes[0]` or loop `decl.Sizes` |\n| `decl.IsArray` / `decl.Size` (ChanDecl) | `len(decl.Sizes) > 0` / `decl.Sizes` |\n| `send.ChannelIndex` | `send.ChannelIndices` |\n| `recv.ChannelIndex` | `recv.ChannelIndices` |\n| `vr.ChannelIndex` | `vr.ChannelIndices` |\n| `c.ChannelIndex` (AltCase) | `c.ChannelIndices` |\n| `assign.Index` | `assign.Indices` |\n| `target.Index` | `target.Indices` |\n| `p.IsChanArray` | `p.ChanArrayDims > 0` |\n| `p.IsOpenArray` | `p.OpenArrayDims > 0` |\n\nKey locations in **codegen.go** to update:\n- `containsMostExpr()` (line 802): `s.Index` → loop `s.Indices`\n- `containsMostExpr()` (line 805): `t.Index` → loop `t.Indices`\n- `walkStatements()` (line 2871): `s.Index` → loop `s.Indices`\n- `generateChanDecl()` (line 1119): `decl.IsArray` → `len(decl.Sizes) > 0`\n- `generateSend()` (line 1160): single index → loop\n- `generateReceive()` (line 1206): single index → loop\n- `generateVariantReceive()` (line 1297): single index → loop\n- `generateAssignment()` (line 1602): single index → loop\n- `generateMultiAssignment()` (line 2216): single index → loop\n- `generateProcParams()` (line 2040): `IsChanArray` → `ChanArrayDims`\n- `generateProcCall()` (line 2103): `IsChanArray`/`IsOpenArray` → dims > 0\n- `generateProcDecl()` (line 1946): `IsChanArray`/`IsOpenArray` → dims > 0\n- `collectChanProtocols()` (line 1356): `IsChanArray` → `ChanArrayDims > 0`\n- ALT codegen (lines 1793, 1873): single index → loop\n- `generateFuncDecl()` (line 2143): `IsChanArray` → `ChanArrayDims > 0`\n- `generateFuncCallExpr()` (line 2199): `IsOpenArray` → `OpenArrayDims > 0`\n- `generateRetypesDecl()` (line 2671): `r.IsArray` → `len(r.Sizes) > 0` — wait, this is RetypesDecl, keep as-is\n\nKey locations in **parser.go** to update:\n- `parseProcParams()` shared-type copy (line 2034-2035): copy `ChanArrayDims`/`OpenArrayDims`\n- `parseProcParams()` direction check (lines 2041, 2160): `IsChanArray` → `ChanArrayDims > 0`\n- `parseAbbreviation()` (line 420): `IsOpenArray` stays (on Abbreviation, not ProcParam)\n- All `ChannelIndex:` assignments become `ChannelIndices: []Expression{...}` or append\n\n## Step 3: Parser Multi-Dim Loops\n\n### 3a. `parseArrayDecl()` (line 530)\nAfter parsing first `[size]` and `]`, loop while `peekTokenIs(LBRACKET)` to collect additional sizes:\n```\nsizes := []Expression{firstSize}\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken(); p.nextToken() // move past [\n size := p.parseExpression(LOWEST)\n p.expectPeek(lexer.RBRACKET)\n sizes = append(sizes, size)\n}\n```\nThen create `ChanDecl{Sizes: sizes}` or `ArrayDecl{Sizes: sizes}`.\n\n### 3b. `parseIndexedOperation()` (line 676)\nAfter parsing first `name[index]`, loop while `peekTokenIs(LBRACKET)`:\n```\nindices := []Expression{index}\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken(); p.nextToken()\n idx := p.parseExpression(LOWEST)\n p.expectPeek(lexer.RBRACKET)\n indices = append(indices, idx)\n}\n```\nThen use `Indices: indices` / `ChannelIndices: indices` in all branches.\n\n### 3c. `parseProcParams()` (line 2070)\nCount consecutive `[]` pairs for open array params:\n```\ndims := 0\nfor p.curTokenIs(LBRACKET) && p.peekTokenIs(RBRACKET) {\n dims++\n p.nextToken(); p.nextToken() // consume []\n}\n```\nThen set `param.ChanArrayDims = dims` or `param.OpenArrayDims = dims`.\n\n### 3d. `parseAltCase()` (lines 1733, 1773)\nAfter parsing channel name, loop while `peekTokenIs(LBRACKET)`:\n```\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken(); p.nextToken()\n idx := p.parseExpression(LOWEST)\n p.expectPeek(lexer.RBRACKET)\n altCase.ChannelIndices = append(altCase.ChannelIndices, idx)\n}\n```\n\n### 3e. `parseMultiAssignmentFrom()` (line 503)\nWhen parsing subsequent targets with `[`, loop for multiple indices.\n\n### 3f. `parseVariantReceiveWithIndex()` (line 1384)\nSignature changes from `(channel string, channelIndex Expression, ...)` to `(channel string, channelIndices []Expression, ...)`.\n\n## Step 4: Codegen Multi-Dim Generation\n\n### 4a. Add `generateIndices()` helper\n```go\nfunc (g *Generator) generateIndices(indices []ast.Expression) {\n for _, idx := range indices {\n g.write(\"[\"); g.generateExpression(idx); g.write(\"]\")\n }\n}\n```\nUse this in `generateSend`, `generateAssignment`, `generateMultiAssignment`, ALT codegen.\n\n### 4b. `generateArrayDecl()` — nested make + init loops\nFor `[5][3]INT arr:` (Sizes=[5,3]):\n```go\narr := make([][]int, 5)\nfor _i0 := range arr { arr[_i0] = make([]int, 3) }\n```\nImplement as recursive nested loop generator for arbitrary depth.\n\n### 4c. `generateChanDecl()` — nested make + init loops + innermost channel init\nFor `[w][h][n]CHAN OF STATE link:` (Sizes=[w,h,n]):\n```go\nlink := make([][][]chan bool, w)\nfor _i0 := range link {\n link[_i0] = make([][]chan bool, h)\n for _i1 := range link[_i0] {\n link[_i0][_i1] = make([]chan bool, n)\n for _i2 := range link[_i0][_i1] { link[_i0][_i1][_i2] = make(chan bool) }\n }\n}\n```\n\n### 4d. `generateProcParams()` — multi-dim type strings\n```go\nif p.ChanArrayDims > 0 {\n goType = strings.Repeat(\"[]\", p.ChanArrayDims) + \"chan \" + ...\n} else if p.OpenArrayDims > 0 {\n goType = strings.Repeat(\"[]\", p.OpenArrayDims) + ...\n}\n```\n\n### 4e. Update receive/variant-receive codegen\nLoop over `ChannelIndices` building `chanRef` string with nested `[idx]`.\n\n## Step 5: Tests\n\n### Parser tests (`parser/parser_test.go`)\n- Update existing tests: `decl.Size` → `decl.Sizes[0]`, etc.\n- Add: `TestMultiDimArrayDecl`, `TestMultiDimChanArrayDecl`, `TestMultiDimIndexedSend`, `TestMultiDimIndexedReceive`, `TestMultiDimIndexedAssignment`, `TestMultiDimOpenArrayParam`, `TestMultiDimChanArrayParam`\n\n### Codegen unit tests (`codegen/codegen_test.go`)\n- Update existing tests referencing old field names\n- Add: `TestMultiDimArrayDeclCodegen`, `TestMultiDimChanDeclCodegen`, `TestMultiDimSendCodegen`, `TestMultiDimReceiveCodegen`, `TestMultiDimProcParamCodegen`\n\n### E2E tests (`codegen/e2e_array_test.go`)\n- Add: `TestE2E_MultiDimArray` (2D array, fill + read), `TestE2E_MultiDimChanArray` (2D channel array with send/receive), `TestE2E_MultiDimChanArrayWithProc` (pass `[][]CHAN` to proc)\n\n## Step 6: Documentation\n- Update `CLAUDE.md` mapping table to show multi-dim arrays\n- Update `TODO.md` to mark multi-dim arrays as implemented\n\n## Verification\n\n```bash\ngo build ./... # must compile\ngo test ./... # all tests pass (existing + new)\n# Smoke test with life.occ patterns:\n./occam2go -I examples -o /tmp/multidim_test.go examples/multidim_test.occ # if we create a test file\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.