Claude Code transcript - page 1/1

Implement the following plan:

Fix #86: Support scoped declarations in variant receive case bodies

Context

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

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

Changes

1. AST — ast/ast.go line 458

Change VariantCase.Body from single to multi-statement:

Body      Statement    →    Body      []Statement

2. Parser — parser/parser.go

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

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

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

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

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

3. Codegen — codegen/codegen.go

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

// 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.

4. Tests

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

Verification

go test ./...                    # all tests pass
# Specific test for the new feature:
cat > /tmp/test_variant_decl.occ << 'EOF'
PROTOCOL CMD
  CASE
    set.state; BOOL
    evolve
    terminate
:
PROC test(CHAN OF CMD control)
  BOOL state, not.finished :
  SEQ
    state := FALSE
    not.finished := TRUE
    WHILE not.finished
      control ? CASE
        set.state; state
          SKIP
        evolve
          BOOL next.state :
          SEQ
            next.state := TRUE
            state := next.state
        terminate
          not.finished := FALSE
:
CHAN OF CMD ch :
PAR
  test(ch)
  SEQ
    ch ! set.state; TRUE
    ch ! evolve
    ch ! terminate
EOF
./occam2go -o /tmp/test_variant_decl.go /tmp/test_variant_decl.occ
go run /tmp/test_variant_decl.go

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

Thinking

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.

I'll start by reading the relevant source files in parallel.

