Implement the following plan:
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.
ast/ast.goAdd VariableIndices []Expression field to both structs:
Receive (line 378): Add after Variable stringAltCase (line 402): Add after Variable stringparser/parser.goAfter each expectPeek(IDENT) + Variable = curToken.Literal site, add index collection loop:
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
codegen/codegen.goBuild 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:
varRef := goIdent(name)
if len(indices) > 0 {
varRef += g.generateIndicesStr(indices)
}
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
go test ./...
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/cccd5ad5-122e-445d-bd16-94c2d59c7a1a.jsonl
Let me implement this plan step by step. I'll start by reading the relevant files to understand the current code, then make the changes.
{
"file_path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"offset": 370,
"limit": 50
} 370→func (s *Send) statementNode() {}
371→func (s *Send) TokenLiteral() string { return s.Token.Literal }
372→
373→// Receive represents a channel receive: c ? x or c ? x ; y
374→type Receive struct {
375→ Token lexer.Token // the ? token
376→ Channel string // channel name
377→ ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x
378→ Variable string // variable to receive into (simple receive)
379→ Variables []string // additional variables for sequential receives (c ? x ; y)
380→}
381→
382→func (r *Receive) statementNode() {}
383→func (r *Receive) TokenLiteral() string { return r.Token.Literal }
384→
385→// AltBlock represents an ALT block (alternation/select)
386→// If Replicator is non-nil, this is a replicated ALT (ALT i = 0 FOR n)
387→type AltBlock struct {
388→ Token lexer.Token // the ALT token
389→ Cases []AltCase
390→ Replicator *Replicator // optional replicator
391→ Priority bool // true for PRI ALT
392→}
393→
394→func (a *AltBlock) statementNode() {}
395→func (a *AltBlock) TokenLiteral() string { return a.Token.Literal }
396→
397→// AltCase represents a single case in an ALT block
398→type AltCase struct {
399→ Guard Expression // optional guard condition (nil if no guard)
400→ Channel string // channel name
401→ ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT
402→ Variable string // variable to receive into
403→ Body []Statement // the body to execute
404→ IsTimer bool // true if this is a timer AFTER case
405→ IsSkip bool // true if this is a guarded SKIP case (guard & SKIP)
406→ Timer string // timer name (when IsTimer)
407→ Deadline Expression // AFTER deadline expression (when IsTimer)
408→ Declarations []Statement // scoped declarations before channel input (e.g., BYTE ch:)
409→}
410→
411→// TimerDecl represents a timer declaration: TIMER tim:
412→type TimerDecl struct {
413→ Token lexer.Token // the TIMER token
414→ Names []string // timer variable names
415→}
416→
417→func (td *TimerDecl) statementNode() {}
418→func (td *TimerDecl) TokenLiteral() string { return td.Token.Literal }
419→{
"pattern": "func \\(p \\*Parser\\) parseReceive",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true
}{
"pattern": "func \\(p \\*Parser\\) parseAltCase",
"path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"output_mode": "content",
"-n": true
}1648:func (p *Parser) parseAltCases() []ast.AltCase {
1713:func (p *Parser) parseAltCase() *ast.AltCase {{
"pattern": "func \\(g \\*Generator\\) generateReceive",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}{
"pattern": "func \\(g \\*Generator\\) generateAltBlock",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}{
"pattern": "func \\(g \\*Generator\\) generateReplicatedAlt",
"path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"output_mode": "content",
"-n": true
}{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1274,
"limit": 50
} 1274→func (p *Parser) parseReceive() ast.Statement {
1275→ channel := p.curToken.Literal
1276→
1277→ p.nextToken() // move to ?
1278→ recvToken := p.curToken
1279→
1280→ // Check for variant receive: c ? CASE
1281→ if p.peekTokenIs(lexer.CASE) {
1282→ p.nextToken() // move to CASE
1283→ return p.parseVariantReceive(channel, recvToken)
1284→ }
1285→
1286→ stmt := &ast.Receive{
1287→ Channel: channel,
1288→ Token: recvToken,
1289→ }
1290→
1291→ if !p.expectPeek(lexer.IDENT) {
1292→ return nil
1293→ }
1294→ stmt.Variable = p.curToken.Literal
1295→
1296→ // Check for sequential receive: c ? x ; y ; z
1297→ for p.peekTokenIs(lexer.SEMICOLON) {
1298→ p.nextToken() // move to ;
1299→ if !p.expectPeek(lexer.IDENT) {
1300→ return nil
1301→ }
1302→ stmt.Variables = append(stmt.Variables, p.curToken.Literal)
1303→ }
1304→
1305→ return stmt
1306→}
1307→
1308→func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {
1309→ stmt := &ast.VariantReceive{
1310→ Token: token,
1311→ Channel: channel,
1312→ }
1313→
1314→ // Skip to next line
1315→ for p.peekTokenIs(lexer.NEWLINE) {
1316→ p.nextToken()
1317→ }
1318→
1319→ // Expect INDENT
1320→ if !p.peekTokenIs(lexer.INDENT) {
1321→ p.addError("expected indented block after ? CASE")
1322→ return stmt
1323→ }{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 770,
"limit": 50
} 770→ }
771→
772→ if p.peekTokenIs(lexer.RECEIVE) {
773→ // Indexed channel receive: cs[i] ? x or cs[i][j] ? x or cs[i] ? CASE ...
774→ p.nextToken() // move to ?
775→ recvToken := p.curToken
776→
777→ // Check for variant receive: cs[i] ? CASE
778→ if p.peekTokenIs(lexer.CASE) {
779→ p.nextToken() // move to CASE
780→ return p.parseVariantReceiveWithIndex(name, indices, recvToken)
781→ }
782→
783→ stmt := &ast.Receive{
784→ Token: recvToken,
785→ Channel: name,
786→ ChannelIndices: indices,
787→ }
788→
789→ if !p.expectPeek(lexer.IDENT) {
790→ return nil
791→ }
792→ stmt.Variable = p.curToken.Literal
793→
794→ // Check for sequential receive
795→ for p.peekTokenIs(lexer.SEMICOLON) {
796→ p.nextToken() // move to ;
797→ if !p.expectPeek(lexer.IDENT) {
798→ return nil
799→ }
800→ stmt.Variables = append(stmt.Variables, p.curToken.Literal)
801→ }
802→
803→ return stmt
804→ }
805→
806→ // Default: treat as indexed assignment (shouldn't reach here normally)
807→ p.addError(fmt.Sprintf("expected :=, !, or ? after %s[...], got %s", name, p.peekToken.Type))
808→ return nil
809→}
810→
811→func (p *Parser) parseIndexExpression(left ast.Expression) *ast.IndexExpr {
812→ expr := &ast.IndexExpr{
813→ Token: p.curToken,
814→ Left: left,
815→ }
816→
817→ p.nextToken() // move past [
818→ expr.Index = p.parseExpression(LOWEST)
819→{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1713,
"limit": 150
} 1713→func (p *Parser) parseAltCase() *ast.AltCase {
1714→ altCase := &ast.AltCase{}
1715→
1716→ // Parse scoped declarations before the channel input (e.g., BYTE ch:, VAL INT X IS expr:)
1717→ for p.isAltDeclStart() {
1718→ stmt := p.parseStatement()
1719→ if stmt != nil {
1720→ altCase.Declarations = append(altCase.Declarations, stmt)
1721→ }
1722→ // Advance past the end of the declaration
1723→ if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
1724→ p.nextToken()
1725→ }
1726→ // Skip newlines to reach next token
1727→ for p.curTokenIs(lexer.NEWLINE) {
1728→ p.nextToken()
1729→ }
1730→ }
1731→
1732→ // Check for guard: expression & channel ? var
1733→ // or: channel ? var (no guard)
1734→ // or: guard & SKIP
1735→
1736→ // First token should be identifier, TRUE/FALSE, or ( for guard expression
1737→ if !p.curTokenIs(lexer.IDENT) && !p.curTokenIs(lexer.TRUE) && !p.curTokenIs(lexer.FALSE) && !p.curTokenIs(lexer.LPAREN) {
1738→ p.addError(fmt.Sprintf("expected channel name or guard in ALT case, got %s", p.curToken.Type))
1739→ return nil
1740→ }
1741→
1742→ // Look ahead to determine if this is a guard or channel
1743→ // If current is ( then it must be a guard expression
1744→ // If next token is & then we have a guard
1745→ // If next token is ? then it's a channel/timer receive
1746→ if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.RECEIVE) {
1747→ name := p.curToken.Literal
1748→ if p.timerNames[name] {
1749→ // Timer case: tim ? AFTER deadline
1750→ altCase.IsTimer = true
1751→ altCase.Timer = name
1752→ p.nextToken() // move to ?
1753→ if !p.expectPeek(lexer.AFTER) {
1754→ return nil
1755→ }
1756→ p.nextToken() // move past AFTER
1757→ altCase.Deadline = p.parseExpression(LOWEST)
1758→ } else {
1759→ // Simple case: channel ? var
1760→ altCase.Channel = name
1761→ p.nextToken() // move to ?
1762→ if !p.expectPeek(lexer.IDENT) {
1763→ return nil
1764→ }
1765→ altCase.Variable = p.curToken.Literal
1766→ }
1767→ } else if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.LBRACKET) {
1768→ // Indexed channel case: cs[i] ? var or cs[i][j] ? var
1769→ name := p.curToken.Literal
1770→ altCase.Channel = name
1771→ for p.peekTokenIs(lexer.LBRACKET) {
1772→ p.nextToken() // move to [
1773→ p.nextToken() // move past [
1774→ altCase.ChannelIndices = append(altCase.ChannelIndices, p.parseExpression(LOWEST))
1775→ if !p.expectPeek(lexer.RBRACKET) {
1776→ return nil
1777→ }
1778→ }
1779→ if !p.expectPeek(lexer.RECEIVE) {
1780→ return nil
1781→ }
1782→ if !p.expectPeek(lexer.IDENT) {
1783→ return nil
1784→ }
1785→ altCase.Variable = p.curToken.Literal
1786→ } else {
1787→ // Guard followed by & channel ? var, or guard & SKIP
1788→ guard := p.parseExpression(LOWEST)
1789→ altCase.Guard = guard
1790→
1791→ // Expect &
1792→ if !p.peekTokenIs(lexer.AMPERSAND) {
1793→ p.addError("expected & after guard in ALT case")
1794→ return nil
1795→ }
1796→ p.nextToken() // move to &
1797→ p.nextToken() // move past &
1798→
1799→ if p.curTokenIs(lexer.SKIP) {
1800→ // Guarded SKIP: guard & SKIP
1801→ altCase.IsSkip = true
1802→ } else if !p.curTokenIs(lexer.IDENT) {
1803→ p.addError(fmt.Sprintf("expected channel name or SKIP after guard, got %s", p.curToken.Type))
1804→ return nil
1805→ } else {
1806→ // Channel operation after guard
1807→ altCase.Channel = p.curToken.Literal
1808→
1809→ for p.peekTokenIs(lexer.LBRACKET) {
1810→ // Indexed channel with guard: guard & cs[i] ? var or cs[i][j] ? var
1811→ p.nextToken() // move to [
1812→ p.nextToken() // move past [
1813→ altCase.ChannelIndices = append(altCase.ChannelIndices, p.parseExpression(LOWEST))
1814→ if !p.expectPeek(lexer.RBRACKET) {
1815→ return nil
1816→ }
1817→ }
1818→
1819→ if !p.expectPeek(lexer.RECEIVE) {
1820→ return nil
1821→ }
1822→ if !p.expectPeek(lexer.IDENT) {
1823→ return nil
1824→ }
1825→ altCase.Variable = p.curToken.Literal
1826→ }
1827→ }
1828→
1829→ // Skip to next line for the body
1830→ for p.peekTokenIs(lexer.NEWLINE) {
1831→ p.nextToken()
1832→ }
1833→
1834→ // Expect INDENT for body
1835→ if !p.peekTokenIs(lexer.INDENT) {
1836→ p.addError("expected indented body after ALT case")
1837→ return altCase
1838→ }
1839→ p.nextToken() // consume INDENT
1840→ p.nextToken() // move into body
1841→
1842→ altCase.Body = p.parseBodyStatements()
1843→
1844→ return altCase
1845→}
1846→
1847→func (p *Parser) parseBlockStatements() []ast.Statement {
1848→ var statements []ast.Statement
1849→ startLevel := p.indentLevel
1850→
1851→ p.nextToken() // move past INDENT
1852→
1853→ for !p.curTokenIs(lexer.EOF) {
1854→ // Skip newlines
1855→ for p.curTokenIs(lexer.NEWLINE) {
1856→ p.nextToken()
1857→ }
1858→
1859→ // Handle DEDENT tokens
1860→ // If we're at a DEDENT and indentLevel has dropped below startLevel,
1861→ // this DEDENT ends our block - stop parsing
1862→ // If indentLevel >= startLevel, this DEDENT is from a nested block - skip it{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1337,
"limit": 60
} 1337→func (g *Generator) generateReceive(recv *ast.Receive) {
1338→ chanRef := goIdent(recv.Channel)
1339→ if len(recv.ChannelIndices) > 0 {
1340→ chanRef += g.generateIndicesStr(recv.ChannelIndices)
1341→ }
1342→
1343→ if len(recv.Variables) > 0 {
1344→ // Sequential receive: _tmpN := <-c; x = _tmpN._0; y = _tmpN._1
1345→ tmpName := fmt.Sprintf("_tmp%d", g.tmpCounter)
1346→ g.tmpCounter++
1347→ g.writeLine(fmt.Sprintf("%s := <-%s", tmpName, chanRef))
1348→ varRef := goIdent(recv.Variable)
1349→ if g.refParams[recv.Variable] {
1350→ varRef = "*" + varRef
1351→ }
1352→ g.writeLine(fmt.Sprintf("%s = %s._0", varRef, tmpName))
1353→ for i, v := range recv.Variables {
1354→ vRef := goIdent(v)
1355→ if g.refParams[v] {
1356→ vRef = "*" + vRef
1357→ }
1358→ g.writeLine(fmt.Sprintf("%s = %s._%d", vRef, tmpName, i+1))
1359→ }
1360→ } else {
1361→ varRef := goIdent(recv.Variable)
1362→ if g.refParams[recv.Variable] {
1363→ varRef = "*" + varRef
1364→ }
1365→ g.writeLine(fmt.Sprintf("%s = <-%s", varRef, chanRef))
1366→ }
1367→}
1368→
1369→func (g *Generator) generateProtocolDecl(proto *ast.ProtocolDecl) {
1370→ gName := goIdent(proto.Name)
1371→ switch proto.Kind {
1372→ case "simple":
1373→ goType := g.occamTypeToGoBase(proto.Types[0])
1374→ g.writeLine(fmt.Sprintf("type _proto_%s = %s", gName, goType))
1375→ g.writeLine("")
1376→ case "sequential":
1377→ g.writeLine(fmt.Sprintf("type _proto_%s struct {", gName))
1378→ g.indent++
1379→ for i, t := range proto.Types {
1380→ goType := g.occamTypeToGoBase(t)
1381→ g.writeLine(fmt.Sprintf("_%d %s", i, goType))
1382→ }
1383→ g.indent--
1384→ g.writeLine("}")
1385→ g.writeLine("")
1386→ case "variant":
1387→ // Interface type
1388→ g.writeLine(fmt.Sprintf("type _proto_%s interface {", gName))
1389→ g.indent++
1390→ g.writeLine(fmt.Sprintf("_is_%s()", gName))
1391→ g.indent--
1392→ g.writeLine("}")
1393→ g.writeLine("")
1394→ // Concrete types for each variant
1395→ for _, v := range proto.Variants {
1396→ gTag := goIdent(v.Tag){
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1860,
"limit": 100
} 1860→func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
1861→ if alt.Replicator != nil {
1862→ g.generateReplicatedAlt(alt)
1863→ return
1864→ }
1865→
1866→ // ALT becomes Go select statement
1867→ // For guards, we use a pattern with nil channels
1868→
1869→ // Check if any cases have guards
1870→ hasGuards := false
1871→ for _, c := range alt.Cases {
1872→ if c.Guard != nil {
1873→ hasGuards = true
1874→ break
1875→ }
1876→ }
1877→
1878→ if hasGuards {
1879→ // Generate channel variables for guarded cases
1880→ for i, c := range alt.Cases {
1881→ if c.Guard != nil && !c.IsSkip {
1882→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1883→ g.write(fmt.Sprintf("var _alt%d chan ", i))
1884→ // We don't know the channel type here, so use interface{}
1885→ // Actually, we should use the same type as the original channel
1886→ // For now, let's just reference the original channel conditionally
1887→ g.write(fmt.Sprintf("int = nil\n")) // Assuming int for now
1888→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1889→ g.write(fmt.Sprintf("if "))
1890→ g.generateExpression(c.Guard)
1891→ g.write(fmt.Sprintf(" { _alt%d = %s }\n", i, goIdent(c.Channel)))
1892→ }
1893→ }
1894→ }
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
1940→ }
1941→ c := alt.Cases[0]
1942→ rep := alt.Replicator
1943→ v := goIdent(rep.Variable)
1944→
1945→ // Determine receive type from scoped declarations
1946→ recvType := "int" // default
1947→ for _, decl := range c.Declarations {
1948→ if vd, ok := decl.(*ast.VarDecl); ok {
1949→ for _, name := range vd.Names {
1950→ if name == c.Variable {
1951→ recvType = g.occamTypeToGo(vd.Type)
1952→ break
1953→ }
1954→ }
1955→ }
1956→ }
1957→
1958→ // Open a block for scoping
1959→ g.writeLine("{"){
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1935,
"limit": 120
} 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
1940→ }
1941→ c := alt.Cases[0]
1942→ rep := alt.Replicator
1943→ v := goIdent(rep.Variable)
1944→
1945→ // Determine receive type from scoped declarations
1946→ recvType := "int" // default
1947→ for _, decl := range c.Declarations {
1948→ if vd, ok := decl.(*ast.VarDecl); ok {
1949→ for _, name := range vd.Names {
1950→ if name == c.Variable {
1951→ recvType = g.occamTypeToGo(vd.Type)
1952→ break
1953→ }
1954→ }
1955→ }
1956→ }
1957→
1958→ // Open a block for scoping
1959→ g.writeLine("{")
1960→ g.indent++
1961→
1962→ // _altCount := int(<count>)
1963→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1964→ g.write("_altCount := int(")
1965→ g.generateExpression(rep.Count)
1966→ g.write(")\n")
1967→
1968→ // _altCases := make([]reflect.SelectCase, _altCount)
1969→ g.writeLine("_altCases := make([]reflect.SelectCase, _altCount)")
1970→
1971→ // Setup loop: build select cases
1972→ g.writeLine("for _altI := 0; _altI < _altCount; _altI++ {")
1973→ g.indent++
1974→
1975→ // Compute replicator variable
1976→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1977→ if rep.Step != nil {
1978→ g.write(fmt.Sprintf("%s := ", v))
1979→ g.generateExpression(rep.Start)
1980→ g.write(" + _altI * (")
1981→ g.generateExpression(rep.Step)
1982→ g.write(")\n")
1983→ } else {
1984→ g.write(fmt.Sprintf("%s := ", v))
1985→ g.generateExpression(rep.Start)
1986→ g.write(" + _altI\n")
1987→ }
1988→
1989→ // Generate scoped abbreviations (needed for channel index computation)
1990→ for _, decl := range c.Declarations {
1991→ if abbr, ok := decl.(*ast.Abbreviation); ok {
1992→ g.generateAbbreviation(abbr)
1993→ }
1994→ }
1995→
1996→ // Build select case entry
1997→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1998→ g.write("_altCases[_altI] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(")
1999→ if len(c.ChannelIndices) > 0 {
2000→ g.write(goIdent(c.Channel))
2001→ g.generateIndices(c.ChannelIndices)
2002→ } else {
2003→ g.write(goIdent(c.Channel))
2004→ }
2005→ g.write(")}\n")
2006→
2007→ g.indent--
2008→ g.writeLine("}")
2009→
2010→ // Call reflect.Select
2011→ g.writeLine("_altChosen, _altValue, _ := reflect.Select(_altCases)")
2012→
2013→ // Recompute replicator variable from chosen index
2014→ g.builder.WriteString(strings.Repeat("\t", g.indent))
2015→ if rep.Step != nil {
2016→ g.write(fmt.Sprintf("%s := ", v))
2017→ g.generateExpression(rep.Start)
2018→ g.write(" + _altChosen * (")
2019→ g.generateExpression(rep.Step)
2020→ g.write(")\n")
2021→ } else {
2022→ g.write(fmt.Sprintf("%s := ", v))
2023→ g.generateExpression(rep.Start)
2024→ g.write(" + _altChosen\n")
2025→ }
2026→ g.writeLine(fmt.Sprintf("_ = %s", v))
2027→
2028→ // Generate scoped var declarations
2029→ for _, decl := range c.Declarations {
2030→ if vd, ok := decl.(*ast.VarDecl); ok {
2031→ g.generateVarDecl(vd)
2032→ }
2033→ }
2034→
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) {Now I have a clear picture of what needs to change. Let me implement all the changes:
VariableIndices []Expression to Receive and AltCaseLet me also check the existing test files to understand the patterns.
Now I have all the context I need. Let me implement the changes. I'll start with the AST and parser changes, then codegen, then tests.
{
"subject": "Add VariableIndices to AST structs",
"activeForm": "Adding VariableIndices to AST"
}{
"subject": "Add index parsing for receive targets",
"activeForm": "Adding index parsing for receive targets"
}{
"subject": "Update codegen to emit indexed receive targets",
"activeForm": "Updating codegen for indexed targets"
}{
"subject": "Add parser and e2e tests",
"activeForm": "Adding tests"
}Channel string // channel name ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x Variable string // variable to receive into (simple receive) Variables []string // additional variables for sequential receives (c ? x ; y)
Channel string // channel name ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x Variable string // variable to receive into (simple receive) VariableIndices []Expression // non-empty for c ? flags[0] or c ? grid[i][j] Variables []string // additional variables for sequential receives (c ? x ; y)
The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.
Guard Expression // optional guard condition (nil if no guard) Channel string // channel name ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT Variable string // variable to receive into Body []Statement // the body to execute
Guard Expression // optional guard condition (nil if no guard) Channel string // channel name ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT Variable string // variable to receive into VariableIndices []Expression // non-empty for c ? flags[0] or c ? grid[i][j] Body []Statement // the body to execute
The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.
if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variable = p.curToken.Literal
// Check for sequential receive: c ? x ; y ; z
for p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken() // move to ;
if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variables = append(stmt.Variables, p.curToken.Literal)
}
return stmt
}
func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive { if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variable = p.curToken.Literal
// Collect variable indices: c ? flags[0] or c ? grid[i][j]
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken() // move to [
p.nextToken() // move past [
stmt.VariableIndices = append(stmt.VariableIndices, p.parseExpression(LOWEST))
if !p.expectPeek(lexer.RBRACKET) {
return nil
}
}
// Check for sequential receive: c ? x ; y ; z
for p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken() // move to ;
if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variables = append(stmt.Variables, p.curToken.Literal)
}
return stmt
}
func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
stmt := &ast.Receive{
Token: recvToken,
Channel: name,
ChannelIndices: indices,
}
if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variable = p.curToken.Literal
// Check for sequential receive
for p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken() // move to ;
if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variables = append(stmt.Variables, p.curToken.Literal)
}
return stmt
} stmt := &ast.Receive{
Token: recvToken,
Channel: name,
ChannelIndices: indices,
}
if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variable = p.curToken.Literal
// Collect variable indices: cs[i] ? flags[0] or cs[i] ? grid[j][k]
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken() // move to [
p.nextToken() // move past [
stmt.VariableIndices = append(stmt.VariableIndices, p.parseExpression(LOWEST))
if !p.expectPeek(lexer.RBRACKET) {
return nil
}
}
// Check for sequential receive
for p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken() // move to ;
if !p.expectPeek(lexer.IDENT) {
return nil
}
stmt.Variables = append(stmt.Variables, p.curToken.Literal)
}
return stmt
}The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
} else {
// Simple case: channel ? var
altCase.Channel = name
p.nextToken() // move to ?
if !p.expectPeek(lexer.IDENT) {
return nil
}
altCase.Variable = p.curToken.Literal
} } else {
// Simple case: channel ? var or channel ? var[i]
altCase.Channel = name
p.nextToken() // move to ?
if !p.expectPeek(lexer.IDENT) {
return nil
}
altCase.Variable = p.curToken.Literal
// Collect variable indices: ch ? flags[0]
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken() // move to [
p.nextToken() // move past [
altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))
if !p.expectPeek(lexer.RBRACKET) {
return nil
}
}
}The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
if !p.expectPeek(lexer.RECEIVE) {
return nil
}
if !p.expectPeek(lexer.IDENT) {
return nil
}
altCase.Variable = p.curToken.Literal
} else { if !p.expectPeek(lexer.RECEIVE) {
return nil
}
if !p.expectPeek(lexer.IDENT) {
return nil
}
altCase.Variable = p.curToken.Literal
// Collect variable indices: cs[i] ? flags[0]
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken() // move to [
p.nextToken() // move past [
altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))
if !p.expectPeek(lexer.RBRACKET) {
return nil
}
}
} else {The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
Now the guarded channel case (location 5). Let me re-read that section to make sure I have the right context after the previous edits.
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1820,
"limit": 40
} 1820→ if !p.expectPeek(lexer.RBRACKET) {
1821→ return nil
1822→ }
1823→ }
1824→ } else {
1825→ // Guard followed by & channel ? var, or guard & SKIP
1826→ guard := p.parseExpression(LOWEST)
1827→ altCase.Guard = guard
1828→
1829→ // Expect &
1830→ if !p.peekTokenIs(lexer.AMPERSAND) {
1831→ p.addError("expected & after guard in ALT case")
1832→ return nil
1833→ }
1834→ p.nextToken() // move to &
1835→ p.nextToken() // move past &
1836→
1837→ if p.curTokenIs(lexer.SKIP) {
1838→ // Guarded SKIP: guard & SKIP
1839→ altCase.IsSkip = true
1840→ } else if !p.curTokenIs(lexer.IDENT) {
1841→ p.addError(fmt.Sprintf("expected channel name or SKIP after guard, got %s", p.curToken.Type))
1842→ return nil
1843→ } else {
1844→ // Channel operation after guard
1845→ altCase.Channel = p.curToken.Literal
1846→
1847→ for p.peekTokenIs(lexer.LBRACKET) {
1848→ // Indexed channel with guard: guard & cs[i] ? var or cs[i][j] ? var
1849→ p.nextToken() // move to [
1850→ p.nextToken() // move past [
1851→ altCase.ChannelIndices = append(altCase.ChannelIndices, p.parseExpression(LOWEST))
1852→ if !p.expectPeek(lexer.RBRACKET) {
1853→ return nil
1854→ }
1855→ }
1856→
1857→ if !p.expectPeek(lexer.RECEIVE) {
1858→ return nil
1859→ }{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go",
"offset": 1857,
"limit": 20
} 1857→ if !p.expectPeek(lexer.RECEIVE) {
1858→ return nil
1859→ }
1860→ if !p.expectPeek(lexer.IDENT) {
1861→ return nil
1862→ }
1863→ altCase.Variable = p.curToken.Literal
1864→ }
1865→ }
1866→
1867→ // Skip to next line for the body
1868→ for p.peekTokenIs(lexer.NEWLINE) {
1869→ p.nextToken()
1870→ }
1871→
1872→ // Expect INDENT for body
1873→ if !p.peekTokenIs(lexer.INDENT) {
1874→ p.addError("expected indented body after ALT case")
1875→ return altCase
1876→ } if !p.expectPeek(lexer.RECEIVE) {
return nil
}
if !p.expectPeek(lexer.IDENT) {
return nil
}
altCase.Variable = p.curToken.Literal
}
}
// Skip to next line for the body if !p.expectPeek(lexer.RECEIVE) {
return nil
}
if !p.expectPeek(lexer.IDENT) {
return nil
}
altCase.Variable = p.curToken.Literal
// Collect variable indices: guard & ch ? flags[0]
for p.peekTokenIs(lexer.LBRACKET) {
p.nextToken() // move to [
p.nextToken() // move past [
altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))
if !p.expectPeek(lexer.RBRACKET) {
return nil
}
}
}
}
// Skip to next line for the bodyThe file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
if len(recv.Variables) > 0 {
// Sequential receive: _tmpN := <-c; x = _tmpN._0; y = _tmpN._1
tmpName := fmt.Sprintf("_tmp%d", g.tmpCounter)
g.tmpCounter++
g.writeLine(fmt.Sprintf("%s := <-%s", tmpName, chanRef))
varRef := goIdent(recv.Variable)
if g.refParams[recv.Variable] {
varRef = "*" + varRef
}
g.writeLine(fmt.Sprintf("%s = %s._0", varRef, tmpName))
for i, v := range recv.Variables {
vRef := goIdent(v)
if g.refParams[v] {
vRef = "*" + vRef
}
g.writeLine(fmt.Sprintf("%s = %s._%d", vRef, tmpName, i+1))
}
} else {
varRef := goIdent(recv.Variable)
if g.refParams[recv.Variable] {
varRef = "*" + varRef
}
g.writeLine(fmt.Sprintf("%s = <-%s", varRef, chanRef))
} if len(recv.Variables) > 0 {
// Sequential receive: _tmpN := <-c; x = _tmpN._0; y = _tmpN._1
tmpName := fmt.Sprintf("_tmp%d", g.tmpCounter)
g.tmpCounter++
g.writeLine(fmt.Sprintf("%s := <-%s", tmpName, chanRef))
varRef := goIdent(recv.Variable)
if len(recv.VariableIndices) > 0 {
varRef += g.generateIndicesStr(recv.VariableIndices)
} else if g.refParams[recv.Variable] {
varRef = "*" + varRef
}
g.writeLine(fmt.Sprintf("%s = %s._0", varRef, tmpName))
for i, v := range recv.Variables {
vRef := goIdent(v)
if g.refParams[v] {
vRef = "*" + vRef
}
g.writeLine(fmt.Sprintf("%s = %s._%d", vRef, tmpName, i+1))
}
} else {
varRef := goIdent(recv.Variable)
if len(recv.VariableIndices) > 0 {
varRef += g.generateIndicesStr(recv.VariableIndices)
} else if g.refParams[recv.Variable] {
varRef = "*" + varRef
}
g.writeLine(fmt.Sprintf("%s = <-%s", varRef, chanRef))
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1896,
"limit": 25
} 1896→ }
1897→ }
1898→ }
1899→
1900→ g.writeLine("select {")
1901→ for i, c := range alt.Cases {
1902→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1903→ if c.IsSkip {
1904→ g.write("default:\n")
1905→ } else if c.IsTimer {
1906→ g.write("case <-time.After(time.Duration(")
1907→ g.generateExpression(c.Deadline)
1908→ g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
1909→ } else if c.Guard != nil {
1910→ g.write(fmt.Sprintf("case %s = <-_alt%d:\n", goIdent(c.Variable), i))
1911→ } else if len(c.ChannelIndices) > 0 {
1912→ g.write(fmt.Sprintf("case %s = <-%s", goIdent(c.Variable), goIdent(c.Channel)))
1913→ g.generateIndices(c.ChannelIndices)
1914→ g.write(":\n")
1915→ } else {
1916→ g.write(fmt.Sprintf("case %s = <-%s:\n", goIdent(c.Variable), goIdent(c.Channel)))
1917→ }
1918→ g.indent++
1919→ guardedSkip := c.IsSkip && c.Guard != nil
1920→ if guardedSkip {I need to build varRef with indices for the guarded, indexed-channel, and simple-channel branches:
} else if c.Guard != nil {
g.write(fmt.Sprintf("case %s = <-_alt%d:\n", goIdent(c.Variable), i))
} else if len(c.ChannelIndices) > 0 {
g.write(fmt.Sprintf("case %s = <-%s", goIdent(c.Variable), goIdent(c.Channel)))
g.generateIndices(c.ChannelIndices)
g.write(":\n")
} else {
g.write(fmt.Sprintf("case %s = <-%s:\n", goIdent(c.Variable), goIdent(c.Channel)))
} } else if c.Guard != nil {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-_alt%d:\n", varRef, i))
} else if len(c.ChannelIndices) > 0 {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-%s", varRef, goIdent(c.Channel)))
g.generateIndices(c.ChannelIndices)
g.write(":\n")
} else {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel)))
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
// Assign received value from reflect.Value
g.writeLine(fmt.Sprintf("%s = _altValue.Interface().(%s)", goIdent(c.Variable), recvType)) // Assign received value from reflect.Value
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.writeLine(fmt.Sprintf("%s = _altValue.Interface().(%s)", varRef, recvType))The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
{
"pattern": "TestReceive",
"path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"output_mode": "content",
"-n": true
}{
"pattern": "TestE2E_.*Chan",
"path": "/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go",
"output_mode": "content",
"-n": true
}22:func TestE2E_Channel(t *testing.T) {
39:func TestE2E_ChannelExpression(t *testing.T) {
57:func TestE2E_ChannelPingPong(t *testing.T) {
101:func TestE2E_AltSecondChannel(t *testing.T) {
184:func TestE2E_ChanParam(t *testing.T) {
204:func TestE2E_ChanParamTwoWay(t *testing.T) {
229:func TestE2E_ChanDirParam(t *testing.T) {{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"offset": 248,
"limit": 40
} 248→func TestReceive(t *testing.T) {
249→ input := `c ? x
250→`
251→ l := lexer.New(input)
252→ p := New(l)
253→ program := p.ParseProgram()
254→ checkParserErrors(t, p)
255→
256→ if len(program.Statements) != 1 {
257→ t.Fatalf("expected 1 statement, got %d", len(program.Statements))
258→ }
259→
260→ recv, ok := program.Statements[0].(*ast.Receive)
261→ if !ok {
262→ t.Fatalf("expected Receive, got %T", program.Statements[0])
263→ }
264→
265→ if recv.Channel != "c" {
266→ t.Errorf("expected channel 'c', got %s", recv.Channel)
267→ }
268→
269→ if recv.Variable != "x" {
270→ t.Errorf("expected variable 'x', got %s", recv.Variable)
271→ }
272→}
273→
274→func TestAltBlock(t *testing.T) {
275→ input := `ALT
276→ c1 ? x
277→ SKIP
278→ c2 ? y
279→ SKIP
280→`
281→ l := lexer.New(input)
282→ p := New(l)
283→ program := p.ParseProgram()
284→ checkParserErrors(t, p)
285→
286→ if len(program.Statements) != 1 {
287→ t.Fatalf("expected 1 statement, got %d", len(program.Statements)){
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go",
"offset": 22,
"limit": 40
} 22→func TestE2E_Channel(t *testing.T) {
23→ // Test basic channel communication between parallel processes
24→ occam := `SEQ
25→ CHAN OF INT c:
26→ INT result:
27→ PAR
28→ c ! 42
29→ c ? result
30→ print.int(result)
31→`
32→ output := transpileCompileRun(t, occam)
33→ expected := "42\n"
34→ if output != expected {
35→ t.Errorf("expected %q, got %q", expected, output)
36→ }
37→}
38→
39→func TestE2E_ChannelExpression(t *testing.T) {
40→ // Test sending an expression over a channel
41→ occam := `SEQ
42→ CHAN OF INT c:
43→ INT x, result:
44→ x := 10
45→ PAR
46→ c ! x * 2
47→ c ? result
48→ print.int(result)
49→`
50→ output := transpileCompileRun(t, occam)
51→ expected := "20\n"
52→ if output != expected {
53→ t.Errorf("expected %q, got %q", expected, output)
54→ }
55→}
56→
57→func TestE2E_ChannelPingPong(t *testing.T) {
58→ // Test two-way communication: send a value, double it, send back
59→ occam := `SEQ
60→ CHAN OF INT request:
61→ CHAN OF INT response:Good. Now let me find where the parser test file ends and where the e2e concurrency test file ends, so I can append tests.
wc -l /home/david/projects/code-associates/occam2go/parser/parser_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go
3890 /home/david/projects/code-associates/occam2go/parser/parser_test.go 394 /home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go 4284 total
{
"file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
"offset": 3885,
"limit": 10
} 3885→ t.Errorf("expected VarDecl as first body statement, got %T", evolveCase.Body[0])
3886→ }
3887→ if _, ok := evolveCase.Body[1].(*ast.SeqBlock); !ok {
3888→ t.Errorf("expected SeqBlock as second body statement, got %T", evolveCase.Body[1])
3889→ }
3890→}
3891→{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go",
"offset": 389,
"limit": 10
} 389→ output := transpileCompileRun(t, occam)
390→ expected := "99\n"
391→ if output != expected {
392→ t.Errorf("expected %q, got %q", expected, output)
393→ }
394→}
395→ if _, ok := evolveCase.Body[1].(*ast.SeqBlock); !ok {
t.Errorf("expected SeqBlock as second body statement, got %T", evolveCase.Body[1])
}
} if _, ok := evolveCase.Body[1].(*ast.SeqBlock); !ok {
t.Errorf("expected SeqBlock as second body statement, got %T", evolveCase.Body[1])
}
}
func TestReceiveIndexedVariable(t *testing.T) {
input := `ch ? flags[0]
`
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
recv, ok := program.Statements[0].(*ast.Receive)
if !ok {
t.Fatalf("expected Receive, got %T", program.Statements[0])
}
if recv.Channel != "ch" {
t.Errorf("expected channel 'ch', got %s", recv.Channel)
}
if recv.Variable != "flags" {
t.Errorf("expected variable 'flags', got %s", recv.Variable)
}
if len(recv.VariableIndices) != 1 {
t.Fatalf("expected 1 variable index, got %d", len(recv.VariableIndices))
}
}
func TestReceiveMultiIndexedVariable(t *testing.T) {
input := `ch ? grid[i][j]
`
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
recv, ok := program.Statements[0].(*ast.Receive)
if !ok {
t.Fatalf("expected Receive, got %T", program.Statements[0])
}
if recv.Variable != "grid" {
t.Errorf("expected variable 'grid', got %s", recv.Variable)
}
if len(recv.VariableIndices) != 2 {
t.Fatalf("expected 2 variable indices, got %d", len(recv.VariableIndices))
}
}
func TestIndexedChannelReceiveIndexedVariable(t *testing.T) {
input := `cs[0] ? flags[1]
`
l := lexer.New(input)
p := New(l)
program := p.ParseProgram()
checkParserErrors(t, p)
if len(program.Statements) != 1 {
t.Fatalf("expected 1 statement, got %d", len(program.Statements))
}
recv, ok := program.Statements[0].(*ast.Receive)
if !ok {
t.Fatalf("expected Receive, got %T", program.Statements[0])
}
if recv.Channel != "cs" {
t.Errorf("expected channel 'cs', got %s", recv.Channel)
}
if len(recv.ChannelIndices) != 1 {
t.Fatalf("expected 1 channel index, got %d", len(recv.ChannelIndices))
}
if recv.Variable != "flags" {
t.Errorf("expected variable 'flags', got %s", recv.Variable)
}
if len(recv.VariableIndices) != 1 {
t.Fatalf("expected 1 variable index, got %d", len(recv.VariableIndices))
}
}The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
output := transpileCompileRun(t, occam)
expected := "99\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
} output := transpileCompileRun(t, occam)
expected := "99\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}
func TestE2E_ReceiveIntoIndexedVariable(t *testing.T) {
occam := `SEQ
CHAN OF INT c:
[3]INT arr:
arr[0] := 0
arr[1] := 0
arr[2] := 0
PAR
c ! 42
c ? arr[1]
print.int(arr[1])
`
output := transpileCompileRun(t, occam)
expected := "42\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}
func TestE2E_IndexedChannelReceiveIntoIndexedVariable(t *testing.T) {
occam := `SEQ
[2]CHAN OF INT cs:
[3]INT arr:
arr[0] := 0
arr[1] := 0
arr[2] := 0
PAR
cs[0] ! 99
cs[0] ? arr[2]
print.int(arr[2])
`
output := transpileCompileRun(t, occam)
expected := "99\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}<tool_use_error>Found 2 matches of the string to replace, but replace_all is false. To replace all occurrences, set replace_all to true. To replace only one occurrence, please provide more context to uniquely identify the instance.
String: output := transpileCompileRun(t, occam)
expected := "99\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}</tool_use_error>{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go",
"offset": 370,
"limit": 25
} 370→`
371→ output := transpileCompileRun(t, occam)
372→ expected := "42\n"
373→ if output != expected {
374→ t.Errorf("expected %q, got %q", expected, output)
375→ }
376→}
377→
378→func TestE2E_PriPar(t *testing.T) {
379→ // Test PRI PAR: behaves the same as PAR in Go (no priority semantics)
380→ occam := `SEQ
381→ CHAN OF INT c:
382→ INT result:
383→ PRI PAR
384→ c ! 99
385→ SEQ
386→ c ? result
387→ print.int(result)
388→`
389→ output := transpileCompileRun(t, occam)
390→ expected := "99\n"
391→ if output != expected {
392→ t.Errorf("expected %q, got %q", expected, output)
393→ }
394→}func TestE2E_PriPar(t *testing.T) {
// Test PRI PAR: behaves the same as PAR in Go (no priority semantics)
occam := `SEQ
CHAN OF INT c:
INT result:
PRI PAR
c ! 99
SEQ
c ? result
print.int(result)
`
output := transpileCompileRun(t, occam)
expected := "99\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}func TestE2E_PriPar(t *testing.T) {
// Test PRI PAR: behaves the same as PAR in Go (no priority semantics)
occam := `SEQ
CHAN OF INT c:
INT result:
PRI PAR
c ! 99
SEQ
c ? result
print.int(result)
`
output := transpileCompileRun(t, occam)
expected := "99\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}
func TestE2E_ReceiveIntoIndexedVariable(t *testing.T) {
occam := `SEQ
CHAN OF INT c:
[3]INT arr:
arr[0] := 0
arr[1] := 0
arr[2] := 0
PAR
c ! 42
c ? arr[1]
print.int(arr[1])
`
output := transpileCompileRun(t, occam)
expected := "42\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}
func TestE2E_IndexedChannelReceiveIntoIndexedVariable(t *testing.T) {
occam := `SEQ
[2]CHAN OF INT cs:
[3]INT arr:
arr[0] := 0
arr[1] := 0
arr[2] := 0
PAR
cs[0] ! 99
cs[0] ? arr[2]
print.int(arr[2])
`
output := transpileCompileRun(t, occam)
expected := "99\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}The file /home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go has been updated successfully.
? github.com/codeassociates/occam2go [no test files] ? github.com/codeassociates/occam2go/ast [no test files] ok github.com/codeassociates/occam2go/codegen 19.126s ok github.com/codeassociates/occam2go/lexer (cached) ok github.com/codeassociates/occam2go/modgen (cached) ok github.com/codeassociates/occam2go/parser 0.004s ok github.com/codeassociates/occam2go/preproc (cached)
go build -o /tmp/occam2go . && /tmp/occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module && go vet /tmp/course_out.go 2>&1
All done. Here's a summary of the changes:
AST (ast/ast.go): Added VariableIndices []Expression to both Receive and AltCase structs.
Parser (parser/parser.go): Added index collection loops (for p.peekTokenIs(lexer.LBRACKET)) after every Variable = p.curToken.Literal site — 5 locations across parseReceive(), parseIndexedOperation(), and parseAltCase() (simple, indexed-channel, and guarded branches).
Codegen (codegen/codegen.go): At all 6 sites that build varRef from the variable name, added generateIndicesStr() when VariableIndices is non-empty — in generateReceive() (simple + sequential paths), generateAltBlock() (guarded, indexed-channel, and simple-channel branches), and generateReplicatedAlt().
Tests: 3 parser tests (TestReceiveIndexedVariable, TestReceiveMultiIndexedVariable, TestIndexedChannelReceiveIndexedVariable) and 2 e2e tests (TestE2E_ReceiveIntoIndexedVariable, TestE2E_IndexedChannelReceiveIntoIndexedVariable).
All tests pass including the course module vet check.
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_concurrency_test.go modified: parser/parser.go modified: parser/parser_test.go no changes added to commit (use "git add" and/or "git commit -a")
diff --git a/ast/ast.go b/ast/ast.go
index a504e54..634f647 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -373,10 +373,11 @@ func (s *Send) TokenLiteral() string { return s.Token.Literal }
// Receive represents a channel receive: c ? x or c ? x ; y
type Receive struct {
Token lexer.Token // the ? token
- Channel string // channel name
- ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x
- Variable string // variable to receive into (simple receive)
- Variables []string // additional variables for sequential receives (c ? x ; y)
+ Channel string // channel name
+ ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x
+ Variable string // variable to receive into (simple receive)
+ VariableIndices []Expression // non-empty for c ? flags[0] or c ? grid[i][j]
+ Variables []string // additional variables for sequential receives (c ? x ; y)
}
func (r *Receive) statementNode() {}
@@ -396,11 +397,12 @@ func (a *AltBlock) TokenLiteral() string { return a.Token.Literal }
// AltCase represents a single case in an ALT block
type AltCase struct {
- Guard Expression // optional guard condition (nil if no guard)
- Channel string // channel name
- ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT
- Variable string // variable to receive into
- Body []Statement // the body to execute
+ Guard Expression // optional guard condition (nil if no guard)
+ Channel string // channel name
+ ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT
+ Variable string // variable to receive into
+ VariableIndices []Expression // non-empty for c ? flags[0] or c ? grid[i][j]
+ Body []Statement // the body to execute
IsTimer bool // true if this is a timer AFTER case
IsSkip bool // true if this is a guarded SKIP case (guard & SKIP)
Timer string // timer name (when IsTimer)
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 849ec88..8d6e4c8 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -1346,7 +1346,9 @@ func (g *Generator) generateReceive(recv *ast.Receive) {
g.tmpCounter++
g.writeLine(fmt.Sprintf("%s := <-%s", tmpName, chanRef))
varRef := goIdent(recv.Variable)
- if g.refParams[recv.Variable] {
+ if len(recv.VariableIndices) > 0 {
+ varRef += g.generateIndicesStr(recv.VariableIndices)
+ } else if g.refParams[recv.Variable] {
varRef = "*" + varRef
}
g.writeLine(fmt.Sprintf("%s = %s._0", varRef, tmpName))
@@ -1359,7 +1361,9 @@ func (g *Generator) generateReceive(recv *ast.Receive) {
}
} else {
varRef := goIdent(recv.Variable)
- if g.refParams[recv.Variable] {
+ if len(recv.VariableIndices) > 0 {
+ varRef += g.generateIndicesStr(recv.VariableIndices)
+ } else if g.refParams[recv.Variable] {
varRef = "*" + varRef
}
g.writeLine(fmt.Sprintf("%s = <-%s", varRef, chanRef))
@@ -1903,13 +1907,25 @@ func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
g.generateExpression(c.Deadline)
g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
} else if c.Guard != nil {
- g.write(fmt.Sprintf("case %s = <-_alt%d:\n", goIdent(c.Variable), i))
+ varRef := goIdent(c.Variable)
+ if len(c.VariableIndices) > 0 {
+ varRef += g.generateIndicesStr(c.VariableIndices)
+ }
+ g.write(fmt.Sprintf("case %s = <-_alt%d:\n", varRef, i))
} else if len(c.ChannelIndices) > 0 {
- g.write(fmt.Sprintf("case %s = <-%s", goIdent(c.Variable), goIdent(c.Channel)))
+ varRef := goIdent(c.Variable)
+ if len(c.VariableIndices) > 0 {
+ varRef += g.generateIndicesStr(c.VariableIndices)
+ }
+ g.write(fmt.Sprintf("case %s = <-%s", varRef, goIdent(c.Channel)))
g.generateIndices(c.ChannelIndices)
g.write(":\n")
} else {
- g.write(fmt.Sprintf("case %s = <-%s:\n", goIdent(c.Variable), goIdent(c.Channel)))
+ varRef := goIdent(c.Variable)
+ if len(c.VariableIndices) > 0 {
+ varRef += g.generateIndicesStr(c.VariableIndices)
+ }
+ g.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel)))
}
g.indent++
guardedSkip := c.IsSkip && c.Guard != nil
@@ -2040,7 +2056,11 @@ func (g *Generator) generateReplicatedAlt(alt *ast.AltBlock) {
}
// Assign received value from reflect.Value
- g.writeLine(fmt.Sprintf("%s = _altValue.Interface().(%s)", goIdent(c.Variable), recvType))
+ varRef := goIdent(c.Variable)
+ if len(c.VariableIndices) > 0 {
+ varRef += g.generateIndicesStr(c.VariableIndices)
+ }
+ g.writeLine(fmt.Sprintf("%s = _altValue.Interface().(%s)", varRef, recvType))
// Generate body
for _, s := range c.Body {
diff --git a/codegen/e2e_concurrency_test.go b/codegen/e2e_concurrency_test.go
index 229120a..ccb8317 100644
--- a/codegen/e2e_concurrency_test.go
+++ b/codegen/e2e_concurrency_test.go
@@ -392,3 +392,41 @@ func TestE2E_PriPar(t *testing.T) {
t.Errorf("expected %q, got %q", expected, output)
}
}
+
+func TestE2E_ReceiveIntoIndexedVariable(t *testing.T) {
+ occam := `SEQ
+ CHAN OF INT c:
+ [3]INT arr:
+ arr[0] := 0
+ arr[1] := 0
+ arr[2] := 0
+ PAR
+ c ! 42
+ c ? arr[1]
+ print.int(arr[1])
+`
+ output := transpileCompileRun(t, occam)
+ expected := "42\n"
+ if output != expected {
+ t.Errorf("expected %q, got %q", expected, output)
+ }
+}
+
+func TestE2E_IndexedChannelReceiveIntoIndexedVariable(t *testing.T) {
+ occam := `SEQ
+ [2]CHAN OF INT cs:
+ [3]INT arr:
+ arr[0] := 0
+ arr[1] := 0
+ arr[2] := 0
+ PAR
+ cs[0] ! 99
+ cs[0] ? arr[2]
+ print.int(arr[2])
+`
+ output := transpileCompileRun(t, occam)
+ expected := "99\n"
+ if output != expected {
+ t.Errorf("expected %q, got %q", expected, output)
+ }
+}
diff --git a/parser/parser.go b/parser/parser.go
index 704fcb8..9e90386 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -791,6 +791,16 @@ func (p *Parser) parseIndexedOperation() ast.Statement {
}
stmt.Variable = p.curToken.Literal
+ // Collect variable indices: cs[i] ? flags[0] or cs[i] ? grid[j][k]
+ for p.peekTokenIs(lexer.LBRACKET) {
+ p.nextToken() // move to [
+ p.nextToken() // move past [
+ stmt.VariableIndices = append(stmt.VariableIndices, p.parseExpression(LOWEST))
+ if !p.expectPeek(lexer.RBRACKET) {
+ return nil
+ }
+ }
+
// Check for sequential receive
for p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken() // move to ;
@@ -1293,6 +1303,16 @@ func (p *Parser) parseReceive() ast.Statement {
}
stmt.Variable = p.curToken.Literal
+ // Collect variable indices: c ? flags[0] or c ? grid[i][j]
+ for p.peekTokenIs(lexer.LBRACKET) {
+ p.nextToken() // move to [
+ p.nextToken() // move past [
+ stmt.VariableIndices = append(stmt.VariableIndices, p.parseExpression(LOWEST))
+ if !p.expectPeek(lexer.RBRACKET) {
+ return nil
+ }
+ }
+
// Check for sequential receive: c ? x ; y ; z
for p.peekTokenIs(lexer.SEMICOLON) {
p.nextToken() // move to ;
@@ -1756,13 +1776,22 @@ func (p *Parser) parseAltCase() *ast.AltCase {
p.nextToken() // move past AFTER
altCase.Deadline = p.parseExpression(LOWEST)
} else {
- // Simple case: channel ? var
+ // Simple case: channel ? var or channel ? var[i]
altCase.Channel = name
p.nextToken() // move to ?
if !p.expectPeek(lexer.IDENT) {
return nil
}
altCase.Variable = p.curToken.Literal
+ // Collect variable indices: ch ? flags[0]
+ for p.peekTokenIs(lexer.LBRACKET) {
+ p.nextToken() // move to [
+ p.nextToken() // move past [
+ altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))
+ if !p.expectPeek(lexer.RBRACKET) {
+ return nil
+ }
+ }
}
} else if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.LBRACKET) {
// Indexed channel case: cs[i] ? var or cs[i][j] ? var
@@ -1783,6 +1812,15 @@ func (p *Parser) parseAltCase() *ast.AltCase {
return nil
}
altCase.Variable = p.curToken.Literal
+ // Collect variable indices: cs[i] ? flags[0]
+ for p.peekTokenIs(lexer.LBRACKET) {
+ p.nextToken() // move to [
+ p.nextToken() // move past [
+ altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))
+ if !p.expectPeek(lexer.RBRACKET) {
+ return nil
+ }
+ }
} else {
// Guard followed by & channel ? var, or guard & SKIP
guard := p.parseExpression(LOWEST)
@@ -1823,6 +1861,15 @@ func (p *Parser) parseAltCase() *ast.AltCase {
return nil
}
altCase.Variable = p.curToken.Literal
+ // Collect variable indices: guard & ch ? flags[0]
+ for p.peekTokenIs(lexer.LBRACKET) {
+ p.nextToken() // move to [
+ p.nextToken() // move past [
+ altCase.VariableIndices = append(altCase.VariableIndices, p.parseExpression(LOWEST))
+ if !p.expectPeek(lexer.RBRACKET) {
+ return nil
+ }
+ }
}
}
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 1002b29..1fed10a 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -3888,3 +3888,93 @@ PROC test(CHAN OF CMD ch)
t.Errorf("expected SeqBlock as second body statement, got %T", evolveCase.Body[1])
}
}
+
+func TestReceiveIndexedVariable(t *testing.T) {
+ input := `ch ? flags[0]
+`
+ l := lexer.New(input)
+ p := New(l)
+ program := p.ParseProgram()
+ checkParserErrors(t, p)
+
+ if len(program.Statements) != 1 {
+ t.Fatalf("expected 1 statement, got %d", len(program.Statements))
+ }
+
+ recv, ok := program.Statements[0].(*ast.Receive)
+ if !ok {
+ t.Fatalf("expected Receive, got %T", program.Statements[0])
+ }
+
+ if recv.Channel != "ch" {
+ t.Errorf("expected channel 'ch', got %s", recv.Channel)
+ }
+
+ if recv.Variable != "flags" {
+ t.Errorf("expected variable 'flags', got %s", recv.Variable)
+ }
+
+ if len(recv.VariableIndices) != 1 {
+ t.Fatalf("expected 1 variable index, got %d", len(recv.VariableIndices))
+ }
+}
+
+func TestReceiveMultiIndexedVariable(t *testing.T) {
+ input := `ch ? grid[i][j]
+`
+ l := lexer.New(input)
+ p := New(l)
+ program := p.ParseProgram()
+ checkParserErrors(t, p)
+
+ if len(program.Statements) != 1 {
+ t.Fatalf("expected 1 statement, got %d", len(program.Statements))
+ }
+
+ recv, ok := program.Statements[0].(*ast.Receive)
+ if !ok {
+ t.Fatalf("expected Receive, got %T", program.Statements[0])
+ }
+
+ if recv.Variable != "grid" {
+ t.Errorf("expected variable 'grid', got %s", recv.Variable)
+ }
+
+ if len(recv.VariableIndices) != 2 {
+ t.Fatalf("expected 2 variable indices, got %d", len(recv.VariableIndices))
+ }
+}
+
+func TestIndexedChannelReceiveIndexedVariable(t *testing.T) {
+ input := `cs[0] ? flags[1]
+`
+ l := lexer.New(input)
+ p := New(l)
+ program := p.ParseProgram()
+ checkParserErrors(t, p)
+
+ if len(program.Statements) != 1 {
+ t.Fatalf("expected 1 statement, got %d", len(program.Statements))
+ }
+
+ recv, ok := program.Statements[0].(*ast.Receive)
+ if !ok {
+ t.Fatalf("expected Receive, got %T", program.Statements[0])
+ }
+
+ if recv.Channel != "cs" {
+ t.Errorf("expected channel 'cs', got %s", recv.Channel)
+ }
+
+ if len(recv.ChannelIndices) != 1 {
+ t.Fatalf("expected 1 channel index, got %d", len(recv.ChannelIndices))
+ }
+
+ if recv.Variable != "flags" {
+ t.Errorf("expected variable 'flags', got %s", recv.Variable)
+ }
+
+ if len(recv.VariableIndices) != 1 {
+ t.Fatalf("expected 1 variable index, got %d", len(recv.VariableIndices))
+ }
+}32dedd5 Merge pull request #87 from codeassociates/fix/variant-receive-scoped-decl-86 694dcc3 Support scoped declarations in variant receive case bodies (#86) 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
git add ast/ast.go parser/parser.go parser/parser_test.go codegen/codegen.go codegen/e2e_concurrency_test.go && git commit -m "$(cat <<'EOF' Support receive into indexed target variables (#85) Channel receive (`ch ? var`) now accepts indexed targets like `flags[0]` or `grid[i][j]`, matching the existing pattern for indexed assignments. Also supports indexed targets in ALT cases. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> EOF )"
5 files changed, 213 insertions(+), 16 deletions(-)
remote: remote: Create a pull request for 'fix/receive-indexed-target-85' on GitHub by visiting: remote: https://github.com/codeassociates/occam2go/pull/new/fix/receive-indexed-target-85 remote: To associate-1.github.com:codeassociates/occam2go.git * [new branch] fix/receive-indexed-target-85 -> fix/receive-indexed-target-85 branch 'fix/receive-indexed-target-85' set up to track 'origin/fix/receive-indexed-target-85'.
gh pr create --title "Support receive into indexed target variables (#85)" --body "$(cat <<'EOF' ## Summary - Add `VariableIndices []Expression` to `Receive` and `AltCase` AST nodes, mirroring the existing `ChannelIndices` pattern - Parse index expressions after the target variable in channel receive (`ch ? flags[0]`, `cs[i] ? grid[j][k]`) across all 5 parser sites: `parseReceive()`, `parseIndexedOperation()`, and 3 branches of `parseAltCase()` - Generate indexed variable references in codegen across all 6 sites: `generateReceive()` (simple + sequential), `generateAltBlock()` (guarded, indexed-channel, simple-channel), and `generateReplicatedAlt()` Closes #85 ## Test plan - [x] 3 parser unit tests: single index, multi-index, both channel and variable indexed - [x] 2 e2e tests: `c ? arr[1]` and `cs[0] ? arr[2]` patterns - [x] All existing tests pass (`go test ./...`) - [x] Course module still transpiles and passes `go vet` 🤖 Generated with [Claude Code](https://claude.com/claude-code) EOF )"