Implement the following 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
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]):
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]):
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
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
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
If you need specific details from before exiting plan mode (like exact code snippets, error messages, or content you generated), read the full transcript at: /home/david/.claude/projects/-home-david-projects-code-associates-occam2go/5d493ff0-ec84-4714-b1d7-c70b41242661.jsonl