Read
{
  "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→}
Read
{
  "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→
Read
{
  "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.

Grep
{
  "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) {
Grep
{
  "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:
Read
{
  "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 {

Let me see parseBodyStatements to understand the interface.

Grep
{
  "pattern": "func \\(p \\*Parser\\) parseBodyStatements",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
  "output_mode": "content",
  "-n": true
}
1913:func (p *Parser) parseBodyStatements() []ast.Statement {
Read
{
  "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
Read
{
  "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) {
Thinking

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() handles p.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.

Grep
{
  "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()
Thinking

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() handles p.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.

Read
{
  "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:
Read
{
  "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:
Read
{
  "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:
Read
{
  "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:
Read
{
  "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
Read
{
  "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).

✏️ Edit ast.go
/home/david/projects/code-associates/occam2go/ast/ast.go
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.
✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
		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.
✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
		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.

Now update the codegen — the generateVariantReceive function and all 6 scanner functions.

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
		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.
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	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.
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	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.
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	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.
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	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.
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	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.
✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	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.

$ Bash
Build all packages to check for compile errors
go build ./...

Compiles clean. Now run all tests.

$ Bash
Run all tests
go test ./... 2>&1
?   	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.

Grep
{
  "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))
Grep
{
  "pattern": "TestParse.*Variant|CASE.*variant",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "output_mode": "content",
  "-n": true
}
No matches found
Grep
{
  "pattern": "func TestParse",
  "path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "output_mode": "content",
  "-n": true,
  "head_limit": 10
}
No matches found

[Showing results with pagination = limit: 10, offset: 0]
Grep
{
  "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) {

Let me see the end of the parser test file to add my test there.

$ Bash
Line count of parser test file
wc -l /home/david/projects/code-associates/occam2go/parser/parser_test.go
3814 /home/david/projects/code-associates/occam2go/parser/parser_test.go
Read
{
  "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→

Now let me also look at an existing e2e test for variant protocol to see the pattern.

Grep
{
  "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>
Glob
{
  "pattern": "codegen/*e2e*"
}
/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
Grep
{
  "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) {
Read
{
  "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→

Good. Now I'll add both the parser test and the e2e test.

Read
{
  "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→

Now let me add both tests.

✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
	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.
✏️ Edit e2e_protocol_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go
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.

Now run the new tests specifically.

$ Bash
Run new parser test
go test ./parser -run TestVariantReceiveScopedDecl -v 2>&1
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
$ Bash
Run new e2e test
go test ./codegen -run TestE2E_VariantReceiveScopedDecl -v 2>&1
<tool_use_error>Sibling tool call errored</tool_use_error>

Let me check the import pattern in the test file.

Read
{
  "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) {

It's in the parser package already, so no parser. prefix needed. Let me fix the test.

Read
{
  "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
✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
	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.
$ Bash
Run new parser test
go test ./parser -run TestVariantReceiveScopedDecl -v 2>&1
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

WhileLoop.Body is a single Statement. Let me check.

Grep
{
  "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.

✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
	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.
$ Bash
Run new parser test
go test ./parser -run TestVariantReceiveScopedDecl -v 2>&1
=== RUN   TestVariantReceiveScopedDecl
--- PASS: TestVariantReceiveScopedDecl (0.00s)
PASS
ok  	github.com/codeassociates/occam2go/parser	0.001s

Parser test passes. Now run the e2e test.

$ Bash
Run new e2e test
go test ./codegen -run TestE2E_VariantReceiveScopedDecl -v 2>&1
=== RUN   TestE2E_VariantReceiveScopedDecl
--- PASS: TestE2E_VariantReceiveScopedDecl (0.08s)
PASS
ok  	github.com/codeassociates/occam2go/codegen	0.084s

Both new tests pass. Let me run the full test suite to confirm nothing is broken.

$ Bash
Run full test suite
go test ./... 2>&1
?   	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

Thank you. Please create a PR for this work.

Let me check the current state of the repo and prepare the PR.

$ Bash
Show working tree status
git status
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")
$ Bash
Show all changes
git diff
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])
+	}
+}
$ Bash
Show recent commits
git log --oneline -5
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
$ Bash
Create feature branch
git checkout -b fix/variant-receive-scoped-decl-86
Switched to a new branch 'fix/variant-receive-scoped-decl-86'
$ Bash
Commit changes
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(-)
$ Bash
Push branch to remote
git push -u origin fix/variant-receive-scoped-decl-86 2>&1
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'.
$ Bash
Create pull request
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
)"
https://github.com/codeassociates/occam2go/pull/87

PR created: https://github.com/codeassociates/occam2go/pull/87

Thank you. The PR has been approved and passed CI checks so please go ahead and merge.

$ Bash
Merge PR #87
gh pr merge 87 --merge
$ Bash
Merge PR #87
gh pr merge 87 --merge
! Pull request codeassociates/occam2go#87 was already merged

Looks like it was already merged. Let me sync the local main branch.

$ Bash
Switch to main and pull latest
git checkout main && git pull
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(-)

PR #87 is merged and local main is up to date.

Thank you. Please now take a look at fixing issue #85.

$ Bash
View issue #85
gh issue view 85
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.
EnterPlanMode
{}
Entered plan mode. You should now focus on exploring the codebase and designing an implementation approach.

In plan mode, you should:
1. Thoroughly explore the codebase to understand existing patterns
2. Identify similar features and architectural approaches
3. Consider multiple approaches and their trade-offs
4. Use AskUserQuestion if you need to clarify the approach
5. Design a concrete implementation strategy
6. When ready, use ExitPlanMode to present your plan for approval

Remember: DO NOT write or edit any files yet. This is a read-only exploration and planning phase.

Let me explore the relevant code paths to understand the current implementation.

Task
Explore receive parsing code
{
  "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"
}
Task
Explore indexed send patterns
{
  "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.

Task
Plan indexed receive targets
{
  "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>"
  }
]

Let me verify a few key details before writing the final plan.

Read
{
  "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
Read
{
  "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) {

Good, I've confirmed the exact lines. Now let me write the final plan.

📝 Write vectorized-soaring-flurry.md
/home/david/.claude/plans/vectorized-soaring-flurry.md
# 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>
Read
{
  "file_path": "/home/david/.claude/plans/vectorized-soaring-flurry.md"
}
     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→
📝 Write vectorized-soaring-flurry.md
/home/david/.claude/plans/vectorized-soaring-flurry.md
# 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.
ExitPlanMode
{
  "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.

[Request interrupted by user for tool use]