Implement the following plan:
Variant protocol receive (ch ? CASE) case bodies only support a single statement. When a case body has a scoped declaration (e.g., BOOL x :) followed by a compound statement (SEQ ...), parsing fails. This blocks historical-examples/life.occ (issue #72).
The fix follows the identical pattern already used by IF, CASE, and ALT — change Body from a single Statement to []Statement and use parseBodyStatements().
ast/ast.go line 458Change VariantCase.Body from single to multi-statement:
Body Statement → Body []Statement
parser/parser.goparseVariantReceive() (lines 1384-1393): Replace single-statement parse with parseBodyStatements():
// Before:
if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
p.nextToken() // move to body
vc.Body = p.parseStatement()
if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
p.nextToken()
}
}
// After:
if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
vc.Body = p.parseBodyStatements()
}
parseVariantReceiveWithIndex() (lines 1479-1487): Same change.
Note: parseBodyStatements() handles p.nextToken() internally (line 1924: "move past INDENT") and manages its own DEDENT/NEWLINE tracking, so the manual advance code is removed.
codegen/codegen.gogenerateVariantReceive() (lines 1420-1422): Loop over body statements:
// Before:
if vc.Body != nil {
g.generateStatement(vc.Body)
}
// After:
for _, s := range vc.Body {
g.generateStatement(s)
}
All containsX() scanner functions that walk vc.Body — 6 occurrences (containsPar, containsPrint, containsTimer, containsStop, containsMostExpr, walkStatements) plus collectChanProtocols, collectBoolVars, collectRecordVars, containsRetypes, containsAltReplicator. Change from:
if c.Body != nil && g.containsX(c.Body) {
to:
for _, inner := range c.Body {
if g.containsX(inner) {
return true
}
}
This matches the pattern used by IF/CASE choice.Body iteration throughout the file.
parser/parser_test.go for variant receive with scoped declarationscodegen/e2e_test.go that transpiles, compiles, and runs a program using variant receive with declarations in case bodiesgo test ./... # all tests pass
# Specific test for the new feature:
cat > /tmp/test_variant_decl.occ << 'EOF'
PROTOCOL CMD
CASE
set.state; BOOL
evolve
terminate
:
PROC test(CHAN OF CMD control)
BOOL state, not.finished :
SEQ
state := FALSE
not.finished := TRUE
WHILE not.finished
control ? CASE
set.state; state
SKIP
evolve
BOOL next.state :
SEQ
next.state := TRUE
state := next.state
terminate
not.finished := FALSE
:
CHAN OF CMD ch :
PAR
test(ch)
SEQ
ch ! set.state; TRUE
ch ! evolve
ch ! terminate
EOF
./occam2go -o /tmp/test_variant_decl.go /tmp/test_variant_decl.occ
go run /tmp/test_variant_decl.go
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/6850029e-5661-4653-84d5-19bc82f092a9.jsonl
Let me implement the plan to fix #86: Support scoped declarations in variant receive case bodies. I need to modify 4 areas: AST, Parser, Codegen, and Tests. Let me start by reading the relevant files.
{
"file_path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"offset": 450,
"limit": 20
} 450→ Channel string
451→ ChannelIndices []Expression // non-empty for cs[i] ? CASE ... or cs[i][j] ? CASE ...
452→ Cases []VariantCase
453→}
454→
455→type VariantCase struct {
456→ Tag string // variant tag name
457→ Variables []string // variables to bind payload fields
458→ Body Statement
459→}
460→
461→func (vr *VariantReceive) statementNode() {}
462→func (vr *VariantReceive) TokenLiteral() string { return vr.Token.Literal }
463→
464→// RecordDecl represents a record type declaration: RECORD POINT { INT x: INT y: }
465→type RecordDecl struct {
466→ Token lexer.Token // the RECORD token
467→ Name string // record type name
468→ Fields []RecordField // named fields
469→}{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1370,
"limit": 130
} 1370→ // Parse optional variables after semicolons: tag ; x ; y
1371→ for p.peekTokenIs(lexer.SEMICOLON) {
1372→ p.nextToken() // move to ;
1373→ if !p.expectPeek(lexer.IDENT) {
1374→ return stmt
1375→ }
1376→ vc.Variables = append(vc.Variables, p.curToken.Literal)
1377→ }
1378→
1379→ // Skip newlines and expect INDENT for body
1380→ for p.peekTokenIs(lexer.NEWLINE) {
1381→ p.nextToken()
1382→ }
1383→
1384→ if p.peekTokenIs(lexer.INDENT) {
1385→ p.nextToken() // consume INDENT
1386→ p.nextToken() // move to body
1387→ vc.Body = p.parseStatement()
1388→
1389→ // Advance past the last token of the statement if needed
1390→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
1391→ p.nextToken()
1392→ }
1393→ }
1394→
1395→ stmt.Cases = append(stmt.Cases, vc)
1396→
1397→ // No-progress guard: if we haven't moved, break to prevent infinite loop
1398→ if p.curToken == prevToken && p.peekToken == prevPeek {
1399→ p.nextToken() // force progress
1400→ if p.curToken == prevToken {
1401→ break
1402→ }
1403→ }
1404→ }
1405→
1406→ return stmt
1407→}
1408→
1409→func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndices []ast.Expression, token lexer.Token) *ast.VariantReceive {
1410→ stmt := &ast.VariantReceive{
1411→ Token: token,
1412→ Channel: channel,
1413→ ChannelIndices: channelIndices,
1414→ }
1415→
1416→ // Skip to next line
1417→ for p.peekTokenIs(lexer.NEWLINE) {
1418→ p.nextToken()
1419→ }
1420→
1421→ // Expect INDENT
1422→ if !p.peekTokenIs(lexer.INDENT) {
1423→ p.addError("expected indented block after ? CASE")
1424→ return stmt
1425→ }
1426→ p.nextToken() // consume INDENT
1427→ startLevel := p.indentLevel
1428→ p.nextToken() // move into block
1429→
1430→ for !p.curTokenIs(lexer.EOF) {
1431→ for p.curTokenIs(lexer.NEWLINE) {
1432→ p.nextToken()
1433→ }
1434→
1435→ for p.curTokenIs(lexer.DEDENT) {
1436→ if p.indentLevel < startLevel {
1437→ return stmt
1438→ }
1439→ p.nextToken()
1440→ }
1441→
1442→ for p.curTokenIs(lexer.NEWLINE) {
1443→ p.nextToken()
1444→ }
1445→
1446→ if p.curTokenIs(lexer.EOF) {
1447→ break
1448→ }
1449→
1450→ if p.indentLevel < startLevel {
1451→ break
1452→ }
1453→
1454→ // Safety guard: record position before parsing to detect no-progress
1455→ prevToken := p.curToken
1456→ prevPeek := p.peekToken
1457→
1458→ vc := ast.VariantCase{}
1459→
1460→ if !p.curTokenIs(lexer.IDENT) {
1461→ p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
1462→ p.nextToken() // skip unrecognized token to avoid infinite loop
1463→ continue
1464→ }
1465→ vc.Tag = p.curToken.Literal
1466→
1467→ for p.peekTokenIs(lexer.SEMICOLON) {
1468→ p.nextToken() // move to ;
1469→ if !p.expectPeek(lexer.IDENT) {
1470→ return stmt
1471→ }
1472→ vc.Variables = append(vc.Variables, p.curToken.Literal)
1473→ }
1474→
1475→ for p.peekTokenIs(lexer.NEWLINE) {
1476→ p.nextToken()
1477→ }
1478→
1479→ if p.peekTokenIs(lexer.INDENT) {
1480→ p.nextToken() // consume INDENT
1481→ p.nextToken() // move to body
1482→ vc.Body = p.parseStatement()
1483→
1484→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
1485→ p.nextToken()
1486→ }
1487→ }
1488→
1489→ stmt.Cases = append(stmt.Cases, vc)
1490→
1491→ // No-progress guard: if we haven't moved, break to prevent infinite loop
1492→ if p.curToken == prevToken && p.peekToken == prevPeek {
1493→ p.nextToken() // force progress
1494→ if p.curToken == prevToken {
1495→ break
1496→ }
1497→ }
1498→ }
1499→{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1410,
"limit": 30
} 1410→ if len(vr.ChannelIndices) > 0 {
1411→ chanRef += g.generateIndicesStr(vr.ChannelIndices)
1412→ }
1413→ g.writeLine(fmt.Sprintf("switch _v := (<-%s).(type) {", chanRef))
1414→ for _, vc := range vr.Cases {
1415→ g.writeLine(fmt.Sprintf("case _proto_%s_%s:", gProtoName, goIdent(vc.Tag)))
1416→ g.indent++
1417→ for i, v := range vc.Variables {
1418→ g.writeLine(fmt.Sprintf("%s = _v._%d", goIdent(v), i))
1419→ }
1420→ if vc.Body != nil {
1421→ g.generateStatement(vc.Body)
1422→ }
1423→ g.indent--
1424→ }
1425→ g.writeLine("}")
1426→}
1427→
1428→func (g *Generator) isVariantTag(protoName, tagName string) bool {
1429→ proto := g.protocolDefs[protoName]
1430→ if proto == nil {
1431→ return false
1432→ }
1433→ for _, v := range proto.Variants {
1434→ if v.Tag == tagName {
1435→ return true
1436→ }
1437→ }
1438→ return false
1439→}Now let me find all the containsX scanner functions that reference vc.Body or c.Body for variant cases.
{
"pattern": "\\.Body != nil && g\\.contains|\\.Body != nil && walkStatements|\\.Body != nil && g\\.collect|\\.Body != nil && contains",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}569: if c.Body != nil && g.containsPar(c.Body) {
642: if c.Body != nil && g.containsPrint(c.Body) {
718: if c.Body != nil && g.containsTimer(c.Body) {
791: if c.Body != nil && g.containsStop(c.Body) {
912: if c.Body != nil && g.containsMostExpr(c.Body) {{
"pattern": "VariantCase|VariantReceive",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}567: case *ast.VariantReceive:
640: case *ast.VariantReceive:
716: case *ast.VariantReceive:
789: case *ast.VariantReceive:
910: case *ast.VariantReceive:
1065: case *ast.VariantReceive:
1066: g.generateVariantReceive(s)
1406:func (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {
3085: case *ast.VariantReceive:{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1920,
"limit": 30
} 1920→ p.nextToken()
1921→ }
1922→
1923→ // Handle DEDENT tokens
1924→ for p.curTokenIs(lexer.DEDENT) {
1925→ if p.indentLevel < startLevel {
1926→ return statements
1927→ }
1928→ p.nextToken()
1929→ }
1930→
1931→ // Skip any more newlines after DEDENT
1932→ for p.curTokenIs(lexer.NEWLINE) {
1933→ p.nextToken()
1934→ }
1935→
1936→ if p.curTokenIs(lexer.EOF) {
1937→ break
1938→ }
1939→
1940→ if p.indentLevel < startLevel {
1941→ break
1942→ }
1943→
1944→ // Safety guard: record position before parsing to detect no-progress
1945→ prevToken := p.curToken
1946→ prevPeek := p.peekToken
1947→
1948→ stmt := p.parseStatement()
1949→ if stmt != nil {{
"pattern": "func \\(p \\*Parser\\) parseBodyStatements",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true
}{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1897,
"limit": 10
} 1897→
1898→ // After parsing a statement, we need to advance.
1899→ // But if we're already at NEWLINE/DEDENT/EOF, the next iteration will handle it.
1900→ // Only advance if we're still on the last token of the statement.
1901→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
1902→ p.nextToken()
1903→ }
1904→ }
1905→
1906→ return statements{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1905,
"limit": 20
} 1905→
1906→ return statements
1907→}
1908→
1909→// parseBodyStatements parses multiple statements inside a branch body
1910→// (IF choice, CASE choice, ALT case, WHILE). Called after the caller has
1911→// consumed the INDENT token and advanced into the body.
1912→// Returns all statements found at this indentation level.
1913→func (p *Parser) parseBodyStatements() []ast.Statement {
1914→ var statements []ast.Statement
1915→ startLevel := p.indentLevel
1916→
1917→ for !p.curTokenIs(lexer.EOF) {
1918→ // Skip newlines
1919→ for p.curTokenIs(lexer.NEWLINE) {
1920→ p.nextToken()
1921→ }
1922→
1923→ // Handle DEDENT tokens
1924→ for p.curTokenIs(lexer.DEDENT) {Wait, the comment says "Called after the caller has consumed the INDENT token and advanced into the body." But the plan says parseBodyStatements() handles p.nextToken() internally. Let me check more carefully.
Looking at the plan:
Note:
parseBodyStatements()handlesp.nextToken()internally (line 1924: "move past INDENT") and manages its own DEDENT/NEWLINE tracking, so the manual advance code is removed.
Wait, but looking at the actual code, parseBodyStatements() starts with startLevel := p.indentLevel and expects curToken to already be past the INDENT. Let me look at how other callers use it.
Let me check how existing code calls parseBodyStatements() to understand the calling convention.
{
"pattern": "parseBodyStatements\\(\\)",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true,
"-B": 3
}1848- p.nextToken() // consume INDENT
1849- p.nextToken() // move into body
1850-
1851: altCase.Body = p.parseBodyStatements()
--
1910-// (IF choice, CASE choice, ALT case, WHILE). Called after the caller has
1911-// consumed the INDENT token and advanced into the body.
1912-// Returns all statements found at this indentation level.
1913:func (p *Parser) parseBodyStatements() []ast.Statement {
--
2546- p.nextToken() // consume INDENT
2547- p.nextToken() // move to first statement
2548-
2549: loop.Body = p.parseBodyStatements()
--
2628- if p.peekTokenIs(lexer.INDENT) {
2629- p.nextToken() // consume INDENT
2630- p.nextToken() // move to body
2631: choice.Body = p.parseBodyStatements()
--
2718- if p.peekTokenIs(lexer.INDENT) {
2719- p.nextToken() // consume INDENT
2720- p.nextToken() // move to body
2721: choice.Body = p.parseBodyStatements()So the calling convention is: consume INDENT, then call p.nextToken() to move into body, then call parseBodyStatements(). The plan says to just do:
if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
vc.Body = p.parseBodyStatements()
}
But that doesn't match the existing pattern - all other callers do TWO nextToken calls before parseBodyStatements. Let me reread the plan...
The plan says:
Note:
parseBodyStatements()handlesp.nextToken()internally (line 1924: "move past INDENT")
But looking at the code, line 1924 is actually handling DEDENT tokens, not consuming INDENT. The function expects to start at the current token position, so I need to match what all the other callers do: consume INDENT with one nextToken call, then move to the body with another, before calling parseBodyStatements.
I had the pattern wrong initially, but the fix is straightforward — I should follow the established convention that's already working elsewhere in the code.
Now let me read the scanner functions that reference VariantReceive to see the exact patterns.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 560,
"limit": 20
} 560→ for _, choice := range s.Choices {
561→ for _, inner := range choice.Body {
562→ if g.containsPar(inner) {
563→ return true
564→ }
565→ }
566→ }
567→ case *ast.VariantReceive:
568→ for _, c := range s.Cases {
569→ if c.Body != nil && g.containsPar(c.Body) {
570→ return true
571→ }
572→ }
573→ }
574→ return false
575→}
576→
577→func (g *Generator) containsPrint(stmt ast.Statement) bool {
578→ switch s := stmt.(type) {
579→ case *ast.ProcCall:{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 633,
"limit": 20
} 633→ for _, choice := range s.Choices {
634→ for _, inner := range choice.Body {
635→ if g.containsPrint(inner) {
636→ return true
637→ }
638→ }
639→ }
640→ case *ast.VariantReceive:
641→ for _, c := range s.Cases {
642→ if c.Body != nil && g.containsPrint(c.Body) {
643→ return true
644→ }
645→ }
646→ }
647→ return false
648→}
649→
650→func (g *Generator) containsTimer(stmt ast.Statement) bool {
651→ switch s := stmt.(type) {
652→ case *ast.TimerDecl, *ast.TimerRead:{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 709,
"limit": 20
} 709→ for _, choice := range s.Choices {
710→ for _, inner := range choice.Body {
711→ if g.containsTimer(inner) {
712→ return true
713→ }
714→ }
715→ }
716→ case *ast.VariantReceive:
717→ for _, c := range s.Cases {
718→ if c.Body != nil && g.containsTimer(c.Body) {
719→ return true
720→ }
721→ }
722→ }
723→ return false
724→}
725→
726→func (g *Generator) containsStop(stmt ast.Statement) bool {
727→ switch s := stmt.(type) {
728→ case *ast.Stop:{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 782,
"limit": 20
} 782→ for _, choice := range s.Choices {
783→ for _, inner := range choice.Body {
784→ if g.containsStop(inner) {
785→ return true
786→ }
787→ }
788→ }
789→ case *ast.VariantReceive:
790→ for _, c := range s.Cases {
791→ if c.Body != nil && g.containsStop(c.Body) {
792→ return true
793→ }
794→ }
795→ }
796→ return false
797→}
798→
799→func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
800→ switch s := stmt.(type) {
801→ case *ast.Assignment:{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 903,
"limit": 20
} 903→ for _, c := range s.Cases {
904→ for _, inner := range c.Body {
905→ if g.containsMostExpr(inner) {
906→ return true
907→ }
908→ }
909→ }
910→ case *ast.VariantReceive:
911→ for _, c := range s.Cases {
912→ if c.Body != nil && g.containsMostExpr(c.Body) {
913→ return true
914→ }
915→ }
916→ }
917→ return false
918→}
919→
920→func (g *Generator) exprNeedsMath(expr ast.Expression) bool {
921→ if expr == nil {
922→ return false{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 3078,
"limit": 30
} 3078→ for _, c := range s.Cases {
3079→ for _, inner := range c.Body {
3080→ if g.walkStatements(inner, fn) {
3081→ return true
3082→ }
3083→ }
3084→ }
3085→ case *ast.VariantReceive:
3086→ for _, c := range s.Cases {
3087→ if c.Body != nil && g.walkStatements(c.Body, fn) {
3088→ return true
3089→ }
3090→ }
3091→ }
3092→ return false
3093→}
3094→
3095→// walkExpr recursively walks an expression tree, applying fn.
3096→func (g *Generator) walkExpr(expr ast.Expression, fn func(ast.Expression) bool) bool {
3097→ if expr == nil {
3098→ return false
3099→ }
3100→ if fn(expr) {
3101→ return true
3102→ }
3103→ switch e := expr.(type) {
3104→ case *ast.BinaryExpr:
3105→ return g.walkExpr(e.Left, fn) || g.walkExpr(e.Right, fn)
3106→ case *ast.UnaryExpr:
3107→ return g.walkExpr(e.Right, fn)Now I have all the context. Let me make all the changes. First the AST, then both parser locations, then the codegen (generate + 6 scanner functions).
type VariantCase struct {
Tag string // variant tag name
Variables []string // variables to bind payload fields
Body Statement
}type VariantCase struct {
Tag string // variant tag name
Variables []string // variables to bind payload fields
Body []Statement // case body (may include scoped declarations)
}The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.
if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
p.nextToken() // move to body
vc.Body = p.parseStatement()
// Advance past the last token of the statement if needed
if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
p.nextToken()
}
}
stmt.Cases = append(stmt.Cases, vc)
// No-progress guard: if we haven't moved, break to prevent infinite loop
if p.curToken == prevToken && p.peekToken == prevPeek {
p.nextToken() // force progress
if p.curToken == prevToken {
break
}
}
}
return stmt
} if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
p.nextToken() // move to body
vc.Body = p.parseBodyStatements()
}
stmt.Cases = append(stmt.Cases, vc)
// No-progress guard: if we haven't moved, break to prevent infinite loop
if p.curToken == prevToken && p.peekToken == prevPeek {
p.nextToken() // force progress
if p.curToken == prevToken {
break
}
}
}
return stmt
}The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
p.nextToken() // move to body
vc.Body = p.parseStatement()
if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
p.nextToken()
}
}
stmt.Cases = append(stmt.Cases, vc)
// No-progress guard: if we haven't moved, break to prevent infinite loop
if p.curToken == prevToken && p.peekToken == prevPeek {
p.nextToken() // force progress
if p.curToken == prevToken {
break
}
}
} if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
p.nextToken() // move to body
vc.Body = p.parseBodyStatements()
}
stmt.Cases = append(stmt.Cases, vc)
// No-progress guard: if we haven't moved, break to prevent infinite loop
if p.curToken == prevToken && p.peekToken == prevPeek {
p.nextToken() // force progress
if p.curToken == prevToken {
break
}
}
}The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
if vc.Body != nil {
g.generateStatement(vc.Body)
} for _, s := range vc.Body {
g.generateStatement(s)
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
case *ast.VariantReceive:
for _, c := range s.Cases {
if c.Body != nil && g.containsPar(c.Body) {
return true
}
} case *ast.VariantReceive:
for _, c := range s.Cases {
for _, inner := range c.Body {
if g.containsPar(inner) {
return true
}
}
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
case *ast.VariantReceive:
for _, c := range s.Cases {
if c.Body != nil && g.containsPrint(c.Body) {
return true
}
} case *ast.VariantReceive:
for _, c := range s.Cases {
for _, inner := range c.Body {
if g.containsPrint(inner) {
return true
}
}
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
case *ast.VariantReceive:
for _, c := range s.Cases {
if c.Body != nil && g.containsTimer(c.Body) {
return true
}
} case *ast.VariantReceive:
for _, c := range s.Cases {
for _, inner := range c.Body {
if g.containsTimer(inner) {
return true
}
}
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
case *ast.VariantReceive:
for _, c := range s.Cases {
if c.Body != nil && g.containsStop(c.Body) {
return true
}
} case *ast.VariantReceive:
for _, c := range s.Cases {
for _, inner := range c.Body {
if g.containsStop(inner) {
return true
}
}
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
case *ast.VariantReceive:
for _, c := range s.Cases {
if c.Body != nil && g.containsMostExpr(c.Body) {
return true
}
} case *ast.VariantReceive:
for _, c := range s.Cases {
for _, inner := range c.Body {
if g.containsMostExpr(inner) {
return true
}
}
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
case *ast.VariantReceive:
for _, c := range s.Cases {
if c.Body != nil && g.walkStatements(c.Body, fn) {
return true
}
} case *ast.VariantReceive:
for _, c := range s.Cases {
for _, inner := range c.Body {
if g.walkStatements(inner, fn) {
return true
}
}
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
Good, all 7 codegen edits are done. Let me verify the build compiles and run existing tests.
? github.com/codeassociates/occam2go [no test files] ? github.com/codeassociates/occam2go/ast [no test files] ok github.com/codeassociates/occam2go/codegen 18.585s ok github.com/codeassociates/occam2go/lexer (cached) ok github.com/codeassociates/occam2go/modgen (cached) ok github.com/codeassociates/occam2go/parser 0.002s ok github.com/codeassociates/occam2go/preproc (cached)
All tests pass. Now let me add the new tests. First, let me find a good spot in the parser test file for a variant receive test, and check the existing pattern.
{
"pattern": "variant|VariantReceive",
"path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"output_mode": "content",
"-n": true
}1755: if proto.Kind != "variant" {
1756: t.Errorf("expected kind 'variant', got %s", proto.Kind)
1760: t.Fatalf("expected 3 variants, got %d", len(proto.Variants))
1813: if proto.Kind != "variant" {
1814: t.Errorf("expected kind 'variant', got %s", proto.Kind)
1818: t.Fatalf("expected 3 variants, got %d", len(proto.Variants)){
"pattern": "TestParse.*Variant|CASE.*variant",
"path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"output_mode": "content",
"-n": true
}{
"pattern": "func TestParse",
"path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"output_mode": "content",
"-n": true,
"head_limit": 10
}{
"pattern": "^func Test",
"path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"output_mode": "content",
"-n": true
}10:func TestVarDecl(t *testing.T) {
36:func TestMultipleVarDecl(t *testing.T) {
64:func TestAssignment(t *testing.T) {
95:func TestBinaryExpression(t *testing.T) {
133:func TestSeqBlock(t *testing.T) {
167:func TestParBlock(t *testing.T) {
191:func TestChanDecl(t *testing.T) {
217:func TestSend(t *testing.T) {
248:func TestReceive(t *testing.T) {
274:func TestAltBlock(t *testing.T) {
316:func TestAltBlockWithGuard(t *testing.T) {
352:func TestAltBlockWithGuardedSkip(t *testing.T) {
384:func TestPriAltBlock(t *testing.T) {
422:func TestPriParBlock(t *testing.T) {
450:func TestWhileLoop(t *testing.T) {
477:func TestIfStatement(t *testing.T) {
519:func TestReplicatedSeq(t *testing.T) {
562:func TestReplicatedSeqWithStep(t *testing.T) {
616:func TestReplicatedPar(t *testing.T) {
643:func TestReplicatedIf(t *testing.T) {
683:func TestArrayDecl(t *testing.T) {
720:func TestArrayDeclMultipleNames(t *testing.T) {
752:func TestIndexedAssignment(t *testing.T) {
794:func TestIndexExpression(t *testing.T) {
843:func TestFuncDeclIS(t *testing.T) {
886:func TestInlineFuncDecl(t *testing.T) {
925:func TestInlineFuncDeclIS(t *testing.T) {
952:func TestFuncDeclValof(t *testing.T) {
999:func TestMultiResultFuncDecl(t *testing.T) {
1043:func TestMultiAssignment(t *testing.T) {
1081:func TestMultiAssignmentIndexed(t *testing.T) {
1113:func TestMultiAssignmentMixed(t *testing.T) {
1141:func TestCaseStatement(t *testing.T) {
1208:func TestTimerDecl(t *testing.T) {
1230:func TestTimerRead(t *testing.T) {
1273:func TestAfterExpression(t *testing.T) {
1316:func TestChanParam(t *testing.T) {
1352:func TestChanParamMixed(t *testing.T) {
1399:func TestTypeConversion(t *testing.T) {
1434:func TestTypeConversionWithQualifier(t *testing.T) {
1477:func TestTypeConversionInExpression(t *testing.T) {
1523:func TestStringLiteral(t *testing.T) {
1554:func TestStringEscapeConversion(t *testing.T) {
1593:func TestByteLiteral(t *testing.T) {
1636:func TestStringLiteralInProcCall(t *testing.T) {
1667:func TestSimpleProtocolDecl(t *testing.T) {
1697:func TestSequentialProtocolDecl(t *testing.T) {
1730:func TestVariantProtocolDecl(t *testing.T) {
1788:func TestVariantProtocolDeclDottedTags(t *testing.T) {
1846:func TestChanDeclWithProtocol(t *testing.T) {
1873:func TestSequentialSend(t *testing.T) {
1916:func TestRecordDecl(t *testing.T) {
1952:func TestRecordDeclMultipleFieldNames(t *testing.T) {
1983:func TestRecordVarDecl(t *testing.T) {
2012:func TestRecordFieldAssignment(t *testing.T) {
2047:func TestRecordFieldAccess(t *testing.T) {
2092:func TestChanArrayDecl(t *testing.T) {
2130:func TestIndexedSend(t *testing.T) {
2172:func TestIndexedReceive(t *testing.T) {
2210:func TestChanArrayParam(t *testing.T) {
2255:func TestChanDirParam(t *testing.T) {
2305:func TestChanArrayDirParam(t *testing.T) {
2328:func TestSequentialReceive(t *testing.T) {
2358:func TestSizeExpression(t *testing.T) {
2389:func TestSizeExpressionInBinaryExpr(t *testing.T) {
2414:func TestMostNegExpression(t *testing.T) {
2444:func TestMostPosExpression(t *testing.T) {
2470:func TestMostNegInBinaryExpr(t *testing.T) {
2495:func TestValAbbreviation(t *testing.T) {
2533:func TestNonValAbbreviation(t *testing.T) {
2568:func TestValBoolAbbreviation(t *testing.T) {
2596:func TestAbbreviationWithExpression(t *testing.T) {
2624:func TestInitialDecl(t *testing.T) {
2655:func TestInitialDeclWithExpression(t *testing.T) {
2685:func TestOpenArrayParam(t *testing.T) {
2733:func TestChanDeclShorthand(t *testing.T) {
2759:func TestChanArrayDeclShorthand(t *testing.T) {
2789:func TestChanParamShorthand(t *testing.T) {
2833:func TestHexIntegerLiteral(t *testing.T) {
2860:func TestHexIntegerLiteralLarge(t *testing.T) {
2887:func TestNestedProcDecl(t *testing.T) {
2939:func TestNestedFuncDecl(t *testing.T) {
2978:func TestProcLocalVarDecls(t *testing.T) {
3016:func TestCheckedArithmeticOperators(t *testing.T) {
3052:func TestCheckedArithmeticPrecedence(t *testing.T) {
3076:func TestCheckedAndSymbolMixed(t *testing.T) {
3092:func TestMultiStatementIfBody(t *testing.T) {
3142:func TestChannelDirAtCallSite(t *testing.T) {
3184:func TestUntypedValAbbreviation(t *testing.T) {
3222:func TestArrayLiteral(t *testing.T) {
3263:func TestRetypesDecl(t *testing.T) {
3297:func TestRetypesDeclArray(t *testing.T) {
3341:func TestMultiLineBooleanExpression(t *testing.T) {
3394:func TestAltReplicator(t *testing.T) {
3468:func TestAltReplicatorWithAbbreviation(t *testing.T) {
3518:func TestInt16Int32Int64VarDecl(t *testing.T) {
3546:func TestInt16Int32Int64TypeConversion(t *testing.T) {
3578:func TestMostNegMostPosInt16Int32Int64(t *testing.T) {
3610:func TestMultiDimArrayDecl(t *testing.T) {
3650:func TestMultiDimChanArrayDecl(t *testing.T) {
3690:func TestMultiDimIndexedAssignment(t *testing.T) {
3726:func TestMultiDimIndexedSend(t *testing.T) {
3752:func TestMultiDimIndexedReceive(t *testing.T) {
3782:func TestMultiDimOpenArrayParam(t *testing.T) {wc -l /home/david/projects/code-associates/occam2go/parser/parser_test.go
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"offset": 3800,
"limit": 20
} 3800→ if len(proc.Params) != 1 {
3801→ t.Fatalf("expected 1 param, got %d", len(proc.Params))
3802→ }
3803→
3804→ p0 := proc.Params[0]
3805→ if p0.ChanArrayDims != 2 {
3806→ t.Errorf("expected ChanArrayDims=2, got %d", p0.ChanArrayDims)
3807→ }
3808→ if !p0.IsChan {
3809→ t.Error("expected IsChan=true")
3810→ }
3811→ if p0.ChanElemType != "INT" {
3812→ t.Errorf("expected ChanElemType=INT, got %s", p0.ChanElemType)
3813→ }
3814→}
3815→{
"pattern": "variant|VariantProtocol|variant.*protocol",
"path": "/home/david/projects/code-associates/occam2go/codegen/e2e_test.go",
"output_mode": "content",
"-n": true,
"-i": true
}<tool_use_error>Path does not exist: /home/david/projects/code-associates/occam2go/codegen/e2e_test.go. Note: your current working directory is /home/david/projects/code-associates/occam2go.</tool_use_error>
/home/david/projects/code-associates/occam2go/codegen/e2e_preproc_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_record_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_proc_func_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_replicator_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_basic_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_phase2_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_course_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_intrinsics_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_strings_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_params_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_retypes_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_types_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_array_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go
{
"pattern": "func TestE2E.*[Vv]ariant",
"path": "/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go",
"output_mode": "content",
"-n": true
}44:func TestE2E_VariantProtocol(t *testing.T) {
70:func TestE2E_VariantProtocolNoPayload(t *testing.T) {
96:func TestE2E_VariantProtocolDottedTags(t *testing.T) {
177:func TestE2E_VariantProtocolTrailingColon(t *testing.T) {{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go",
"offset": 44,
"limit": 60
} 44→func TestE2E_VariantProtocol(t *testing.T) {
45→ // Variant protocol: tagged union with CASE receive
46→ occam := `PROTOCOL MSG
47→ CASE
48→ data; INT
49→ quit
50→
51→SEQ
52→ CHAN OF MSG c:
53→ INT result:
54→ result := 0
55→ PAR
56→ c ! data ; 42
57→ c ? CASE
58→ data ; result
59→ print.int(result)
60→ quit
61→ print.int(0)
62→`
63→ output := transpileCompileRun(t, occam)
64→ expected := "42\n"
65→ if output != expected {
66→ t.Errorf("expected %q, got %q", expected, output)
67→ }
68→}
69→
70→func TestE2E_VariantProtocolNoPayload(t *testing.T) {
71→ // Variant protocol with no-payload tag
72→ occam := `PROTOCOL MSG
73→ CASE
74→ data; INT
75→ quit
76→
77→SEQ
78→ CHAN OF MSG c:
79→ INT result:
80→ result := 0
81→ PAR
82→ c ! quit
83→ c ? CASE
84→ data ; result
85→ print.int(result)
86→ quit
87→ print.int(99)
88→`
89→ output := transpileCompileRun(t, occam)
90→ expected := "99\n"
91→ if output != expected {
92→ t.Errorf("expected %q, got %q", expected, output)
93→ }
94→}
95→
96→func TestE2E_VariantProtocolDottedTags(t *testing.T) {
97→ // Variant protocol with dotted tag names (e.g., bar.data)
98→ occam := `PROTOCOL BAR.PROTO
99→ CASE
100→ bar.data; INT
101→ bar.terminate
102→ bar.blank; INT
103→{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go",
"offset": 177,
"limit": 60
} 177→func TestE2E_VariantProtocolTrailingColon(t *testing.T) {
178→ // Issue #73: trailing colon on variant protocol declarations
179→ occam := `PROTOCOL MSG
180→ CASE
181→ data; INT
182→ quit
183→:
184→
185→SEQ
186→ CHAN OF MSG c:
187→ INT result:
188→ result := 0
189→ PAR
190→ c ! data ; 55
191→ c ? CASE
192→ data ; result
193→ print.int(result)
194→ quit
195→ print.int(0)
196→`
197→ output := transpileCompileRun(t, occam)
198→ expected := "55\n"
199→ if output != expected {
200→ t.Errorf("expected %q, got %q", expected, output)
201→ }
202→}
203→ if p0.ChanElemType != "INT" {
t.Errorf("expected ChanElemType=INT, got %s", p0.ChanElemType)
}
} if p0.ChanElemType != "INT" {
t.Errorf("expected ChanElemType=INT, got %s", p0.ChanElemType)
}
}
func TestVariantReceiveScopedDecl(t *testing.T) {
input := `PROTOCOL CMD
CASE
data; INT
evolve
quit
PROC test(CHAN OF CMD ch)
BOOL done:
SEQ
done := FALSE
WHILE NOT done
ch ? CASE
data; done
SKIP
evolve
BOOL flag:
SEQ
flag := TRUE
done := flag
quit
done := TRUE
:
`
l := lexer.New(input)
p := parser.New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
// Find the PROC
if len(program.Statements) < 2 {
t.Fatalf("expected at least 2 statements, got %d", len(program.Statements))
}
proc, ok := program.Statements[1].(*ast.ProcDecl)
if !ok {
t.Fatalf("expected ProcDecl, got %T", program.Statements[1])
}
// Walk to the variant receive inside the WHILE
// proc body: VarDecl(done), SeqBlock{ assign, WhileLoop{ VariantReceive } }
seq, ok := proc.Body[1].(*ast.SeqBlock)
if !ok {
t.Fatalf("expected SeqBlock, got %T", proc.Body[1])
}
wl, ok := seq.Statements[1].(*ast.WhileLoop)
if !ok {
t.Fatalf("expected WhileLoop, got %T", seq.Statements[1])
}
vr, ok := wl.Body.(*ast.VariantReceive)
if !ok {
t.Fatalf("expected VariantReceive, got %T", wl.Body)
}
if len(vr.Cases) != 3 {
t.Fatalf("expected 3 variant cases, got %d", len(vr.Cases))
}
// "evolve" case should have 2 body statements: VarDecl + SeqBlock
evolveCase := vr.Cases[1]
if evolveCase.Tag != "evolve" {
t.Errorf("expected tag 'evolve', got %s", evolveCase.Tag)
}
if len(evolveCase.Body) != 2 {
t.Fatalf("expected 2 body statements in 'evolve' case, got %d", len(evolveCase.Body))
}
if _, ok := evolveCase.Body[0].(*ast.VarDecl); !ok {
t.Errorf("expected VarDecl as first body statement, got %T", evolveCase.Body[0])
}
if _, ok := evolveCase.Body[1].(*ast.SeqBlock); !ok {
t.Errorf("expected SeqBlock as second body statement, got %T", evolveCase.Body[1])
}
}The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
func TestE2E_VariantProtocolTrailingColon(t *testing.T) {func TestE2E_VariantReceiveScopedDecl(t *testing.T) {
// Issue #86: scoped declarations in variant receive case bodies
occam := `PROTOCOL CMD
CASE
set.val; INT
evolve
terminate
PROC test(CHAN OF CMD control)
INT state:
BOOL running:
SEQ
state := 0
running := TRUE
WHILE running
control ? CASE
set.val; state
SKIP
evolve
INT next:
SEQ
next := state + 10
state := next
terminate
running := FALSE
print.int(state)
:
CHAN OF CMD ch:
SEQ
PAR
test(ch)
SEQ
ch ! set.val; 5
ch ! evolve
ch ! terminate
`
output := transpileCompileRun(t, occam)
expected := "15\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}
func TestE2E_VariantProtocolTrailingColon(t *testing.T) {The file /home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go has been updated successfully.
Exit code 1 # github.com/codeassociates/occam2go/parser [github.com/codeassociates/occam2go/parser.test] parser/parser_test.go:3841:7: undefined: parser FAIL github.com/codeassociates/occam2go/parser [build failed] FAIL # github.com/codeassociates/occam2go/parser [github.com/codeassociates/occam2go/parser.test] parser/parser_test.go:3841:7: undefined: parser FAIL github.com/codeassociates/occam2go/parser [build failed] FAIL
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"offset": 1,
"limit": 10
} 1→package parser
2→
3→import (
4→ "testing"
5→
6→ "github.com/codeassociates/occam2go/ast"
7→ "github.com/codeassociates/occam2go/lexer"
8→)
9→
10→func TestVarDecl(t *testing.T) {{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"offset": 3836,
"limit": 10
}3836→ quit 3837→ done := TRUE 3838→: 3839→` 3840→ l := lexer.New(input) 3841→ p := parser.New(l) 3842→ program := p.ParseProgram() 3843→ checkParserErrors(t, p) 3844→ 3845→ // Find the PROC
l := lexer.New(input) p := parser.New(l) program := p.ParseProgram() checkParserErrors(t, p) // Find the PROC
l := lexer.New(input) pr := New(l) program := pr.ParseProgram() checkParserErrors(t, pr) // Find the PROC
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
Exit code 1 # github.com/codeassociates/occam2go/parser [github.com/codeassociates/occam2go/parser.test] parser/parser_test.go:3864:12: invalid operation: wl.Body (variable of type []ast.Statement) is not an interface FAIL github.com/codeassociates/occam2go/parser [build failed] FAIL # github.com/codeassociates/occam2go/parser [github.com/codeassociates/occam2go/parser.test] parser/parser_test.go:3864:12: invalid operation: wl.Body (variable of type []ast.Statement) is not an interface FAIL github.com/codeassociates/occam2go/parser [build failed] FAIL
{
"pattern": "type WhileLoop struct",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-n": true,
"-A": 5
}191:type WhileLoop struct {
192- Token lexer.Token // the WHILE token
193- Condition Expression
194- Body []Statement
195-}
196-Body is []Statement, so I need to index into it. The while body will have a single VariantReceive statement.
wl, ok := seq.Statements[1].(*ast.WhileLoop)
if !ok {
t.Fatalf("expected WhileLoop, got %T", seq.Statements[1])
}
vr, ok := wl.Body.(*ast.VariantReceive)
if !ok {
t.Fatalf("expected VariantReceive, got %T", wl.Body)
} wl, ok := seq.Statements[1].(*ast.WhileLoop)
if !ok {
t.Fatalf("expected WhileLoop, got %T", seq.Statements[1])
}
if len(wl.Body) < 1 {
t.Fatalf("expected at least 1 statement in while body, got %d", len(wl.Body))
}
vr, ok := wl.Body[0].(*ast.VariantReceive)
if !ok {
t.Fatalf("expected VariantReceive, got %T", wl.Body[0])
}The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
=== RUN TestVariantReceiveScopedDecl --- PASS: TestVariantReceiveScopedDecl (0.00s) PASS ok github.com/codeassociates/occam2go/parser 0.001s
=== RUN TestE2E_VariantReceiveScopedDecl --- PASS: TestE2E_VariantReceiveScopedDecl (0.08s) PASS ok github.com/codeassociates/occam2go/codegen 0.084s
? github.com/codeassociates/occam2go [no test files] ? github.com/codeassociates/occam2go/ast [no test files] ok github.com/codeassociates/occam2go/codegen 19.272s ok github.com/codeassociates/occam2go/lexer (cached) ok github.com/codeassociates/occam2go/modgen (cached) ok github.com/codeassociates/occam2go/parser 0.002s ok github.com/codeassociates/occam2go/preproc (cached)
All tests pass. Here's a summary of the changes:
AST (ast/ast.go): Changed VariantCase.Body from Statement to []Statement.
Parser (parser/parser.go): In both parseVariantReceive() and parseVariantReceiveWithIndex(), replaced single-statement parsing with parseBodyStatements(), which handles multiple statements including scoped declarations at the same indentation level.
Codegen (codegen/codegen.go):
- generateVariantReceive(): Loop over vc.Body slice instead of single nil-check
- 6 scanner functions (containsPar, containsPrint, containsTimer, containsStop, containsMostExpr, walkStatements): Changed single-statement checks to iterate over the c.Body slice, matching the pattern already used by IF/CASE choices
Tests:
- Parser test (TestVariantReceiveScopedDecl): Verifies that a variant case body with a BOOL flag: declaration followed by a SEQ block parses into 2 body statements
- E2E test (TestE2E_VariantReceiveScopedDecl): Full transpile-compile-run test with scoped INT next: declaration inside a variant case body, verifying correct output
On branch main Your branch is up to date with 'origin/main'. Changes not staged for commit: (use "git add <file>..." to update what will be committed) (use "git restore <file>..." to discard changes in working directory) modified: ast/ast.go modified: codegen/codegen.go modified: codegen/e2e_protocol_test.go modified: parser/parser.go modified: parser/parser_test.go no changes added to commit (use "git add" and/or "git commit -a")
diff --git a/ast/ast.go b/ast/ast.go
index 95752e6..a504e54 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -453,9 +453,9 @@ type VariantReceive struct {
}
type VariantCase struct {
- Tag string // variant tag name
- Variables []string // variables to bind payload fields
- Body Statement
+ Tag string // variant tag name
+ Variables []string // variables to bind payload fields
+ Body []Statement // case body (may include scoped declarations)
}
func (vr *VariantReceive) statementNode() {}
diff --git a/codegen/codegen.go b/codegen/codegen.go
index cec8b5b..849ec88 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -566,8 +566,10 @@ func (g *Generator) containsPar(stmt ast.Statement) bool {
}
case *ast.VariantReceive:
for _, c := range s.Cases {
- if c.Body != nil && g.containsPar(c.Body) {
- return true
+ for _, inner := range c.Body {
+ if g.containsPar(inner) {
+ return true
+ }
}
}
}
@@ -639,8 +641,10 @@ func (g *Generator) containsPrint(stmt ast.Statement) bool {
}
case *ast.VariantReceive:
for _, c := range s.Cases {
- if c.Body != nil && g.containsPrint(c.Body) {
- return true
+ for _, inner := range c.Body {
+ if g.containsPrint(inner) {
+ return true
+ }
}
}
}
@@ -715,8 +719,10 @@ func (g *Generator) containsTimer(stmt ast.Statement) bool {
}
case *ast.VariantReceive:
for _, c := range s.Cases {
- if c.Body != nil && g.containsTimer(c.Body) {
- return true
+ for _, inner := range c.Body {
+ if g.containsTimer(inner) {
+ return true
+ }
}
}
}
@@ -788,8 +794,10 @@ func (g *Generator) containsStop(stmt ast.Statement) bool {
}
case *ast.VariantReceive:
for _, c := range s.Cases {
- if c.Body != nil && g.containsStop(c.Body) {
- return true
+ for _, inner := range c.Body {
+ if g.containsStop(inner) {
+ return true
+ }
}
}
}
@@ -909,8 +917,10 @@ func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
}
case *ast.VariantReceive:
for _, c := range s.Cases {
- if c.Body != nil && g.containsMostExpr(c.Body) {
- return true
+ for _, inner := range c.Body {
+ if g.containsMostExpr(inner) {
+ return true
+ }
}
}
}
@@ -1417,8 +1427,8 @@ func (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {
for i, v := range vc.Variables {
g.writeLine(fmt.Sprintf("%s = _v._%d", goIdent(v), i))
}
- if vc.Body != nil {
- g.generateStatement(vc.Body)
+ for _, s := range vc.Body {
+ g.generateStatement(s)
}
g.indent--
}
@@ -3084,8 +3094,10 @@ func (g *Generator) walkStatements(stmt ast.Statement, fn func(ast.Expression) b
}
case *ast.VariantReceive:
for _, c := range s.Cases {
- if c.Body != nil && g.walkStatements(c.Body, fn) {
- return true
+ for _, inner := range c.Body {
+ if g.walkStatements(inner, fn) {
+ return true
+ }
}
}
}
diff --git a/codegen/e2e_protocol_test.go b/codegen/e2e_protocol_test.go
index 900ff95..292d718 100644
--- a/codegen/e2e_protocol_test.go
+++ b/codegen/e2e_protocol_test.go
@@ -174,6 +174,49 @@ SEQ
}
}
+func TestE2E_VariantReceiveScopedDecl(t *testing.T) {
+ // Issue #86: scoped declarations in variant receive case bodies
+ occam := `PROTOCOL CMD
+ CASE
+ set.val; INT
+ evolve
+ terminate
+
+PROC test(CHAN OF CMD control)
+ INT state:
+ BOOL running:
+ SEQ
+ state := 0
+ running := TRUE
+ WHILE running
+ control ? CASE
+ set.val; state
+ SKIP
+ evolve
+ INT next:
+ SEQ
+ next := state + 10
+ state := next
+ terminate
+ running := FALSE
+ print.int(state)
+:
+CHAN OF CMD ch:
+SEQ
+ PAR
+ test(ch)
+ SEQ
+ ch ! set.val; 5
+ ch ! evolve
+ ch ! terminate
+`
+ output := transpileCompileRun(t, occam)
+ expected := "15\n"
+ if output != expected {
+ t.Errorf("expected %q, got %q", expected, output)
+ }
+}
+
func TestE2E_VariantProtocolTrailingColon(t *testing.T) {
// Issue #73: trailing colon on variant protocol declarations
occam := `PROTOCOL MSG
diff --git a/parser/parser.go b/parser/parser.go
index 3f78b71..704fcb8 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1384,12 +1384,7 @@ func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.Var
if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
p.nextToken() // move to body
- vc.Body = p.parseStatement()
-
- // Advance past the last token of the statement if needed
- if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
- p.nextToken()
- }
+ vc.Body = p.parseBodyStatements()
}
stmt.Cases = append(stmt.Cases, vc)
@@ -1479,11 +1474,7 @@ func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndices []a
if p.peekTokenIs(lexer.INDENT) {
p.nextToken() // consume INDENT
p.nextToken() // move to body
- vc.Body = p.parseStatement()
-
- if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
- p.nextToken()
- }
+ vc.Body = p.parseBodyStatements()
}
stmt.Cases = append(stmt.Cases, vc)
diff --git a/parser/parser_test.go b/parser/parser_test.go
index ad499ac..1002b29 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -3812,3 +3812,79 @@ func TestMultiDimOpenArrayParam(t *testing.T) {
t.Errorf("expected ChanElemType=INT, got %s", p0.ChanElemType)
}
}
+
+func TestVariantReceiveScopedDecl(t *testing.T) {
+ input := `PROTOCOL CMD
+ CASE
+ data; INT
+ evolve
+ quit
+
+PROC test(CHAN OF CMD ch)
+ BOOL done:
+ SEQ
+ done := FALSE
+ WHILE NOT done
+ ch ? CASE
+ data; done
+ SKIP
+ evolve
+ BOOL flag:
+ SEQ
+ flag := TRUE
+ done := flag
+ quit
+ done := TRUE
+:
+`
+ l := lexer.New(input)
+ pr := New(l)
+ program := pr.ParseProgram()
+ checkParserErrors(t, pr)
+
+ // Find the PROC
+ if len(program.Statements) < 2 {
+ t.Fatalf("expected at least 2 statements, got %d", len(program.Statements))
+ }
+ proc, ok := program.Statements[1].(*ast.ProcDecl)
+ if !ok {
+ t.Fatalf("expected ProcDecl, got %T", program.Statements[1])
+ }
+
+ // Walk to the variant receive inside the WHILE
+ // proc body: VarDecl(done), SeqBlock{ assign, WhileLoop{ VariantReceive } }
+ seq, ok := proc.Body[1].(*ast.SeqBlock)
+ if !ok {
+ t.Fatalf("expected SeqBlock, got %T", proc.Body[1])
+ }
+ wl, ok := seq.Statements[1].(*ast.WhileLoop)
+ if !ok {
+ t.Fatalf("expected WhileLoop, got %T", seq.Statements[1])
+ }
+ if len(wl.Body) < 1 {
+ t.Fatalf("expected at least 1 statement in while body, got %d", len(wl.Body))
+ }
+ vr, ok := wl.Body[0].(*ast.VariantReceive)
+ if !ok {
+ t.Fatalf("expected VariantReceive, got %T", wl.Body[0])
+ }
+
+ if len(vr.Cases) != 3 {
+ t.Fatalf("expected 3 variant cases, got %d", len(vr.Cases))
+ }
+
+ // "evolve" case should have 2 body statements: VarDecl + SeqBlock
+ evolveCase := vr.Cases[1]
+ if evolveCase.Tag != "evolve" {
+ t.Errorf("expected tag 'evolve', got %s", evolveCase.Tag)
+ }
+ if len(evolveCase.Body) != 2 {
+ t.Fatalf("expected 2 body statements in 'evolve' case, got %d", len(evolveCase.Body))
+ }
+ if _, ok := evolveCase.Body[0].(*ast.VarDecl); !ok {
+ t.Errorf("expected VarDecl as first body statement, got %T", evolveCase.Body[0])
+ }
+ if _, ok := evolveCase.Body[1].(*ast.SeqBlock); !ok {
+ t.Errorf("expected SeqBlock as second body statement, got %T", evolveCase.Body[1])
+ }
+}2bd8573 Merge pull request #83 from codeassociates/feature/pri-alt-par-76 b49c3b8 Add PRI ALT and PRI PAR support (#76) 5ded5ab Merge pull request #82 from codeassociates/fix/guarded-skip-alt-77 24dcefe Fix guarded SKIP in ALT to respect guard expression (#77) 27f95e8 Merge pull request #81 from codeassociates/feature/multi-dim-arrays
git add ast/ast.go codegen/codegen.go codegen/e2e_protocol_test.go parser/parser.go parser/parser_test.go && git commit -m "$(cat <<'EOF' Support scoped declarations in variant receive case bodies (#86) Change VariantCase.Body from single Statement to []Statement and use parseBodyStatements(), matching the pattern already used by IF, CASE, and ALT. This allows case bodies to contain scoped declarations (e.g. BOOL x:) followed by compound statements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> EOF )"
5 files changed, 150 insertions(+), 28 deletions(-)
remote: remote: Create a pull request for 'fix/variant-receive-scoped-decl-86' on GitHub by visiting: remote: https://github.com/codeassociates/occam2go/pull/new/fix/variant-receive-scoped-decl-86 remote: To associate-1.github.com:codeassociates/occam2go.git * [new branch] fix/variant-receive-scoped-decl-86 -> fix/variant-receive-scoped-decl-86 branch 'fix/variant-receive-scoped-decl-86' set up to track 'origin/fix/variant-receive-scoped-decl-86'.
gh pr create --title "Support scoped declarations in variant receive case bodies" --body "$(cat <<'EOF' ## Summary - Fixes #86: variant protocol receive (`ch ? CASE`) case bodies now support scoped declarations (e.g. `BOOL x:`, `INT next:`) followed by compound statements - Changes `VariantCase.Body` from single `Statement` to `[]Statement` and uses `parseBodyStatements()`, matching the pattern already used by IF, CASE, and ALT - Updates all 7 codegen scanner/walker functions to iterate over the body slice ## Test plan - [x] New parser test (`TestVariantReceiveScopedDecl`) verifies multi-statement body parsing - [x] New e2e test (`TestE2E_VariantReceiveScopedDecl`) transpiles, compiles, and runs a program with scoped declarations in variant case bodies - [x] Full test suite passes (`go test ./...`) 🤖 Generated with [Claude Code](https://claude.com/claude-code) EOF )"
Switched to branch 'main' Your branch is up to date with 'origin/main'. From associate-1.github.com:codeassociates/occam2go 2bd8573..32dedd5 main -> origin/main Updating 2bd8573..32dedd5 Fast-forward ast/ast.go | 6 ++-- codegen/codegen.go | 40 +++++++++++++++-------- codegen/e2e_protocol_test.go | 43 +++++++++++++++++++++++++ parser/parser.go | 13 ++------ parser/parser_test.go | 76 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 150 insertions(+), 28 deletions(-)
title: Support receive into indexed target variables state: OPEN author: associate-1 labels: comments: 0 assignees: projects: milestone: number: 85 -- ## Description The parser only accepts a simple identifier after `?` in channel receive statements. Indexed target variables like `flags[0]` or `state.of.neighbour[d]` cause parse failures. ## Occam2 syntax ```occam [8]CHAN OF INT link : [8]BOOL flags : SEQ link[0] ? flags[0] ``` ```occam link[nx[d]][ny[d]][d] ? state.of.neighbour[d] ``` ## Current behavior ``` expected type after array size, got NEWLINE ``` The parser consumes the variable name (`flags`) but leaves the index expression (`[0]`) unprocessed. The leftover `[` token is then misinterpreted as the start of an array declaration. ## Root cause Both `parseReceive()` (line 1291) and `parseIndexedOperation()` (line 789) do `p.expectPeek(lexer.IDENT)` after `?`, accepting only a bare identifier. The `Receive` AST node stores `Variable string` which cannot represent an indexed target. The same limitation exists in ALT case parsing (`AltCase.Variable` is also a `string`). ## Expected Go output ```go // link[0] ? flags[0] flags[0] = <-link[0] // link[nx[d]][ny[d]][d] ? state.of.neighbour[d] state_of_neighbour[d] = <-link[nx[d]][ny[d]][d] ``` ## Fix scope - **AST**: Extend `Receive.Variable` (and `AltCase.Variable`) to support indexed targets — either add a `VariableIndices []Expression` field or change `Variable` to an `Expression` - **Parser**: Update `parseReceive()`, `parseIndexedOperation()` receive handling, and ALT case parsing to accept `IDENT[expr]` after `?` - **Codegen**: Generate indexed receive targets ## Context Found in `historical-examples/life.occ` (issue #72). Affects lines 57, 225, and any other receive-into-indexed-array pattern.
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.
{
"prompt": "Explore the occam2go codebase to understand how channel receive is currently parsed and generated. I need to understand:\n\n1. The `Receive` AST node in `ast/ast.go` - its fields, especially `Variable`\n2. The `AltCase` AST node in `ast/ast.go` - its fields, especially `Variable`\n3. How `parseReceive()` works in `parser/parser.go` (around line 1291) - specifically how it handles the target variable after `?`\n4. How `parseIndexedOperation()` handles receive in `parser/parser.go` (around line 789) - how indexed channel receive like `ch[i] ? x` works\n5. How ALT case parsing handles the receive variable (search for AltCase in parser.go)\n6. How `generateReceive()` in `codegen/codegen.go` generates Go code for receives\n7. How ALT case generation handles the receive variable in codegen\n\nFor each, provide the exact line numbers and relevant code snippets.",
"subagent_type": "Explore"
}{
"prompt": "Explore the occam2go codebase to understand how INDEXED operations are handled. Specifically:\n\n1. How does `IndexedAssignment` work in the AST (`ast/ast.go`)? What fields does it have for representing indexed targets?\n2. How does `IndexedSend` work (send to a channel at an index like `ch[i] ! expr`)?\n3. How is `Receive` with channel indices handled (`ChannelIndices` field)?\n4. Look at the `Assignment` AST node - does it have any `Indices` field?\n5. In the parser, how does `parseIndexedOperation()` work (around line 789 in parser.go)? Show the full function.\n6. In codegen, how does `generateReceive()` handle `ChannelIndices`? Show the full function.\n7. In codegen, look at how ALT cases generate receive code - how do they handle the receive variable?\n\nProvide exact line numbers and code snippets for all findings.",
"subagent_type": "Explore"
}[
{
"type": "text",
"text": "Great! Now I have all the information I need. Let me compile a comprehensive report:\n\n## Summary: Channel Receive Parsing and Code Generation in occam2go\n\n### 1. **Receive AST Node** (`ast/ast.go`, lines 373-383)\n\n```go\ntype Receive struct {\n\tToken lexer.Token // the ? token\n\tChannel string // channel name\n\tChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x\n\tVariable string // variable to receive into (simple receive)\n\tVariables []string // additional variables for sequential receives (c ? x ; y)\n}\n```\n\n**Key Fields:**\n- **`Token`**: The `?` token for reference\n- **`Channel`**: Simple channel name (e.g., `\"c\"`)\n- **`ChannelIndices`**: Slice of index expressions for indexed receives like `cs[i] ? x` or `cs[i][j] ? x` (empty for simple channels)\n- **`Variable`**: The first/primary target variable name (stored as string, not Expression)\n- **`Variables`**: Additional target variables for sequential receives (c ? x ; y ; z)\n\n**Design Notes:**\n- Variable names are stored as strings, not as expressions\n- Only handles one \"primary\" variable plus additional sequential variables\n- No support for indexed targets in the Variable field itself\n\n---\n\n### 2. **AltCase AST Node** (`ast/ast.go`, lines 397-409)\n\n```go\ntype AltCase struct {\n\tGuard Expression // optional guard condition (nil if no guard)\n\tChannel string // channel name\n\tChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT\n\tVariable string // variable to receive into\n\tBody []Statement // the body to execute\n\tIsTimer bool // true if this is a timer AFTER case\n\tIsSkip bool // true if this is a guarded SKIP case (guard & SKIP)\n\tTimer string // timer name (when IsTimer)\n\tDeadline Expression // AFTER deadline expression (when IsTimer)\n\tDeclarations []Statement // scoped declarations before channel input (e.g., BYTE ch:)\n}\n```\n\n**Key Fields for Receive:**\n- **`Channel`**: Channel name (string)\n- **`ChannelIndices`**: Index expressions for indexed channels in ALT\n- **`Variable`**: Single target variable name (string) - no `Variables` array like `Receive`\n- **`Declarations`**: Scoped declarations that can declare the receive variable type\n\n**Design Notes:**\n- ALT cases only support a single receive variable (no sequential receives like `c ? x ; y`)\n- Variable type is determined via scoped declarations in `Declarations` field\n- Used in `generateReplicatedAlt()` to infer receive type\n\n---\n\n### 3. **parseReceive() Function** (`parser/parser.go`, lines 1274-1306)\n\n```go\nfunc (p *Parser) parseReceive() ast.Statement {\n\tchannel := p.curToken.Literal // Save channel name\n\n\tp.nextToken() // Move to ?\n\trecvToken := p.curToken\n\n\t// Check for variant receive: c ? CASE\n\tif p.peekTokenIs(lexer.CASE) {\n\t\tp.nextToken() // Move to CASE\n\t\treturn p.parseVariantReceive(channel, recvToken)\n\t}\n\n\tstmt := &ast.Receive{\n\t\tChannel: channel,\n\t\tToken: recvToken,\n\t}\n\n\t// LINE 1291-1294: CRITICAL - only expects IDENT after ?\n\tif !p.expectPeek(lexer.IDENT) { // Expect identifier as target\n\t\treturn nil\n\t}\n\tstmt.Variable = p.curToken.Literal // Store variable name as string\n\n\t// Check for sequential receive: c ? x ; y ; z\n\tfor p.peekTokenIs(lexer.SEMICOLON) {\n\t\tp.nextToken() // Move to ;\n\t\tif !p.expectPeek(lexer.IDENT) {\n\t\t\treturn nil\n\t\t}\n\t\tstmt.Variables = append(stmt.Variables, p.curToken.Literal)\n\t}\n\n\treturn stmt\n}\n```\n\n**Parsing Logic:**\n1. Line 1275: Captures channel name from current token\n2. Line 1277-1278: Moves to `?` and saves its token\n3. Line 1281-1283: Checks if variant receive (`c ? CASE`) before creating Receive node\n4. **Line 1291-1294**: After `?`, expects an IDENT token and stores it as Variable string\n5. Lines 1297-1303: Handles sequential receives by looping on semicolons, collecting additional IDENT tokens\n\n**Key Limitation:**\n- Line 1291 uses `p.expectPeek(lexer.IDENT)` - **assumes the target is always a simple identifier**\n- Does NOT support indexed targets like `c ? arr[0]` or complex expressions\n\n---\n\n### 4. **parseIndexedOperation() Function** (`parser/parser.go`, lines 689-809)\n\nHandles indexed channel operations like `cs[i] ? x` or `cs[i][j] ? x`.\n\n**Receive Handling** (lines 772-804):\n```go\nif p.peekTokenIs(lexer.RECEIVE) {\n\t// Indexed channel receive: cs[i] ? x or cs[i][j] ? x or cs[i] ? CASE ...\n\tp.nextToken() // Move to ?\n\trecvToken := p.curToken\n\n\t// Check for variant receive: cs[i] ? CASE\n\tif p.peekTokenIs(lexer.CASE) {\n\t\tp.nextToken() // Move to CASE\n\t\treturn p.parseVariantReceiveWithIndex(name, indices, recvToken)\n\t}\n\n\tstmt := &ast.Receive{\n\t\tToken: recvToken,\n\t\tChannel: name,\n\t\tChannelIndices: indices, // Store collected indices\n\t}\n\n\t// LINE 789-792: Same expectation - IDENT only\n\tif !p.expectPeek(lexer.IDENT) {\n\t\treturn nil\n\t}\n\tstmt.Variable = p.curToken.Literal\n\n\t// Check for sequential receive\n\tfor p.peekTokenIs(lexer.SEMICOLON) {\n\t\tp.nextToken() // Move to ;\n\t\tif !p.expectPeek(lexer.IDENT) {\n\t\t\treturn nil\n\t\t}\n\t\tstmt.Variables = append(stmt.Variables, p.curToken.Literal)\n\t}\n\n\treturn stmt\n}\n```\n\n**Key Points:**\n- Lines 703-711: Collects multiple indices (`[i]`, `[i][j]`, etc.) into `indices` slice\n- Line 772-774: Detects `?` and saves the token\n- Line 778-780: Checks for variant receive pattern\n- **Line 789-792**: After collecting indices and `?`, expects simple IDENT (no indexed targets)\n- Lines 795-801: Handles sequential receives the same way\n\n---\n\n### 5. **parseAltCase() Function** (`parser/parser.go`, lines 1713-1845)\n\nHandles ALT case parsing, including channel receive cases.\n\n**Three Main Cases:**\n\n**Case A: Simple Channel** (lines 1746-1766):\n```go\nif p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.RECEIVE) {\n\tname := p.curToken.Literal\n\tif p.timerNames[name] {\n\t\t// Timer case: tim ? AFTER deadline\n\t\taltCase.IsTimer = true\n\t\taltCase.Timer = name\n\t\tp.nextToken() // Move to ?\n\t\tif !p.expectPeek(lexer.AFTER) {\n\t\t\treturn nil\n\t\t}\n\t\tp.nextToken() // Move past AFTER\n\t\taltCase.Deadline = p.parseExpression(LOWEST)\n\t} else {\n\t\t// Simple case: channel ? var\n\t\taltCase.Channel = name\n\t\tp.nextToken() // Move to ?\n\t\t// LINE 1762-1765: Expects IDENT, stores as Variable string\n\t\tif !p.expectPeek(lexer.IDENT) {\n\t\t\treturn nil\n\t\t}\n\t\taltCase.Variable = p.curToken.Literal\n\t}\n}\n```\n\n**Case B: Indexed Channel** (lines 1767-1785):\n```go\nelse if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.LBRACKET) {\n\t// Indexed channel case: cs[i] ? var or cs[i][j] ? var\n\tname := p.curToken.Literal\n\taltCase.Channel = name\n\tfor p.peekTokenIs(lexer.LBRACKET) {\n\t\tp.nextToken() // Move to [\n\t\tp.nextToken() // Move past [\n\t\taltCase.ChannelIndices = append(altCase.ChannelIndices, p.parseExpression(LOWEST))\n\t\tif !p.expectPeek(lexer.RBRACKET) {\n\t\t\treturn nil\n\t\t}\n\t}\n\tif !p.expectPeek(lexer.RECEIVE) {\n\t\treturn nil\n\t}\n\t// LINE 1782-1785: Expects IDENT after ], stores as Variable string\n\tif !p.expectPeek(lexer.IDENT) {\n\t\treturn nil\n\t}\n\taltCase.Variable = p.curToken.Literal\n}\n```\n\n**Case C: Guard Expression** (lines 1786-1827):\n```go\nelse {\n\t// Guard followed by & channel ? var, or guard & SKIP\n\tguard := p.parseExpression(LOWEST)\n\taltCase.Guard = guard\n\n\tif !p.peekTokenIs(lexer.AMPERSAND) {\n\t\tp.addError(\"expected & after guard in ALT case\")\n\t\treturn nil\n\t}\n\tp.nextToken() // Move to &\n\tp.nextToken() // Move past &\n\n\tif p.curTokenIs(lexer.SKIP) {\n\t\t// Guarded SKIP: guard & SKIP\n\t\taltCase.IsSkip = true\n\t} else if !p.curTokenIs(lexer.IDENT) {\n\t\tp.addError(fmt.Sprintf(\"expected channel name or SKIP after guard, got %s\", p.curToken.Type))\n\t\treturn nil\n\t} else {\n\t\t// Channel operation after guard\n\t\taltCase.Channel = p.curToken.Literal\n\n\t\tfor p.peekTokenIs(lexer.LBRACKET) {\n\t\t\t// Indexed channel with guard: guard & cs[i] ? var or cs[i][j] ? var\n\t\t\tp.nextToken() // Move to [\n\t\t\tp.nextToken() // Move past [\n\t\t\taltCase.ChannelIndices = append(altCase.ChannelIndices, p.parseExpression(LOWEST))\n\t\t\tif !p.expectPeek(lexer.RBRACKET) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tif !p.expectPeek(lexer.RECEIVE) {\n\t\t\treturn nil\n\t\t}\n\t\t// LINE 1822-1825: Expects IDENT, stores as Variable string\n\t\tif !p.expectPeek(lexer.IDENT) {\n\t\t\treturn nil\n\t\t}\n\t\taltCase.Variable = p.curToken.Literal\n\t}\n}\n```\n\n**Key Points for All Cases:**\n- Lines 1762-1765, 1782-1785, 1822-1825: All expect simple IDENT after `?`\n- AltCase stores only a single `Variable` string (no sequential receive like Receive's `Variables`)\n- Lines 1716-1730: Scoped declarations in `Declarations` determine variable type\n\n---\n\n### 6. **generateReceive() Function** (`codegen/codegen.go`, lines 1337-1367)\n\nConverts `Receive` AST to Go code:\n\n```go\nfunc (g *Generator) generateReceive(recv *ast.Receive) {\n\tchanRef := goIdent(recv.Channel)\n\tif len(recv.ChannelIndices) > 0 {\n\t\tchanRef += g.generateIndicesStr(recv.ChannelIndices)\n\t}\n\n\tif len(recv.Variables) > 0 {\n\t\t// Sequential receive: _tmpN := <-c; x = _tmpN._0; y = _tmpN._1\n\t\ttmpName := fmt.Sprintf(\"_tmp%d\", g.tmpCounter)\n\t\tg.tmpCounter++\n\t\tg.writeLine(fmt.Sprintf(\"%s := <-%s\", tmpName, chanRef))\n\t\tvarRef := goIdent(recv.Variable)\n\t\tif g.refParams[recv.Variable] {\n\t\t\tvarRef = \"*\" + varRef\n\t\t}\n\t\tg.writeLine(fmt.Sprintf(\"%s = %s._0\", varRef, tmpName))\n\t\tfor i, v := range recv.Variables {\n\t\t\tvRef := goIdent(v)\n\t\t\tif g.refParams[v] {\n\t\t\t\tvRef = \"*\" + vRef\n\t\t\t}\n\t\t\tg.writeLine(fmt.Sprintf(\"%s = %s._%d\", vRef, tmpName, i+1))\n\t\t}\n\t} else {\n\t\t// Simple receive: x = <-c\n\t\tvarRef := goIdent(recv.Variable)\n\t\tif g.refParams[recv.Variable] {\n\t\t\tvarRef = \"*\" + varRef\n\t\t}\n\t\tg.writeLine(fmt.Sprintf(\"%s = <-%s\", varRef, chanRef))\n\t}\n}\n```\n\n**Code Generation Logic:**\n\n1. **Channel Reference** (lines 1338-1341):\n - Uses `goIdent()` to escape reserved words and replace dots\n - For indexed channels, appends generated indices string\n\n2. **Sequential Receive** (lines 1343-1359):\n - If `recv.Variables` is not empty, it's a sequential protocol receive\n - Creates temporary variable `_tmp0`, `_tmp1`, etc.\n - Reads into temp: `_tmp0 := <-c`\n - Assigns fields from temp to each variable:\n - First: `x = _tmp0._0`\n - Additional: `y = _tmp0._1`, `z = _tmp0._2`, etc.\n - **Line 1349-1350, 1355-1356**: Checks `g.refParams[variable]` to determine if variable is a reference parameter\n - If yes: prefix with `*` (e.g., `*x = _tmp0._0`)\n\n3. **Simple Receive** (lines 1360-1366):\n - Direct assignment: `x = <-c`\n - **Line 1362-1363**: Applies `*` prefix if variable is a reference parameter\n\n**Reference Parameter Handling:**\n- Line 1349, 1355, 1362: Uses `g.refParams` map (type `map[string]bool`)\n- This is populated during proc signature collection to track non-VAL parameters\n- When receiving into a reference parameter, must dereference the assignment target\n\n---\n\n### 7. **generateAltBlock() Function** (`codegen/codegen.go`, lines 1860-1933)\n\nConverts `AltBlock` to Go `select` statement:\n\n**Structure** (lines 1896-1932):\n```go\ng.writeLine(\"select {\")\nfor i, c := range alt.Cases {\n\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\tif c.IsSkip {\n\t\t// Default case (guarded SKIP)\n\t\tg.write(\"default:\\n\")\n\t} else if c.IsTimer {\n\t\t// Timer case: case <-time.After(...)\n\t\tg.write(\"case <-time.After(time.Duration(\")\n\t\tg.generateExpression(c.Deadline)\n\t\tg.write(\" - int(time.Now().UnixMicro())) * time.Microsecond):\\n\")\n\t} else if c.Guard != nil {\n\t\t// Guarded channel case: case var = <-_altN:\n\t\tg.write(fmt.Sprintf(\"case %s = <-_alt%d:\\n\", goIdent(c.Variable), i))\n\t} else if len(c.ChannelIndices) > 0 {\n\t\t// Indexed channel case: case var = <-ch[i]:\n\t\tg.write(fmt.Sprintf(\"case %s = <-%s\", goIdent(c.Variable), goIdent(c.Channel)))\n\t\tg.generateIndices(c.ChannelIndices)\n\t\tg.write(\":\\n\")\n\t} else {\n\t\t// Simple channel case: case var = <-ch:\n\t\tg.write(fmt.Sprintf(\"case %s = <-%s:\\n\", goIdent(c.Variable), goIdent(c.Channel)))\n\t}\n\tg.indent++\n\t// ... generate body statements ...\n\tg.indent--\n}\ng.writeLine(\"}\")\n```\n\n**Key Points:**\n\n1. **Variable Name Handling** (lines 1906, 1908, 1912):\n - Uses `goIdent(c.Variable)` to escape reserved words\n - Variable is a simple string (no indexed access)\n - Used on the left side of `=` in `case` statement\n\n2. **Guard Handling** (lines 1878-1894, 1905-1906):\n - For guarded cases, generates nil-channel variables: `var _alt0 chan int = nil`\n - Conditionally sets channel: `if guard { _alt0 = ch }`\n - In select: `case var = <-_alt0:` (guard variable name)\n\n3. **Indexed Channel Handling** (lines 1907-1910):\n - For `cs[i] ? var`, generates: `case var = <-ch[i]:`\n - Calls `g.generateIndices(c.ChannelIndices)` to emit index expressions\n\n4. **Type Inference** (lines 1945-1956 in `generateReplicatedAlt`):\n - For replicated ALT, examines `c.Declarations` for type information\n - Looks for `VarDecl` that matches `c.Variable` name\n - Uses matched type for `reflect.Select` slice setup\n\n---\n\n## Summary Table\n\n| Aspect | Receive | AltCase |\n|--------|---------|---------|\n| **Variable Field Type** | `string` (primary) + `[]string` (sequential) | `string` (single only) |\n| **Channel Field** | `string` | `string` |\n| **Indices Field** | `[]Expression` | `[]Expression` |\n| **Parse Location** | `parseReceive()` (line 1274) or `parseIndexedOperation()` (line 772) | `parseAltCase()` (line 1713) |\n| **Variable Parsing** | Line 1291, 1789-1792: expects IDENT only | Lines 1762-1765, 1782-1785, 1822-1825: expects IDENT only |\n| **Sequential Support** | Yes (line 1297-1303) | No (single Variable only) |\n| **Type Declaration** | Implicit from surrounding context | Via `Declarations` field (line 408) |\n| **Codegen** | `generateReceive()` (line 1337) | `generateAltBlock()` (line 1860) |\n| **Reference Param Handling** | Line 1349-1350, 1355-1356, 1362-1363: checks `refParams` | No direct handling (uses Variable string as-is) |\n\n---\n\n## Critical Observations\n\n1. **Variable Target is Always String**: Both `Receive.Variable` and `AltCase.Variable` are strings, not expressions. This means the parser explicitly rejects indexed targets like `c ? arr[i]` or more complex expressions.\n\n2. **Sequential Receives Only in Receive Node**: The `Receive` AST node has a `Variables` array for sequential protocol receives (e.g., `c ? x ; y`), but `AltCase` does not. ALT cases support only single-variable receives.\n\n3. **Type Inference in ALT**: Since `AltCase.Variable` is just a string with no type information, the codegen relies on `AltCase.Declarations` to find type information (for replicated ALT).\n\n4. **Reference Parameter Tracking**: The `generateReceive()` function explicitly checks `g.refParams[recv.Variable]` to determine if the target should be dereferenced (for non-VAL parameters).\n\n5. **goIdent() Function**: Both parser and codegen use `goIdent()` to escape reserved words (e.g., `byte` → `_byte`) and replace dots with underscores."
},
{
"type": "text",
"text": "agentId: aecfdd11ac5d8c274 (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 51690\ntool_uses: 18\nduration_ms: 60499</usage>"
}
][
{
"type": "text",
"text": "Perfect! Now I have all the information I need. Let me compile a comprehensive report with all the findings:\n\n## OCCAM2GO INDEXED OPERATIONS EXPLORATION\n\nI've thoroughly explored the occam2go codebase to understand how INDEXED operations are handled. Here are my detailed findings with exact line numbers and code snippets:\n\n---\n\n### 1. **IndexedAssignment in AST** (`ast/ast.go`, lines 57-67)\n\n```go\n// Assignment represents an assignment: x := 5 or arr[i] := 5 or arr[i][j] := 5 or [arr FROM n FOR m] := value\ntype Assignment struct {\n\tToken lexer.Token // the := token\n\tName string // variable name\n\tIndices []Expression // optional: index expressions for arr[i][j] := x (nil/empty for simple assignments)\n\tSliceTarget *SliceExpr // optional: slice target for [arr FROM n FOR m] := value\n\tValue Expression // the value being assigned\n}\n```\n\n**Key finding:** The `Assignment` node has an `Indices` field (line 61) which is a slice of `Expression` types representing multi-dimensional indices. This handles both single-dimension `arr[i]` and multi-dimension `arr[i][j]` assignments.\n\n---\n\n### 2. **IndexedSend in AST** (`ast/ast.go`, lines 360-371)\n\n```go\n// Send represents a channel send: c ! x or c ! x ; y or c ! tag ; x\ntype Send struct {\n\tToken lexer.Token // the ! token\n\tChannel string // channel name\n\tChannelIndices []Expression // non-empty for cs[i] ! value or cs[i][j] ! value\n\tValue Expression // value to send (simple send, backward compat)\n\tValues []Expression // additional values for sequential sends (c ! x ; y)\n\tVariantTag string // variant tag name for variant sends (c ! tag ; x)\n}\n```\n\n**Key finding:** The `Send` node has a `ChannelIndices` field (line 364) for handling indexed channel sends like `cs[i] ! value` or `cs[i][j] ! value`.\n\n---\n\n### 3. **Receive with ChannelIndices in AST** (`ast/ast.go`, lines 373-383)\n\n```go\n// Receive represents a channel receive: c ? x or c ? x ; y\ntype Receive struct {\n\tToken lexer.Token // the ? token\n\tChannel string // channel name\n\tChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x\n\tVariable string // variable to receive into (simple receive)\n\tVariables []string // additional variables for sequential receives (c ? x ; y)\n}\n```\n\n**Key finding:** The `Receive` node has a `ChannelIndices` field (line 377) mirroring `Send`, handling indexed channel receives.\n\n---\n\n### 4. **MultiAssignTarget with Indices** (`ast/ast.go`, lines 69-74)\n\n```go\n// MultiAssignTarget represents one target in a multi-assignment.\n// Name is always set. Indices is non-empty for indexed targets like arr[i] or arr[i][j].\ntype MultiAssignTarget struct {\n\tName string // variable name\n\tIndices []Expression // optional: index expressions for arr[i][j] (nil/empty for simple ident)\n}\n```\n\n**Key finding:** Multi-assignment targets can have indexed accesses. The `Indices` field enables expressions like `x[0], x[1] := x[1], x[0]`.\n\n---\n\n### 5. **Parser: parseIndexedOperation()** (`parser/parser.go`, lines 689-809)\n\nThis is the **master function** for handling indexed operations at statement level:\n\n```go\nfunc (p *Parser) parseIndexedOperation() ast.Statement {\n\tname := p.curToken.Literal\n\n\tp.nextToken() // move to [\n\tp.nextToken() // move past [\n\tindex := p.parseExpression(LOWEST)\n\n\t// Expect ]\n\tif !p.expectPeek(lexer.RBRACKET) {\n\t\treturn nil\n\t}\n\n\t// Collect additional indices: name[i][j]...\n\tindices := []ast.Expression{index}\n\tfor p.peekTokenIs(lexer.LBRACKET) {\n\t\tp.nextToken() // move to [\n\t\tp.nextToken() // move past [\n\t\tidx := p.parseExpression(LOWEST)\n\t\tif !p.expectPeek(lexer.RBRACKET) {\n\t\t\treturn nil\n\t\t}\n\t\tindices = append(indices, idx)\n\t}\n\n\t// Check what follows ]\n\tif p.peekTokenIs(lexer.COMMA) {\n\t\t// Multi-assignment starting with indexed target: name[index], ... := ...\n\t\tfirstTarget := ast.MultiAssignTarget{Name: name, Indices: indices}\n\t\treturn p.parseMultiAssignmentFrom(firstTarget)\n\t}\n\tif p.peekTokenIs(lexer.ASSIGN) {\n\t\t// Indexed assignment: name[index] := value or name[i][j] := value\n\t\tp.nextToken() // move to :=\n\t\tstmt := &ast.Assignment{\n\t\t\tName: name,\n\t\t\tToken: p.curToken,\n\t\t\tIndices: indices,\n\t\t}\n\t\tp.nextToken() // move past :=\n\t\tstmt.Value = p.parseExpression(LOWEST)\n\t\treturn stmt\n\t}\n\n\tif p.peekTokenIs(lexer.SEND) {\n\t\t// Indexed channel send: cs[i] ! value or cs[i][j] ! value\n\t\tp.nextToken() // move to !\n\t\tsendToken := p.curToken\n\t\tp.nextToken() // move past !\n\n\t\tstmt := &ast.Send{\n\t\t\tToken: sendToken,\n\t\t\tChannel: name,\n\t\t\tChannelIndices: indices,\n\t\t}\n\n\t\t// Check if this is a variant send: first token is an identifier that is a variant tag\n\t\tif p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.SEMICOLON) {\n\t\t\tpossibleTag := p.curToken.Literal\n\t\t\tif p.isVariantTag(possibleTag) {\n\t\t\t\tstmt.VariantTag = possibleTag\n\t\t\t\tp.nextToken() // move to ;\n\t\t\t\tfor p.curTokenIs(lexer.SEMICOLON) {\n\t\t\t\t\tp.nextToken() // move past ;\n\t\t\t\t\tval := p.parseExpression(LOWEST)\n\t\t\t\t\tstmt.Values = append(stmt.Values, val)\n\t\t\t\t}\n\t\t\t\treturn stmt\n\t\t\t}\n\t\t}\n\n\t\tstmt.Value = p.parseExpression(LOWEST)\n\n\t\t// Check for sequential send\n\t\tfor p.peekTokenIs(lexer.SEMICOLON) {\n\t\t\tp.nextToken() // move to ;\n\t\t\tp.nextToken() // move past ;\n\t\t\tval := p.parseExpression(LOWEST)\n\t\t\tstmt.Values = append(stmt.Values, val)\n\t\t}\n\n\t\treturn stmt\n\t}\n\n\tif p.peekTokenIs(lexer.RECEIVE) {\n\t\t// Indexed channel receive: cs[i] ? x or cs[i][j] ? x or cs[i] ? CASE ...\n\t\tp.nextToken() // move to ?\n\t\trecvToken := p.curToken\n\n\t\t// Check for variant receive: cs[i] ? CASE\n\t\tif p.peekTokenIs(lexer.CASE) {\n\t\t\tp.nextToken() // move to CASE\n\t\t\treturn p.parseVariantReceiveWithIndex(name, indices, recvToken)\n\t\t}\n\n\t\tstmt := &ast.Receive{\n\t\t\tToken: recvToken,\n\t\t\tChannel: name,\n\t\t\tChannelIndices: indices,\n\t\t}\n\n\t\tif !p.expectPeek(lexer.IDENT) {\n\t\t\treturn nil\n\t\t}\n\t\tstmt.Variable = p.curToken.Literal\n\n\t\t// Check for sequential receive\n\t\tfor p.peekTokenIs(lexer.SEMICOLON) {\n\t\t\tp.nextToken() // move to ;\n\t\t\tif !p.expectPeek(lexer.IDENT) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tstmt.Variables = append(stmt.Variables, p.curToken.Literal)\n\t\t}\n\n\t\treturn stmt\n\t}\n\n\t// Default: treat as indexed assignment (shouldn't reach here normally)\n\tp.addError(fmt.Sprintf(\"expected :=, !, or ? after %s[...], got %s\", name, p.peekToken.Type))\n\treturn nil\n}\n```\n\n**Function strategy:** Parses name followed by indices in a loop (lines 702-711), then **dispatches** based on what follows the `]`:\n- `COMMA` → multi-assignment with indexed first target\n- `ASSIGN` → indexed assignment\n- `SEND` → indexed channel send\n- `RECEIVE` → indexed channel receive (with variant check)\n\n---\n\n### 6. **parseIndexExpression()** (`parser/parser.go`, lines 811-825)\n\nThis handles **expression-level** index operations (like in `arr[i]` within expressions):\n\n```go\nfunc (p *Parser) parseIndexExpression(left ast.Expression) *ast.IndexExpr {\n\texpr := &ast.IndexExpr{\n\t\tToken: p.curToken,\n\t\tLeft: left,\n\t}\n\n\tp.nextToken() // move past [\n\texpr.Index = p.parseExpression(LOWEST)\n\n\tif !p.expectPeek(lexer.RBRACKET) {\n\t\treturn nil\n\t}\n\n\treturn expr\n}\n```\n\n**Note:** This only handles **single-dimension** indexing (line 818 shows `expr.Index` is a single `Expression`). Multi-dimension indexing at expression level happens through chaining: `arr[i]` returns an `IndexExpr`, and the parser chains another `parseIndexExpression` to it.\n\n---\n\n### 7. **Codegen: generateIndices()** (`codegen/codegen.go`, lines 1270-1277)\n\n```go\n// generateIndices emits [idx1][idx2]... for multi-dimensional index access.\nfunc (g *Generator) generateIndices(indices []ast.Expression) {\n\tfor _, idx := range indices {\n\t\tg.write(\"[\")\n\t\tg.generateExpression(idx)\n\t\tg.write(\"]\")\n\t}\n}\n```\n\n**Simple helper:** Emits multiple `[idx]` segments for multi-dimensional access.\n\n---\n\n### 8. **Codegen: generateIndicesStr()** (`codegen/codegen.go`, lines 1279-1292)\n\n```go\n// generateIndicesStr generates indices into a buffer and returns the string.\nfunc (g *Generator) generateIndicesStr(indices []ast.Expression) string {\n\tvar buf strings.Builder\n\tfor _, idx := range indices {\n\t\tbuf.WriteString(\"[\")\n\t\toldBuilder := g.builder\n\t\tg.builder = strings.Builder{}\n\t\tg.generateExpression(idx)\n\t\tbuf.WriteString(g.builder.String())\n\t\tg.builder = oldBuilder\n\t\tbuf.WriteString(\"]\")\n\t}\n\treturn buf.String()\n}\n```\n\n**Variant of above:** Builds indices into a temporary buffer and returns as a string (for cases where we need the indices without writing directly to the output).\n\n---\n\n### 9. **Codegen: generateSend()** (`codegen/codegen.go`, lines 1294-1335)\n\nKey excerpt showing indexed send handling:\n\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\tg.generateIndices(send.ChannelIndices) // LINE 1297: Emit indices like [i][j]\n\tg.write(\" <- \")\n\t// ... rest of code for protocol handling ...\n}\n```\n\n**Line 1297:** Directly calls `generateIndices()` to emit `cs[i]` or `cs[i][j]` based on `ChannelIndices`.\n\n---\n\n### 10. **Codegen: generateReceive()** (`codegen/codegen.go`, lines 1337-1367)\n\n```go\nfunc (g *Generator) generateReceive(recv *ast.Receive) {\n\tchanRef := goIdent(recv.Channel)\n\tif len(recv.ChannelIndices) > 0 {\n\t\tchanRef += g.generateIndicesStr(recv.ChannelIndices) // LINE 1340: Build indexed channel ref\n\t}\n\n\tif len(recv.Variables) > 0 {\n\t\t// Sequential receive: _tmpN := <-c; x = _tmpN._0; y = _tmpN._1\n\t\ttmpName := fmt.Sprintf(\"_tmp%d\", g.tmpCounter)\n\t\tg.tmpCounter++\n\t\tg.writeLine(fmt.Sprintf(\"%s := <-%s\", tmpName, chanRef))\n\t\tvarRef := goIdent(recv.Variable)\n\t\tif g.refParams[recv.Variable] {\n\t\t\tvarRef = \"*\" + varRef\n\t\t}\n\t\tg.writeLine(fmt.Sprintf(\"%s = %s._0\", varRef, tmpName))\n\t\tfor i, v := range recv.Variables {\n\t\t\tvRef := goIdent(v)\n\t\t\tif g.refParams[v] {\n\t\t\t\tvRef = \"*\" + vRef\n\t\t\t}\n\t\t\tg.writeLine(fmt.Sprintf(\"%s = %s._%d\", vRef, tmpName, i+1))\n\t\t}\n\t} else {\n\t\tvarRef := goIdent(recv.Variable)\n\t\tif g.refParams[recv.Variable] {\n\t\t\tvarRef = \"*\" + varRef\n\t\t}\n\t\tg.writeLine(fmt.Sprintf(\"%s = <-%s\", varRef, chanRef)) // LINE 1365: Generate simple indexed receive\n\t}\n}\n```\n\n**Critical pattern:**\n- **Line 1340:** If indices exist, appends them to channel reference via `generateIndicesStr()`\n- **Line 1347/1365:** Generates receive statements using the indexed channel reference\n- Handles both simple receives (`x = <-cs[i]`) and sequential receives with temporary variables\n\n---\n\n### 11. **Codegen: generateAltBlock()** (`codegen/codegen.go`, lines 1860-1933)\n\nKey excerpt showing how ALT cases handle indexed receives:\n\n```go\nfunc (g *Generator) generateAltBlock(alt *ast.AltBlock) {\n\t// ... guard setup code ...\n\n\tg.writeLine(\"select {\")\n\tfor i, c := range alt.Cases {\n\t\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\t\tif c.IsSkip {\n\t\t\tg.write(\"default:\\n\")\n\t\t} else if c.IsTimer {\n\t\t\t// ... timer case ...\n\t\t} else if c.Guard != nil {\n\t\t\tg.write(fmt.Sprintf(\"case %s = <-_alt%d:\\n\", goIdent(c.Variable), i))\n\t\t} else if len(c.ChannelIndices) > 0 {\n\t\t\t// LINE 1908-1910: Indexed receive in ALT\n\t\t\tg.write(fmt.Sprintf(\"case %s = <-%s\", goIdent(c.Variable), goIdent(c.Channel)))\n\t\t\tg.generateIndices(c.ChannelIndices)\n\t\t\tg.write(\":\\n\")\n\t\t} else {\n\t\t\tg.write(fmt.Sprintf(\"case %s = <-%s:\\n\", goIdent(c.Channel)))\n\t\t}\n\t\tg.indent++\n\t\t// ... body execution ...\n\t\tg.indent--\n\t}\n\tg.writeLine(\"}\")\n}\n```\n\n**Lines 1907-1910:** Shows how indexed ALT cases work:\n- For non-guarded cases with indices, emit: `case varName = <-chanName[idx1][idx2]:`\n- The receive **variable** is on the left of `=`\n- The **indexed channel reference** is on the right after `<-`\n\n---\n\n### 12. **Codegen: generateAssignment()** (`codegen/codegen.go`, lines 1696-1748)\n\nKey excerpt for indexed assignments:\n\n```go\nfunc (g *Generator) generateAssignment(assign *ast.Assignment) {\n\tg.builder.WriteString(strings.Repeat(\"\\t\", g.indent))\n\n\tif assign.SliceTarget != nil {\n\t\t// ... slice assignment code ...\n\t}\n\n\tif len(assign.Indices) > 0 {\n\t\t// Check if this is a record field access (single index that is an identifier)\n\t\tif len(assign.Indices) == 1 {\n\t\t\tif _, ok := g.recordVars[assign.Name]; ok {\n\t\t\t\tif ident, ok := assign.Indices[0].(*ast.Identifier); ok {\n\t\t\t\t\t// Record field: p.x = value (Go auto-dereferences pointers)\n\t\t\t\t\tg.write(goIdent(assign.Name))\n\t\t\t\t\tg.write(\".\")\n\t\t\t\t\tg.write(goIdent(ident.Value))\n\t\t\t\t\tg.write(\" = \")\n\t\t\t\t\tg.generateExpression(assign.Value)\n\t\t\t\t\tg.write(\"\\n\")\n\t\t\t\t\treturn\n\t\t\t\t}\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.generateIndices(assign.Indices) // LINE 1737: Emit indices\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\n**Lines 1716-1737:** Handles indexed assignments:\n- Checks for record field access first (single index that's an identifier)\n- Falls through to array indexing\n- Uses `generateIndices()` to emit `[i][j]...` segments\n\n---\n\n### 13. **Codegen: generateMultiAssignment()** (`codegen/codegen.go`, lines 2335-2376)\n\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 len(target.Indices) > 0 {\n\t\t\t// Check if this is a record field access (single index that is an identifier)\n\t\t\tif len(target.Indices) == 1 {\n\t\t\t\tif _, ok := g.recordVars[target.Name]; ok {\n\t\t\t\t\tif ident, ok := target.Indices[0].(*ast.Identifier); ok {\n\t\t\t\t\t\tg.write(goIdent(target.Name))\n\t\t\t\t\t\tg.write(\".\")\n\t\t\t\t\t\tg.write(goIdent(ident.Value))\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\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.generateIndices(target.Indices) // LINE 2360: Emit indices for target\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\n**Line 2360:** Similar to indexed assignment, uses `generateIndices()` to emit indexed targets in multi-assignment.\n\n---\n\n### 14. **Test Cases Demonstrating Indexed Operations**\n\nFrom `codegen/codegen_test.go`:\n\n**Line 194-202 (TestIndexedAssignment):**\n```go\nfunc TestIndexedAssignment(t *testing.T) {\n\tinput := `arr[2] := 10\n`\n\toutput := transpile(t, input)\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\n**Line 595-603 (TestIndexedSendGen):**\n```go\nfunc TestIndexedSendGen(t *testing.T) {\n\tinput := `cs[0] ! 42\n`\n\toutput := transpile(t, input)\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\n**Line 605-613 (TestIndexedReceiveGen):**\n```go\nfunc TestIndexedReceiveGen(t *testing.T) {\n\tinput := `cs[0] ? x\n`\n\toutput := transpile(t, input)\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\n**Line 730-737 (TestMultiAssignmentIndexed):**\n```go\nfunc TestMultiAssignmentIndexed(t *testing.T) {\n\tinput := `x[0], x[1] := x[1], x[0]\n`\n\toutput := transpile(t, input)\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\n**Line 819-826 (TestMultiDimSendCodegen):**\n```go\ninput := `cs[i][j] ! 42\n`\noutput := transpile(t, input)\nif !strings.Contains(output, \"cs[i][j] <- 42\") {\n\tt.Errorf(\"expected 'cs[i][j] <- 42' in output, got:\\n%s\", output)\n}\n```\n\n**Line 829-835 (TestMultiDimReceiveCodegen):**\n```go\ninput := `cs[i][j] ? x\n`\noutput := transpile(t, input)\nif !strings.Contains(output, \"x = <-cs[i][j]\") {\n\tt.Errorf(\"expected 'x = <-cs[i][j]' in output, got:\\n%s\", output)\n}\n```\n\n---\n\n## Summary Table\n\n| Component | Location | Key Field/Function | Purpose |\n|-----------|----------|-------------------|---------|\n| **AST: Simple Assignment** | `ast.go:57-67` | `Assignment.Indices` | Multi-dim array assign |\n| **AST: Indexed Send** | `ast.go:360-371` | `Send.ChannelIndices` | `cs[i][j] ! value` |\n| **AST: Indexed Receive** | `ast.go:373-383` | `Receive.ChannelIndices` | `cs[i][j] ? x` |\n| **AST: Multi-assign Target** | `ast.go:69-74` | `MultiAssignTarget.Indices` | `x[0], x[1] := ...` |\n| **Parser: Dispatcher** | `parser.go:689-809` | `parseIndexedOperation()` | Routes based on operator |\n| **Parser: Expression Index** | `parser.go:811-825` | `parseIndexExpression()` | Single-dim expr indexing |\n| **Codegen: Index Generator** | `codegen.go:1270-1277` | `generateIndices()` | Emits `[i][j]...` |\n| **Codegen: Index String** | `codegen.go:1279-1292` | `generateIndicesStr()` | Returns indices as string |\n| **Codegen: Send** | `codegen.go:1294-1335` | Line 1297 | Uses `generateIndices()` |\n| **Codegen: Receive** | `codegen.go:1337-1367` | Lines 1340, 1365 | Builds indexed chan ref |\n| **Codegen: ALT** | `codegen.go:1860-1933` | Lines 1907-1910 | Handles indexed ALT cases |\n| **Codegen: Assignment** | `codegen.go:1696-1748` | Line 1737 | Indexed assign generation |\n| **Codegen: Multi-assign** | `codegen.go:2335-2376` | Line 2360 | Indexed target generation |\n\n---\n\n## Key Patterns\n\n1. **Parser-level dispatch:** `parseIndexedOperation()` is called when parser sees `IDENT` followed by `LBRACKET` at statement level, and it dispatches to different AST node types based on what follows the `]`.\n\n2. **Multi-dimensional support:** Indices are stored in **slices** (`[]Expression`), allowing arbitrary nesting like `cs[i][j][k]`.\n\n3. **Receive variable placement:** In receive operations, the receive **variable** is separate from `ChannelIndices`. The variable is assigned the value from the indexed channel: `x = <-cs[i]`.\n\n4. **ALT case handling:** ALT cases store both indexed channel and receive variable separately, enabling correct code generation: `case x = <-cs[i]:`.\n\n5. **Record field vs array index:** Special handling for single indices on record types to generate dot notation (`p.x`) instead of bracket notation (`p[x]`)."
},
{
"type": "text",
"text": "agentId: a05be3d591f82497c (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 55427\ntool_uses: 32\nduration_ms: 83171</usage>"
}
]Now I have a thorough understanding. Let me launch a Plan agent to design the implementation.
{
"prompt": "Design an implementation plan for supporting receive into indexed target variables in the occam2go transpiler (issue #85).\n\n## Problem\nChannel receive (`ch ? var`) currently only supports simple identifiers as the target variable. Indexed targets like `flags[0]` or `state.of.neighbour[d]` cause parse failures. The same limitation affects ALT case parsing.\n\n## Current State\n\n### AST\n- `Receive` node (`ast/ast.go:373-383`): Has `Variable string` and `Variables []string` for sequential receives. Has `ChannelIndices []Expression` for indexed channels.\n- `AltCase` node (`ast/ast.go:397-409`): Has `Variable string`. Has `ChannelIndices []Expression` for indexed channels.\n\n### Parser\nThree places parse receive target variables, all using `p.expectPeek(lexer.IDENT)`:\n\n1. **`parseReceive()`** (parser.go:1291): After `?`, expects IDENT only for `Variable`.\n2. **`parseIndexedOperation()`** (parser.go:789): After `cs[i] ?`, expects IDENT only.\n3. **`parseAltCase()`** (parser.go): Three sub-cases, all use `expectPeek(IDENT)`:\n - Simple channel (line 1762): `ch ? var`\n - Indexed channel (line 1782): `cs[i] ? var`\n - Guarded (line 1822): `guard & ch ? var`\n\n### Codegen\n- **`generateReceive()`** (codegen.go:1337-1367): Uses `goIdent(recv.Variable)` as a string. Checks `g.refParams[recv.Variable]` for pointer dereference.\n- **`generateAltBlock()`** (codegen.go:1860-1933): Uses `goIdent(c.Variable)` in `case var = <-ch:` patterns. Lines 1906, 1908, 1912.\n- **`generateReplicatedAlt()`**: Uses `c.Variable` for type inference from `c.Declarations`.\n\n### Existing patterns for indexed targets\n- `Assignment` node has `Indices []Expression` alongside `Name string`\n- `MultiAssignTarget` has `Name string` + `Indices []Expression`\n- `generateAssignment()` uses `g.generateIndices(assign.Indices)` to emit `[i][j]...`\n- `generateIndices()` and `generateIndicesStr()` are helper functions that emit index expressions\n\n## Design Considerations\n\n1. **AST approach**: Add `VariableIndices []Expression` to both `Receive` and `AltCase`, mirroring the pattern used by `Assignment.Indices` and `Send.ChannelIndices`. This is minimal and consistent with existing patterns. The alternative (changing `Variable` to an `Expression`) would be a much larger refactor touching many more code paths.\n\n2. **Parser approach**: After parsing the IDENT for the variable name, check if the next token is `LBRACKET` and if so, collect index expressions in a loop (same pattern used for channel indices in `parseIndexedOperation()` and `parseAltCase()`).\n\n3. **Sequential receives with indexed targets**: `c ? arr[0] ; arr[1]` - the `Variables []string` field on `Receive` would also need indices. This is more complex. However, looking at the issue examples (`link[0] ? flags[0]`), only the primary variable needs indexing. Sequential receives with indexed secondary targets can be deferred.\n\n4. **Codegen**: Build a variable reference string using `goIdent(name) + generateIndicesStr(indices)` instead of just `goIdent(name)`. The `refParams` check should still use just the variable name (the base name, not the indexed form).\n\n5. **ALT codegen**: Multiple places in `generateAltBlock()` and `generateReplicatedAlt()` emit `goIdent(c.Variable)` and need to append indices.\n\nPlease design a detailed, step-by-step plan covering AST changes, parser changes, codegen changes, and tests. Use the exact file paths and line numbers. Follow existing patterns closely.",
"subagent_type": "Plan"
}[
{
"type": "text",
"text": "I now have a thorough understanding of the entire codebase relevant to this issue. Let me compile the detailed implementation plan.\n\n---\n\n## Implementation Plan: Receive into Indexed Target Variables (Issue #85)\n\n### Overview\n\nThe change adds support for indexed target variables in channel receive operations. Currently `ch ? flags[0]` and `cs[i] ? state[d]` cause parse failures because the parser expects a bare `IDENT` after `?`. The fix follows the established `Assignment.Indices` pattern: add a `VariableIndices []Expression` field to `Receive` and `AltCase` AST nodes, parse bracket indices after the variable identifier, and emit them in codegen using the existing `generateIndicesStr` helper.\n\n### Step 1: AST Changes (`/home/david/projects/code-associates/occam2go/ast/ast.go`)\n\n**1a. Add `VariableIndices` to `Receive` (line 378-379)**\n\nAdd a new field `VariableIndices []Expression` to the `Receive` struct, placed after `Variable`:\n\n```go\ntype Receive struct {\n Token lexer.Token // the ? token\n Channel string // channel name\n ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x\n Variable string // variable to receive into (simple receive)\n VariableIndices []Expression // non-empty for ch ? arr[i] or ch ? arr[i][j]\n Variables []string // additional variables for sequential receives (c ? x ; y)\n}\n```\n\n**1b. Add `VariableIndices` to `AltCase` (line 402)**\n\nAdd a new field `VariableIndices []Expression` to the `AltCase` struct, placed after `Variable`:\n\n```go\ntype AltCase struct {\n Guard Expression // optional guard condition (nil if no guard)\n Channel string // channel name\n ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT\n Variable string // variable to receive into\n VariableIndices []Expression // non-empty for ch ? arr[i] or ch ? arr[i][j]\n Body []Statement // the body to execute\n IsTimer bool // true if this is a timer AFTER case\n IsSkip bool // true if this is a guarded SKIP case (guard & SKIP)\n Timer string // timer name (when IsTimer)\n Deadline Expression // AFTER deadline expression (when IsTimer)\n Declarations []Statement // scoped declarations before channel input (e.g., BYTE ch:)\n}\n```\n\nThe naming `VariableIndices` mirrors `ChannelIndices` and follows the same `[]Expression` pattern used by `Assignment.Indices` and `MultiAssignTarget.Indices`.\n\n### Step 2: Parser Changes (`/home/david/projects/code-associates/occam2go/parser/parser.go`)\n\nThe pattern for parsing indices after an identifier is already established. After consuming `IDENT` for the variable name, check if `peekTokenIs(lexer.LBRACKET)` and if so, loop to collect index expressions -- exactly like the channel index parsing in `parseIndexedOperation` (lines 703-711) and `parseAltCase` (lines 1771-1778, 1809-1816).\n\n**2a. `parseReceive()` (line 1291-1294)**\n\nCurrently:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\nstmt.Variable = p.curToken.Literal\n```\n\nChange to:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\nstmt.Variable = p.curToken.Literal\n\n// Check for indexed variable: ch ? arr[i] or ch ? arr[i][j]\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken() // move to [\n p.nextToken() // move past [\n stmt.VariableIndices = append(stmt.VariableIndices, p.parseExpression(LOWEST))\n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n}\n```\n\n**2b. `parseIndexedOperation()` - indexed channel receive (line 789-792)**\n\nCurrently:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\nstmt.Variable = p.curToken.Literal\n```\n\nChange to:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\nstmt.Variable = p.curToken.Literal\n\n// Check for indexed variable: cs[i] ? arr[j]\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken() // move to [\n p.nextToken() // move past [\n stmt.VariableIndices = append(stmt.VariableIndices, p.parseExpression(LOWEST))\n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n}\n```\n\n**2c. `parseAltCase()` - simple channel case (line 1762-1765)**\n\nCurrently:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\naltCase.Variable = p.curToken.Literal\n```\n\nChange to:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\naltCase.Variable = p.curToken.Literal\n\n// Check for indexed variable: ch ? arr[i]\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken() // move to [\n p.nextToken() // move past [\n altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))\n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n}\n```\n\n**2d. `parseAltCase()` - indexed channel case (line 1782-1785)**\n\nCurrently:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\naltCase.Variable = p.curToken.Literal\n```\n\nChange to:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\naltCase.Variable = p.curToken.Literal\n\n// Check for indexed variable: cs[i] ? arr[j]\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken() // move to [\n p.nextToken() // move past [\n altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))\n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n}\n```\n\n**2e. `parseAltCase()` - guarded channel case (line 1822-1825)**\n\nCurrently:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\naltCase.Variable = p.curToken.Literal\n```\n\nChange to:\n```go\nif !p.expectPeek(lexer.IDENT) {\n return nil\n}\naltCase.Variable = p.curToken.Literal\n\n// Check for indexed variable: guard & ch ? arr[i]\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken() // move to [\n p.nextToken() // move past [\n altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))\n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n}\n```\n\n**Parser helper consideration**: The index-parsing loop is identical in all 5 locations. An optional refactoring is to extract a helper like `parseVariableIndices() []ast.Expression`, but since this is a 5-line pattern and matches the existing inline style for channel indices, keeping it inline is consistent with the codebase convention.\n\n### Step 3: Codegen Changes (`/home/david/projects/code-associates/occam2go/codegen/codegen.go`)\n\n**3a. `generateReceive()` - simple receive path (lines 1360-1366)**\n\nCurrently:\n```go\n} else {\n varRef := goIdent(recv.Variable)\n if g.refParams[recv.Variable] {\n varRef = \"*\" + varRef\n }\n g.writeLine(fmt.Sprintf(\"%s = <-%s\", varRef, chanRef))\n}\n```\n\nChange to:\n```go\n} else {\n varRef := goIdent(recv.Variable)\n if g.refParams[recv.Variable] {\n varRef = \"*\" + varRef\n }\n if len(recv.VariableIndices) > 0 {\n varRef += g.generateIndicesStr(recv.VariableIndices)\n }\n g.writeLine(fmt.Sprintf(\"%s = <-%s\", varRef, chanRef))\n}\n```\n\nNote: The `refParams` check uses the base variable name (`recv.Variable`), not the indexed form. This is correct because Go pointer dereference `*arr` occurs on the base variable, and array indexing follows: `(*arr)[i]`. However, for array variables that are slices (not pointers), the `refParams` check will be false and indexing works directly: `arr[i]`. In practice, indexed receives are almost always into array elements (slices), not pointer-dereferenced targets, so the existing `refParams` logic is adequate. If the base variable is a ref param and has indices, the generated code would be `*arr[i]` which is wrong Go syntax for `(*arr)[i]` -- but this edge case (receiving into an indexed element of a pointer-passed variable) is extremely unlikely in occam code since arrays passed by reference are already slices. We can leave this as-is for now and note it as a future edge case if needed.\n\n**3b. `generateReceive()` - sequential receive path (lines 1343-1359)**\n\nCurrently the sequential receive uses `goIdent(recv.Variable)` for the first variable and `goIdent(v)` for subsequent `Variables`. The primary variable needs indices support:\n\n```go\nvarRef := goIdent(recv.Variable)\nif g.refParams[recv.Variable] {\n varRef = \"*\" + varRef\n}\nif len(recv.VariableIndices) > 0 {\n varRef += g.generateIndicesStr(recv.VariableIndices)\n}\ng.writeLine(fmt.Sprintf(\"%s = %s._0\", varRef, tmpName))\n```\n\nThe secondary `Variables []string` in sequential receives do not get indices support in this change. Supporting `c ? arr[0] ; arr[1]` with separate indices per secondary variable would require changing `Variables []string` to a struct with both name and indices. This is a separate, more complex change and can be deferred. The typical use case from the issue (`link[0] ? flags[0]`) only involves the primary variable.\n\n**3c. `generateAltBlock()` - simple channel case (line 1912)**\n\nCurrently:\n```go\ng.write(fmt.Sprintf(\"case %s = <-%s:\\n\", goIdent(c.Variable), goIdent(c.Channel)))\n```\n\nChange to:\n```go\nvarRef := goIdent(c.Variable)\nif len(c.VariableIndices) > 0 {\n varRef += g.generateIndicesStr(c.VariableIndices)\n}\ng.write(fmt.Sprintf(\"case %s = <-%s:\\n\", varRef, goIdent(c.Channel)))\n```\n\n**3d. `generateAltBlock()` - indexed channel case (lines 1908-1910)**\n\nCurrently:\n```go\ng.write(fmt.Sprintf(\"case %s = <-%s\", goIdent(c.Variable), goIdent(c.Channel)))\ng.generateIndices(c.ChannelIndices)\ng.write(\":\\n\")\n```\n\nChange to:\n```go\nvarRef := goIdent(c.Variable)\nif len(c.VariableIndices) > 0 {\n varRef += g.generateIndicesStr(c.VariableIndices)\n}\ng.write(fmt.Sprintf(\"case %s = <-%s\", varRef, goIdent(c.Channel)))\ng.generateIndices(c.ChannelIndices)\ng.write(\":\\n\")\n```\n\n**3e. `generateAltBlock()` - guarded case (line 1906)**\n\nCurrently:\n```go\ng.write(fmt.Sprintf(\"case %s = <-_alt%d:\\n\", goIdent(c.Variable), i))\n```\n\nChange to:\n```go\nvarRef := goIdent(c.Variable)\nif len(c.VariableIndices) > 0 {\n varRef += g.generateIndicesStr(c.VariableIndices)\n}\ng.write(fmt.Sprintf(\"case %s = <-_alt%d:\\n\", varRef, i))\n```\n\n**3f. `generateReplicatedAlt()` (line 2043)**\n\nCurrently:\n```go\ng.writeLine(fmt.Sprintf(\"%s = _altValue.Interface().(%s)\", goIdent(c.Variable), recvType))\n```\n\nChange to:\n```go\nvarRef := goIdent(c.Variable)\nif len(c.VariableIndices) > 0 {\n varRef += g.generateIndicesStr(c.VariableIndices)\n}\ng.writeLine(fmt.Sprintf(\"%s = _altValue.Interface().(%s)\", varRef, recvType))\n```\n\n**Codegen helper consideration**: The pattern `varRef := goIdent(c.Variable); if len(c.VariableIndices) > 0 { varRef += g.generateIndicesStr(c.VariableIndices) }` repeats across all codegen sites. An optional refactoring is to add a small helper method:\n\n```go\nfunc (g *Generator) varRefStr(name string, indices []ast.Expression) string {\n ref := goIdent(name)\n if len(indices) > 0 {\n ref += g.generateIndicesStr(indices)\n }\n return ref\n}\n```\n\nThis would reduce duplication. However, it is not strictly necessary since each site also potentially checks `refParams`, so the logic is not perfectly uniform. I would suggest adding the helper only for the non-refParams sites (ALT cases), and inlining for the `generateReceive` sites where `refParams` is also checked. Either approach is reasonable.\n\n### Step 4: Tests\n\n**4a. Parser unit tests (`/home/david/projects/code-associates/occam2go/parser/parser_test.go`)**\n\nAdd after the existing `TestReceive` (line 272):\n\n```go\nfunc TestReceiveIndexedVariable(t *testing.T) {\n input := `ch ? flags[0]\n`\n l := lexer.New(input)\n p := New(l)\n program := p.ParseProgram()\n checkParserErrors(t, p)\n\n if len(program.Statements) != 1 {\n t.Fatalf(\"expected 1 statement, got %d\", len(program.Statements))\n }\n\n recv, ok := program.Statements[0].(*ast.Receive)\n if !ok {\n t.Fatalf(\"expected Receive, got %T\", program.Statements[0])\n }\n\n if recv.Channel != \"ch\" {\n t.Errorf(\"expected channel 'ch', got %s\", recv.Channel)\n }\n if recv.Variable != \"flags\" {\n t.Errorf(\"expected variable 'flags', got %s\", recv.Variable)\n }\n if len(recv.VariableIndices) != 1 {\n t.Fatalf(\"expected 1 variable index, got %d\", len(recv.VariableIndices))\n }\n}\n\nfunc TestReceiveMultiIndexedVariable(t *testing.T) {\n input := `ch ? grid[i][j]\n`\n l := lexer.New(input)\n p := New(l)\n program := p.ParseProgram()\n checkParserErrors(t, p)\n\n recv := program.Statements[0].(*ast.Receive)\n if recv.Variable != \"grid\" {\n t.Errorf(\"expected variable 'grid', got %s\", recv.Variable)\n }\n if len(recv.VariableIndices) != 2 {\n t.Fatalf(\"expected 2 variable indices, got %d\", len(recv.VariableIndices))\n }\n}\n\nfunc TestIndexedChannelReceiveIndexedVariable(t *testing.T) {\n input := `cs[0] ? flags[1]\n`\n l := lexer.New(input)\n p := New(l)\n program := p.ParseProgram()\n checkParserErrors(t, p)\n\n recv := program.Statements[0].(*ast.Receive)\n if recv.Channel != \"cs\" {\n t.Errorf(\"expected channel 'cs', got %s\", recv.Channel)\n }\n if len(recv.ChannelIndices) != 1 {\n t.Fatalf(\"expected 1 channel index, got %d\", len(recv.ChannelIndices))\n }\n if recv.Variable != \"flags\" {\n t.Errorf(\"expected variable 'flags', got %s\", recv.Variable)\n }\n if len(recv.VariableIndices) != 1 {\n t.Fatalf(\"expected 1 variable index, got %d\", len(recv.VariableIndices))\n }\n}\n```\n\n**4b. Codegen unit tests (`/home/david/projects/code-associates/occam2go/codegen/codegen_test.go`)**\n\nAdd after the existing `TestIndexedReceiveGen` (line 613):\n\n```go\nfunc TestIndexedVariableReceiveGen(t *testing.T) {\n input := `ch ? flags[0]\n`\n output := transpile(t, input)\n if !strings.Contains(output, \"flags[0] = <-ch\") {\n t.Errorf(\"expected 'flags[0] = <-ch' in output, got:\\n%s\", output)\n }\n}\n\nfunc TestIndexedChannelIndexedVariableReceiveGen(t *testing.T) {\n input := `cs[i] ? flags[j]\n`\n output := transpile(t, input)\n if !strings.Contains(output, \"flags[j] = <-cs[i]\") {\n t.Errorf(\"expected 'flags[j] = <-cs[i]' in output, got:\\n%s\", output)\n }\n}\n\nfunc TestMultiDimIndexedVariableReceiveGen(t *testing.T) {\n input := `ch ? grid[i][j]\n`\n output := transpile(t, input)\n if !strings.Contains(output, \"grid[i][j] = <-ch\") {\n t.Errorf(\"expected 'grid[i][j] = <-ch' in output, got:\\n%s\", output)\n }\n}\n```\n\n**4c. E2E tests (`/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go`)**\n\nAdd an end-to-end test that exercises the full pipeline (transpile, compile, run):\n\n```go\nfunc TestE2E_ReceiveIntoIndexedVariable(t *testing.T) {\n // Test receiving into an indexed array variable\n occam := `SEQ\n CHAN OF INT c:\n [3]INT flags:\n PAR\n c ! 42\n SEQ\n c ? flags[1]\n print.int(flags[1])\n`\n output := transpileCompileRun(t, occam)\n expected := \"42\\n\"\n if output != expected {\n t.Errorf(\"expected %q, got %q\", expected, output)\n }\n}\n\nfunc TestE2E_AltReceiveIntoIndexedVariable(t *testing.T) {\n // Test ALT receiving into an indexed array variable\n occam := `SEQ\n CHAN OF INT c:\n [3]INT results:\n PAR\n c ! 99\n ALT\n c ? results[0]\n print.int(results[0])\n`\n output := transpileCompileRun(t, occam)\n expected := \"99\\n\"\n if output != expected {\n t.Errorf(\"expected %q, got %q\", expected, output)\n }\n}\n\nfunc TestE2E_IndexedChannelReceiveIntoIndexedVariable(t *testing.T) {\n // Test indexed channel receiving into indexed variable: cs[i] ? arr[j]\n occam := `SEQ\n [2]CHAN OF INT cs:\n [2]INT flags:\n PAR\n cs[0] ! 77\n SEQ\n cs[0] ? flags[1]\n print.int(flags[1])\n`\n output := transpileCompileRun(t, occam)\n expected := \"77\\n\"\n if output != expected {\n t.Errorf(\"expected %q, got %q\", expected, output)\n }\n}\n```\n\n### Step 5: Documentation Updates\n\n**5a. `CLAUDE.md`** - Add to the Occam-to-Go mapping table:\n\n| `ch ? arr[i]` (indexed receive target) | `arr[i] = <-ch` |\n\n**5b. `TODO.md`** - Mark the indexed receive target feature as implemented.\n\n### Sequencing and Dependencies\n\n1. **AST first** (Step 1) -- both `Receive` and `AltCase` get `VariableIndices`. No behavioral change.\n2. **Parser second** (Step 2) -- all 5 parse sites get the index-collection loop. The new AST fields are populated.\n3. **Codegen third** (Step 3) -- all 6 generation sites emit the variable indices. Uses existing `generateIndicesStr`.\n4. **Tests fourth** (Step 4) -- parser, codegen unit, and e2e tests validate the full pipeline.\n5. **Docs last** (Step 5) -- update CLAUDE.md and TODO.md.\n\n### Potential Challenges\n\n1. **Sequential receive with indexed secondary targets**: `c ? arr[0] ; arr[1]` would require changing `Variables []string` to a slice of structs. This is explicitly out of scope for this change. The sequential receive loop (lines 1296-1303) will continue to expect bare `IDENT` for secondary variables. If a user writes `c ? arr[0] ; arr[1]`, the parse will succeed for `arr[0]` (consuming the index), then fail on `;` because the parser will be past the index tokens. This is acceptable -- it will produce a parse error, and the feature can be added later.\n\n2. **Expression precedence in index**: The `parseExpression(LOWEST)` call inside brackets is the same pattern used everywhere for channel indices and works correctly for expressions like `d + 1` or `i * 2` inside brackets.\n\n3. **Interaction with record field access**: Record field access in occam uses bracket syntax too: `p[x]` means `p.x`. The parser at the receive-variable sites will collect these as indices. In codegen, the `generateIndicesStr` function will produce `[x]` not `.x`. However, receiving into a record field (`ch ? p[x]`) is not a typical occam pattern since record fields are not channel-typed. If needed in the future, the codegen could check `g.recordVars` for the variable name -- but this is out of scope.\n\n4. **`refParams` interaction with indices**: As noted in 3a, if a variable is both a ref param and has indices, the generated code `*arr[i]` is Go for `*(arr[i])`, not `(*arr)[i]`. For array parameters, occam maps them to Go slices (not pointers), so `refParams` would be false. This is not a concern in practice.\n\n### Critical Files for Implementation\n- `/home/david/projects/code-associates/occam2go/ast/ast.go` - Add `VariableIndices []Expression` field to `Receive` and `AltCase` structs\n- `/home/david/projects/code-associates/occam2go/parser/parser.go` - Add index parsing after variable IDENT in 5 locations: `parseReceive`, `parseIndexedOperation`, and 3 branches of `parseAltCase`\n- `/home/david/projects/code-associates/occam2go/codegen/codegen.go` - Emit variable indices in 6 locations: `generateReceive` (2 paths), `generateAltBlock` (3 case types), `generateReplicatedAlt` (1 path)\n- `/home/david/projects/code-associates/occam2go/parser/parser_test.go` - Add parser unit tests for indexed receive targets\n- `/home/david/projects/code-associates/occam2go/codegen/codegen_test.go` - Add codegen unit tests for indexed variable receive generation"
},
{
"type": "text",
"text": "agentId: a53979daf9293938b (for resuming to continue this agent's work if needed)\n<usage>total_tokens: 52510\ntool_uses: 39\nduration_ms: 189597</usage>"
}
]{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1895,
"limit": 45
} 1895→
1896→ g.writeLine("select {")
1897→ for i, c := range alt.Cases {
1898→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1899→ if c.IsSkip {
1900→ g.write("default:\n")
1901→ } else if c.IsTimer {
1902→ g.write("case <-time.After(time.Duration(")
1903→ g.generateExpression(c.Deadline)
1904→ g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
1905→ } else if c.Guard != nil {
1906→ g.write(fmt.Sprintf("case %s = <-_alt%d:\n", goIdent(c.Variable), i))
1907→ } else if len(c.ChannelIndices) > 0 {
1908→ g.write(fmt.Sprintf("case %s = <-%s", goIdent(c.Variable), goIdent(c.Channel)))
1909→ g.generateIndices(c.ChannelIndices)
1910→ g.write(":\n")
1911→ } else {
1912→ g.write(fmt.Sprintf("case %s = <-%s:\n", goIdent(c.Variable), goIdent(c.Channel)))
1913→ }
1914→ g.indent++
1915→ guardedSkip := c.IsSkip && c.Guard != nil
1916→ if guardedSkip {
1917→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1918→ g.write("if ")
1919→ g.generateExpression(c.Guard)
1920→ g.write(" {\n")
1921→ g.indent++
1922→ }
1923→ for _, s := range c.Body {
1924→ g.generateStatement(s)
1925→ }
1926→ if guardedSkip {
1927→ g.indent--
1928→ g.writeLine("}")
1929→ }
1930→ g.indent--
1931→ }
1932→ g.writeLine("}")
1933→}
1934→
1935→func (g *Generator) generateReplicatedAlt(alt *ast.AltBlock) {
1936→ // Replicated ALT: ALT i = start FOR count
1937→ // Uses reflect.Select for runtime-variable case count
1938→ if len(alt.Cases) == 0 {
1939→ return{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 2035,
"limit": 20
} 2035→ // Generate scoped abbreviations
2036→ for _, decl := range c.Declarations {
2037→ if abbr, ok := decl.(*ast.Abbreviation); ok {
2038→ g.generateAbbreviation(abbr)
2039→ }
2040→ }
2041→
2042→ // Assign received value from reflect.Value
2043→ g.writeLine(fmt.Sprintf("%s = _altValue.Interface().(%s)", goIdent(c.Variable), recvType))
2044→
2045→ // Generate body
2046→ for _, s := range c.Body {
2047→ g.generateStatement(s)
2048→ }
2049→
2050→ g.indent--
2051→ g.writeLine("}")
2052→}
2053→
2054→func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {# Fix #85: Support receive into indexed target variables
## Context
Channel receive (`ch ? var`) only accepts a simple identifier as the target. Indexed targets like `flags[0]` or `state.of.neighbour[d]` cause parse failures because the parser uses `expectPeek(IDENT)` and leaves the `[...]` unprocessed. The same limitation affects ALT case parsing. This blocks `historical-examples/life.occ` (issue #72).
The fix adds `VariableIndices []Expression` to `Receive` and `AltCase`, mirroring the existing `ChannelIndices` and `Assignment.Indices` patterns.
## Changes
### 1. AST — `ast/ast.go`
Add `VariableIndices []Expression` field to both structs:
- **`Receive`** (line 378): Add after `Variable string`
- **`AltCase`** (line 402): Add after `Variable string`
### 2. Parser — `parser/parser.go`
After each `expectPeek(IDENT)` + `Variable = curToken.Literal` site, add index collection loop:
```go
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken() // move to [
p.nextToken() // move past [
<target>.VariableIndices = append(<target>.VariableIndices, p.parseExpression(LOWEST))
if !p.expectPeek(lexer.RBRACKET) {
return nil
}
}
```
**5 locations:**
1. `parseReceive()` (~line 1294) — `stmt.VariableIndices`
2. `parseIndexedOperation()` receive branch (~line 792) — `stmt.VariableIndices`
3. `parseAltCase()` simple channel (~line 1765) — `altCase.VariableIndices`
4. `parseAltCase()` indexed channel (~line 1785) — `altCase.VariableIndices`
5. `parseAltCase()` guarded channel (~line 1825) — `altCase.VariableIndices`
### 3. Codegen — `codegen/codegen.go`
Build variable reference with indices using existing `generateIndicesStr()`:
**6 locations:**
1. `generateReceive()` simple path (~line 1362): Append indices to `varRef`
2. `generateReceive()` sequential path (~line 1349): Append indices to primary `varRef`
3. `generateAltBlock()` guarded case (line 1906): Build `varRef` with indices
4. `generateAltBlock()` indexed channel case (line 1908): Build `varRef` with indices
5. `generateAltBlock()` simple channel case (line 1912): Build `varRef` with indices
6. `generateReplicatedAlt()` (line 2043): Build `varRef` with indices
Pattern at each site:
```go
varRef := goIdent(name)
if len(indices) > 0 {
varRef += g.generateIndicesStr(indices)
}
```
### 4. Tests
**Parser tests** (`parser/parser_test.go`):
- `TestReceiveIndexedVariable`: `ch ? flags[0]` — verify `VariableIndices` has 1 entry
- `TestReceiveMultiIndexedVariable`: `ch ? grid[i][j]` — verify 2 indices
- `TestIndexedChannelReceiveIndexedVariable`: `cs[0] ? flags[1]` — both channel and variable indexed
**E2E tests** (`codegen/e2e_concurrency_test.go`):
- `TestE2E_ReceiveIntoIndexedVariable`: Simple channel receive into array element
- `TestE2E_IndexedChannelReceiveIntoIndexedVariable`: `cs[i] ? arr[j]` pattern
## Verification
```bash
go test ./...
```
<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>
1→# Fix #86: Support scoped declarations in variant receive case bodies
2→
3→## Context
4→
5→Variant protocol receive (`ch ? CASE`) case bodies only support a single statement. When a case body has a scoped declaration (e.g., `BOOL x :`) followed by a compound statement (`SEQ ...`), parsing fails. This blocks `historical-examples/life.occ` (issue #72).
6→
7→The fix follows the identical pattern already used by IF, CASE, and ALT — change `Body` from a single `Statement` to `[]Statement` and use `parseBodyStatements()`.
8→
9→## Changes
10→
11→### 1. AST — `ast/ast.go` line 458
12→
13→Change `VariantCase.Body` from single to multi-statement:
14→
15→```
16→Body Statement → Body []Statement
17→```
18→
19→### 2. Parser — `parser/parser.go`
20→
21→**`parseVariantReceive()`** (lines 1384-1393): Replace single-statement parse with `parseBodyStatements()`:
22→
23→```go
24→// Before:
25→if p.peekTokenIs(lexer.INDENT) {
26→ p.nextToken() // consume INDENT
27→ p.nextToken() // move to body
28→ vc.Body = p.parseStatement()
29→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
30→ p.nextToken()
31→ }
32→}
33→
34→// After:
35→if p.peekTokenIs(lexer.INDENT) {
36→ p.nextToken() // consume INDENT
37→ vc.Body = p.parseBodyStatements()
38→}
39→```
40→
41→**`parseVariantReceiveWithIndex()`** (lines 1479-1487): Same change.
42→
43→Note: `parseBodyStatements()` handles `p.nextToken()` internally (line 1924: "move past INDENT") and manages its own DEDENT/NEWLINE tracking, so the manual advance code is removed.
44→
45→### 3. Codegen — `codegen/codegen.go`
46→
47→**`generateVariantReceive()`** (lines 1420-1422): Loop over body statements:
48→
49→```go
50→// Before:
51→if vc.Body != nil {
52→ g.generateStatement(vc.Body)
53→}
54→
55→// After:
56→for _, s := range vc.Body {
57→ g.generateStatement(s)
58→}
59→```
60→
61→**All `containsX()` scanner functions** that walk `vc.Body` — 6 occurrences (containsPar, containsPrint, containsTimer, containsStop, containsMostExpr, walkStatements) plus collectChanProtocols, collectBoolVars, collectRecordVars, containsRetypes, containsAltReplicator. Change from:
62→
63→```go
64→if c.Body != nil && g.containsX(c.Body) {
65→```
66→
67→to:
68→
69→```go
70→for _, inner := range c.Body {
71→ if g.containsX(inner) {
72→ return true
73→ }
74→}
75→```
76→
77→This matches the pattern used by IF/CASE `choice.Body` iteration throughout the file.
78→
79→### 4. Tests
80→
81→- Add a parser test in `parser/parser_test.go` for variant receive with scoped declarations
82→- Add an e2e test in `codegen/e2e_test.go` that transpiles, compiles, and runs a program using variant receive with declarations in case bodies
83→
84→## Verification
85→
86→```bash
87→go test ./... # all tests pass
88→# Specific test for the new feature:
89→cat > /tmp/test_variant_decl.occ << 'EOF'
90→PROTOCOL CMD
91→ CASE
92→ set.state; BOOL
93→ evolve
94→ terminate
95→:
96→PROC test(CHAN OF CMD control)
97→ BOOL state, not.finished :
98→ SEQ
99→ state := FALSE
100→ not.finished := TRUE
101→ WHILE not.finished
102→ control ? CASE
103→ set.state; state
104→ SKIP
105→ evolve
106→ BOOL next.state :
107→ SEQ
108→ next.state := TRUE
109→ state := next.state
110→ terminate
111→ not.finished := FALSE
112→:
113→CHAN OF CMD ch :
114→PAR
115→ test(ch)
116→ SEQ
117→ ch ! set.state; TRUE
118→ ch ! evolve
119→ ch ! terminate
120→EOF
121→./occam2go -o /tmp/test_variant_decl.go /tmp/test_variant_decl.occ
122→go run /tmp/test_variant_decl.go
123→```
124→# Fix #85: Support receive into indexed target variables
## Context
Channel receive (`ch ? var`) only accepts a simple identifier as the target. Indexed targets like `flags[0]` or `state.of.neighbour[d]` cause parse failures because the parser uses `expectPeek(IDENT)` and leaves the `[...]` unprocessed. The same limitation affects ALT case parsing. This blocks `historical-examples/life.occ` (issue #72).
The fix adds `VariableIndices []Expression` to `Receive` and `AltCase`, mirroring the existing `ChannelIndices` and `Assignment.Indices` patterns.
## Changes
### 1. AST — `ast/ast.go`
Add `VariableIndices []Expression` field to both structs:
- **`Receive`** (line 378): Add after `Variable string`
- **`AltCase`** (line 402): Add after `Variable string`
### 2. Parser — `parser/parser.go`
After each `expectPeek(IDENT)` + `Variable = curToken.Literal` site, add index collection loop:
```go
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken() // move to [
p.nextToken() // move past [
<target>.VariableIndices = append(<target>.VariableIndices, p.parseExpression(LOWEST))
if !p.expectPeek(lexer.RBRACKET) {
return nil
}
}
```
**5 locations:**
1. `parseReceive()` (~line 1294) — `stmt.VariableIndices`
2. `parseIndexedOperation()` receive branch (~line 792) — `stmt.VariableIndices`
3. `parseAltCase()` simple channel (~line 1765) — `altCase.VariableIndices`
4. `parseAltCase()` indexed channel (~line 1785) — `altCase.VariableIndices`
5. `parseAltCase()` guarded channel (~line 1825) — `altCase.VariableIndices`
### 3. Codegen — `codegen/codegen.go`
Build variable reference with indices using existing `generateIndicesStr()`:
**6 locations:**
1. `generateReceive()` simple path (~line 1362): Append indices to `varRef`
2. `generateReceive()` sequential path (~line 1349): Append indices to primary `varRef`
3. `generateAltBlock()` guarded case (line 1906): Build `varRef` with indices
4. `generateAltBlock()` indexed channel case (line 1908): Build `varRef` with indices
5. `generateAltBlock()` simple channel case (line 1912): Build `varRef` with indices
6. `generateReplicatedAlt()` (line 2043): Build `varRef` with indices
Pattern at each site:
```go
varRef := goIdent(name)
if len(indices) > 0 {
varRef += g.generateIndicesStr(indices)
}
```
### 4. Tests
**Parser tests** (`parser/parser_test.go`):
- `TestReceiveIndexedVariable`: `ch ? flags[0]` — verify `VariableIndices` has 1 entry
- `TestReceiveMultiIndexedVariable`: `ch ? grid[i][j]` — verify 2 indices
- `TestIndexedChannelReceiveIndexedVariable`: `cs[0] ? flags[1]` — both channel and variable indexed
**E2E tests** (`codegen/e2e_concurrency_test.go`):
- `TestE2E_ReceiveIntoIndexedVariable`: Simple channel receive into array element
- `TestE2E_IndexedChannelReceiveIntoIndexedVariable`: `cs[i] ? arr[j]` pattern
## Verification
```bash
go test ./...
```
The file /home/david/.claude/plans/vectorized-soaring-flurry.md has been updated successfully.
{
"allowedPrompts": [
{
"tool": "Bash",
"prompt": "run tests"
}
],
"plan": "# Fix #85: Support receive into indexed target variables\n\n## Context\n\nChannel receive (`ch ? var`) only accepts a simple identifier as the target. Indexed targets like `flags[0]` or `state.of.neighbour[d]` cause parse failures because the parser uses `expectPeek(IDENT)` and leaves the `[...]` unprocessed. The same limitation affects ALT case parsing. This blocks `historical-examples/life.occ` (issue #72).\n\nThe fix adds `VariableIndices []Expression` to `Receive` and `AltCase`, mirroring the existing `ChannelIndices` and `Assignment.Indices` patterns.\n\n## Changes\n\n### 1. AST — `ast/ast.go`\n\nAdd `VariableIndices []Expression` field to both structs:\n\n- **`Receive`** (line 378): Add after `Variable string`\n- **`AltCase`** (line 402): Add after `Variable string`\n\n### 2. Parser — `parser/parser.go`\n\nAfter each `expectPeek(IDENT)` + `Variable = curToken.Literal` site, add index collection loop:\n\n```go\nfor p.peekTokenIs(lexer.LBRACKET) {\n p.nextToken() // move to [\n p.nextToken() // move past [\n <target>.VariableIndices = append(<target>.VariableIndices, p.parseExpression(LOWEST))\n if !p.expectPeek(lexer.RBRACKET) {\n return nil\n }\n}\n```\n\n**5 locations:**\n1. `parseReceive()` (~line 1294) — `stmt.VariableIndices`\n2. `parseIndexedOperation()` receive branch (~line 792) — `stmt.VariableIndices`\n3. `parseAltCase()` simple channel (~line 1765) — `altCase.VariableIndices`\n4. `parseAltCase()` indexed channel (~line 1785) — `altCase.VariableIndices`\n5. `parseAltCase()` guarded channel (~line 1825) — `altCase.VariableIndices`\n\n### 3. Codegen — `codegen/codegen.go`\n\nBuild variable reference with indices using existing `generateIndicesStr()`:\n\n**6 locations:**\n1. `generateReceive()` simple path (~line 1362): Append indices to `varRef`\n2. `generateReceive()` sequential path (~line 1349): Append indices to primary `varRef`\n3. `generateAltBlock()` guarded case (line 1906): Build `varRef` with indices\n4. `generateAltBlock()` indexed channel case (line 1908): Build `varRef` with indices\n5. `generateAltBlock()` simple channel case (line 1912): Build `varRef` with indices\n6. `generateReplicatedAlt()` (line 2043): Build `varRef` with indices\n\nPattern at each site:\n```go\nvarRef := goIdent(name)\nif len(indices) > 0 {\n varRef += g.generateIndicesStr(indices)\n}\n```\n\n### 4. Tests\n\n**Parser tests** (`parser/parser_test.go`):\n- `TestReceiveIndexedVariable`: `ch ? flags[0]` — verify `VariableIndices` has 1 entry\n- `TestReceiveMultiIndexedVariable`: `ch ? grid[i][j]` — verify 2 indices\n- `TestIndexedChannelReceiveIndexedVariable`: `cs[0] ? flags[1]` — both channel and variable indexed\n\n**E2E tests** (`codegen/e2e_concurrency_test.go`):\n- `TestE2E_ReceiveIntoIndexedVariable`: Simple channel receive into array element\n- `TestE2E_IndexedChannelReceiveIntoIndexedVariable`: `cs[i] ? arr[j]` pattern\n\n## Verification\n\n```bash\ngo test ./...\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.