Claude Code transcript - page 1/2

Implement the following plan:

Fix Parser Infinite Loop & Support Course Library Transpilation

Context

When transpiling programs that #INCLUDE "course.module", the parser enters an infinite loop causing OOM. The root cause is that occam allows multiple declarations (specifications) scoped before a process in IF/CASE/ALT/WHILE branch bodies, but the parser only calls parseStatement() once per body. After parsing the first declaration, it tries to interpret subsequent declarations as new branch conditions, looping forever.

A secondary issue is that channel direction annotations at call sites (out!, in?) aren't handled in expression parsing, causing the first parse error at line 188 of the preprocessed output.

Changes

1. AST: Change Body from Statement to []Statement (ast/ast.go)

Change in four structs: - IfChoice.Body (line ~207) - CaseChoice.Body (line ~223) - AltCase.Body (line ~397) - WhileLoop.Body (line ~192)

2. Parser: Add parseBodyStatements() helper (parser/parser.go)

Add a new function modeled on parseBlockStatements() (line 1649) that: - Is called after the caller has consumed the INDENT token - Loops parsing statements at the current indent level until DEDENT - Includes a "no progress" safety guard to prevent infinite loops - Returns []ast.Statement

Update four call sites to use it: - parseIfStatement() (line ~2225): replace parseStatement() with parseBodyStatements() - parseCaseStatement() (line ~2305): same - parseWhileLoop() (line ~2148): same - parseAltCase() (line ~1635): same

3. Parser: Handle channel direction at call sites (parser/parser.go)

In parseProcCall() (line 1893): after each parseExpression(LOWEST) for an argument, check if peekToken is SEND (!) or RECEIVE (?) and consume it. The direction annotation is informational — in Go codegen we just pass the channel.

4. Codegen: Update to iterate over body slices (codegen/codegen.go)

Generators — change g.generateStatement(x.Body) to loop: - generateWhileLoop() (line 1503) - generateIfStatement() (lines 1551, 1574) - generateCaseStatement() (line 1605) - generateAltBlock() (line 1252)

Scanner functions — change containsX(x.Body) to loop over slice: - containsPar() — AltCase (198), WhileLoop (215), IfChoice (220), CaseChoice (226) - containsPrint() — same pattern at ~258, 275, 280, 286 - containsTimer() — same pattern at ~309, 338, 343, 349 - containsStop() — same pattern at ~381, 398, 403, 409 - containsMostExpr() — same pattern at ~468, 476, 490, 511 - collectChanProtocols() — same pattern at ~901, 906, 912, 918 - collectRecordVars() — same pattern at ~957, 962, 968, 974

Note: VariantCase.Body has the same single-statement pattern but is left for a follow-up.

5. Tests

  • Update existing parser tests that access .Body (change nil checks to len() checks, access Body[0] instead of Body)
  • Add parser test for multi-statement IF body
  • Add parser test for channel direction at call site
  • Add e2e tests for both features

6. Documentation

Update TODO.md to note these features are supported.

Implementation Order

  1. AST changes (will cause compile errors)
  2. Parser changes (fixes parser compile errors)
  3. Codegen changes (fixes codegen compile errors)
  4. Test updates + new tests
  5. go test ./... to verify
  6. Try transpiling course library demo

Verification

go test ./...                    # all existing tests pass
go build -o occam2go .
./occam2go gen-module -o kroc/modules/course/libsrc/course.module kroc/modules/course/libsrc/SConscript
./occam2go -I kroc/modules/course/libsrc kroc/demos/complexity/concurrent_hello_1.occ

Note: The full course library transpilation may surface additional unsupported constructs (e.g., RETYPES in float_io.occ). Those would be addressed as follow-up work. The immediate goal is fixing the infinite loop and the two feature gaps that block parsing.

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/c48afd9c-f64c-4ecf-931b-d6fbb0089261.jsonl

Thinking

Let me start by understanding the current codebase state and then implement the changes in order. Let me read the key files first.

I'll implement this step by step. Let me start by reading the key files.

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/ast/ast.go"
}
     1→package ast
     2→
     3→import (
     4→	"github.com/codeassociates/occam2go/lexer"
     5→)
     6→
     7→// Node is the base interface for all AST nodes
     8→type Node interface {
     9→	TokenLiteral() string
    10→}
    11→
    12→// Statement represents a statement node
    13→type Statement interface {
    14→	Node
    15→	statementNode()
    16→}
    17→
    18→// Expression represents an expression node
    19→type Expression interface {
    20→	Node
    21→	expressionNode()
    22→}
    23→
    24→// Program is the root node of every AST
    25→type Program struct {
    26→	Statements []Statement
    27→}
    28→
    29→func (p *Program) TokenLiteral() string {
    30→	if len(p.Statements) > 0 {
    31→		return p.Statements[0].TokenLiteral()
    32→	}
    33→	return ""
    34→}
    35→
    36→// VarDecl represents a variable declaration: INT x:
    37→type VarDecl struct {
    38→	Token lexer.Token // the type token (INT, BYTE, BOOL)
    39→	Type  string      // "INT", "BYTE", "BOOL", etc.
    40→	Names []string    // variable names (can declare multiple: INT x, y, z:)
    41→}
    42→
    43→func (v *VarDecl) statementNode()       {}
    44→func (v *VarDecl) TokenLiteral() string { return v.Token.Literal }
    45→
    46→// ArrayDecl represents an array declaration: [5]INT arr:
    47→type ArrayDecl struct {
    48→	Token lexer.Token // the [ token
    49→	Size  Expression  // array size
    50→	Type  string      // element type ("INT", "BYTE", "BOOL", etc.)
    51→	Names []string    // variable names
    52→}
    53→
    54→func (a *ArrayDecl) statementNode()       {}
    55→func (a *ArrayDecl) TokenLiteral() string { return a.Token.Literal }
    56→
    57→// Assignment represents an assignment: x := 5 or arr[i] := 5 or [arr FROM n FOR m] := value
    58→type Assignment struct {
    59→	Token       lexer.Token // the := token
    60→	Name        string      // variable name
    61→	Index       Expression  // optional: index expression for arr[i] := x (nil for simple assignments)
    62→	SliceTarget *SliceExpr  // optional: slice target for [arr FROM n FOR m] := value
    63→	Value       Expression  // the value being assigned
    64→}
    65→
    66→func (a *Assignment) statementNode()       {}
    67→func (a *Assignment) TokenLiteral() string { return a.Token.Literal }
    68→
    69→// MultiAssignTarget represents one target in a multi-assignment.
    70→// Name is always set. Index is non-nil for indexed targets like arr[i].
    71→type MultiAssignTarget struct {
    72→	Name  string     // variable name
    73→	Index Expression // optional: index expression for arr[i] (nil for simple ident)
    74→}
    75→
    76→// MultiAssignment represents a multi-target assignment: a, b := func(x)
    77→type MultiAssignment struct {
    78→	Token   lexer.Token         // the := token
    79→	Targets []MultiAssignTarget // targets on the left side
    80→	Values  []Expression        // expressions on the right side
    81→}
    82→
    83→func (m *MultiAssignment) statementNode()       {}
    84→func (m *MultiAssignment) TokenLiteral() string { return m.Token.Literal }
    85→
    86→// SeqBlock represents a SEQ block (sequential execution)
    87→// If Replicator is non-nil, this is a replicated SEQ (SEQ i = 0 FOR n)
    88→type SeqBlock struct {
    89→	Token      lexer.Token // the SEQ token
    90→	Statements []Statement
    91→	Replicator *Replicator // optional replicator
    92→}
    93→
    94→func (s *SeqBlock) statementNode()       {}
    95→func (s *SeqBlock) TokenLiteral() string { return s.Token.Literal }
    96→
    97→// ParBlock represents a PAR block (parallel execution)
    98→// If Replicator is non-nil, this is a replicated PAR (PAR i = 0 FOR n)
    99→type ParBlock struct {
   100→	Token      lexer.Token // the PAR token
   101→	Statements []Statement
   102→	Replicator *Replicator // optional replicator
   103→}
   104→
   105→func (p *ParBlock) statementNode()       {}
   106→func (p *ParBlock) TokenLiteral() string { return p.Token.Literal }
   107→
   108→// Replicator represents a replication spec: i = start FOR count [STEP step]
   109→type Replicator struct {
   110→	Variable string     // loop variable name
   111→	Start    Expression // start value
   112→	Count    Expression // number of iterations
   113→	Step     Expression // optional step value (nil means step of 1)
   114→}
   115→
   116→// Skip represents the SKIP statement (no-op)
   117→type Skip struct {
   118→	Token lexer.Token
   119→}
   120→
   121→func (s *Skip) statementNode()       {}
   122→func (s *Skip) TokenLiteral() string { return s.Token.Literal }
   123→
   124→// Stop represents the STOP statement (deadlock/halt)
   125→type Stop struct {
   126→	Token lexer.Token
   127→}
   128→
   129→func (s *Stop) statementNode()       {}
   130→func (s *Stop) TokenLiteral() string { return s.Token.Literal }
   131→
   132→// ProcDecl represents a procedure declaration
   133→type ProcDecl struct {
   134→	Token  lexer.Token // the PROC token
   135→	Name   string
   136→	Params []ProcParam
   137→	Body   []Statement // local declarations + body process
   138→}
   139→
   140→func (p *ProcDecl) statementNode()       {}
   141→func (p *ProcDecl) TokenLiteral() string { return p.Token.Literal }
   142→
   143→// ProcParam represents a procedure parameter
   144→type ProcParam struct {
   145→	IsVal        bool   // VAL parameter (pass by value)
   146→	Type         string // INT, BYTE, BOOL, etc.
   147→	Name         string
   148→	IsChan       bool   // true if this is a CHAN OF <type> parameter
   149→	IsChanArray  bool   // true for []CHAN OF TYPE params
   150→	IsOpenArray  bool   // true for []TYPE params (open array)
   151→	ChanElemType string // element type when IsChan (e.g., "INT")
   152→	ChanDir      string // "?" for input, "!" for output, "" for bidirectional
   153→}
   154→
   155→// ProcCall represents a procedure call
   156→type ProcCall struct {
   157→	Token lexer.Token // the procedure name token
   158→	Name  string
   159→	Args  []Expression
   160→}
   161→
   162→func (p *ProcCall) statementNode()       {}
   163→func (p *ProcCall) TokenLiteral() string { return p.Token.Literal }
   164→
   165→// FuncDecl represents a function declaration (single or multi-result)
   166→type FuncDecl struct {
   167→	Token       lexer.Token    // the return type token
   168→	ReturnTypes []string       // return types: ["INT"], ["INT", "INT"], etc.
   169→	Name        string
   170→	Params      []ProcParam
   171→	Body        []Statement    // local decls + body statements (VALOF form), empty for IS form
   172→	ResultExprs []Expression   // return expressions (from IS or RESULT)
   173→}
   174→
   175→func (f *FuncDecl) statementNode()       {}
   176→func (f *FuncDecl) TokenLiteral() string { return f.Token.Literal }
   177→
   178→// FuncCall represents a function call expression
   179→type FuncCall struct {
   180→	Token lexer.Token // the function name token
   181→	Name  string
   182→	Args  []Expression
   183→}
   184→
   185→func (f *FuncCall) expressionNode()      {}
   186→func (f *FuncCall) TokenLiteral() string { return f.Token.Literal }
   187→
   188→// WhileLoop represents a WHILE loop
   189→type WhileLoop struct {
   190→	Token     lexer.Token // the WHILE token
   191→	Condition Expression
   192→	Body      Statement
   193→}
   194→
   195→func (w *WhileLoop) statementNode()       {}
   196→func (w *WhileLoop) TokenLiteral() string { return w.Token.Literal }
   197→
   198→// IfStatement represents an IF statement
   199→type IfStatement struct {
   200→	Token      lexer.Token // the IF token
   201→	Choices    []IfChoice
   202→	Replicator *Replicator // optional replicator for IF i = start FOR count
   203→}
   204→
   205→type IfChoice struct {
   206→	Condition Expression
   207→	Body      Statement
   208→}
   209→
   210→func (i *IfStatement) statementNode()       {}
   211→func (i *IfStatement) TokenLiteral() string { return i.Token.Literal }
   212→
   213→// CaseStatement represents a CASE statement
   214→type CaseStatement struct {
   215→	Token    lexer.Token  // the CASE token
   216→	Selector Expression   // the selector expression
   217→	Choices  []CaseChoice
   218→}
   219→
   220→type CaseChoice struct {
   221→	Values []Expression // nil/empty for ELSE
   222→	IsElse bool
   223→	Body   Statement
   224→}
   225→
   226→func (c *CaseStatement) statementNode()       {}
   227→func (c *CaseStatement) TokenLiteral() string { return c.Token.Literal }
   228→
   229→// Expressions
   230→
   231→// Identifier represents a variable reference
   232→type Identifier struct {
   233→	Token lexer.Token
   234→	Value string
   235→}
   236→
   237→func (i *Identifier) expressionNode()      {}
   238→func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
   239→
   240→// IntegerLiteral represents an integer literal
   241→type IntegerLiteral struct {
   242→	Token lexer.Token
   243→	Value int64
   244→}
   245→
   246→func (il *IntegerLiteral) expressionNode()      {}
   247→func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal }
   248→
   249→// BooleanLiteral represents TRUE or FALSE
   250→type BooleanLiteral struct {
   251→	Token lexer.Token
   252→	Value bool
   253→}
   254→
   255→func (bl *BooleanLiteral) expressionNode()      {}
   256→func (bl *BooleanLiteral) TokenLiteral() string { return bl.Token.Literal }
   257→
   258→// StringLiteral represents a string literal: "hello"
   259→type StringLiteral struct {
   260→	Token lexer.Token
   261→	Value string
   262→}
   263→
   264→func (sl *StringLiteral) expressionNode()      {}
   265→func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal }
   266→
   267→// ByteLiteral represents a byte literal: 'A', '*n', etc.
   268→type ByteLiteral struct {
   269→	Token lexer.Token
   270→	Value byte
   271→}
   272→
   273→func (bl *ByteLiteral) expressionNode()      {}
   274→func (bl *ByteLiteral) TokenLiteral() string { return bl.Token.Literal }
   275→
   276→// BinaryExpr represents a binary operation: x + y
   277→type BinaryExpr struct {
   278→	Token    lexer.Token // the operator token
   279→	Left     Expression
   280→	Operator string
   281→	Right    Expression
   282→}
   283→
   284→func (be *BinaryExpr) expressionNode()      {}
   285→func (be *BinaryExpr) TokenLiteral() string { return be.Token.Literal }
   286→
   287→// UnaryExpr represents a unary operation: NOT x, -x
   288→type UnaryExpr struct {
   289→	Token    lexer.Token // the operator token
   290→	Operator string
   291→	Right    Expression
   292→}
   293→
   294→func (ue *UnaryExpr) expressionNode()      {}
   295→func (ue *UnaryExpr) TokenLiteral() string { return ue.Token.Literal }
   296→
   297→// TypeConversion represents a type conversion expression: INT x, BYTE n, etc.
   298→type TypeConversion struct {
   299→	Token      lexer.Token // the type token (INT, BYTE, etc.)
   300→	TargetType string      // "INT", "BYTE", "BOOL", "REAL"
   301→	Expr       Expression  // the expression to convert
   302→}
   303→
   304→func (tc *TypeConversion) expressionNode()      {}
   305→func (tc *TypeConversion) TokenLiteral() string { return tc.Token.Literal }
   306→
   307→// SizeExpr represents a SIZE expression: SIZE arr
   308→type SizeExpr struct {
   309→	Token lexer.Token // the SIZE token
   310→	Expr  Expression  // the array/string expression
   311→}
   312→
   313→func (se *SizeExpr) expressionNode()      {}
   314→func (se *SizeExpr) TokenLiteral() string { return se.Token.Literal }
   315→
   316→// MostExpr represents MOSTNEG/MOSTPOS type expressions: MOSTNEG INT, MOSTPOS BYTE, etc.
   317→type MostExpr struct {
   318→	Token    lexer.Token // the MOSTNEG or MOSTPOS token
   319→	ExprType string      // "INT", "BYTE", "REAL32", "REAL64", etc.
   320→	IsNeg    bool        // true for MOSTNEG, false for MOSTPOS
   321→}
   322→
   323→func (me *MostExpr) expressionNode()      {}
   324→func (me *MostExpr) TokenLiteral() string { return me.Token.Literal }
   325→
   326→// ParenExpr represents a parenthesized expression
   327→type ParenExpr struct {
   328→	Token lexer.Token
   329→	Expr  Expression
   330→}
   331→
   332→func (pe *ParenExpr) expressionNode()      {}
   333→func (pe *ParenExpr) TokenLiteral() string { return pe.Token.Literal }
   334→
   335→// IndexExpr represents an array index expression: arr[i]
   336→type IndexExpr struct {
   337→	Token lexer.Token // the [ token
   338→	Left  Expression  // the array expression
   339→	Index Expression  // the index expression
   340→}
   341→
   342→func (ie *IndexExpr) expressionNode()      {}
   343→func (ie *IndexExpr) TokenLiteral() string { return ie.Token.Literal }
   344→
   345→// ChanDecl represents a channel declaration: CHAN OF INT c: or [n]CHAN OF INT cs:
   346→type ChanDecl struct {
   347→	Token    lexer.Token // the CHAN token
   348→	ElemType string      // the element type (INT, BYTE, etc.)
   349→	Names    []string    // channel names
   350→	IsArray  bool        // true for [n]CHAN OF TYPE
   351→	Size     Expression  // array size when IsArray
   352→}
   353→
   354→func (c *ChanDecl) statementNode()       {}
   355→func (c *ChanDecl) TokenLiteral() string { return c.Token.Literal }
   356→
   357→// Send represents a channel send: c ! x or c ! x ; y or c ! tag ; x
   358→type Send struct {
   359→	Token        lexer.Token  // the ! token
   360→	Channel      string       // channel name
   361→	ChannelIndex Expression   // non-nil for cs[i] ! value
   362→	Value        Expression   // value to send (simple send, backward compat)
   363→	Values       []Expression // additional values for sequential sends (c ! x ; y)
   364→	VariantTag   string       // variant tag name for variant sends (c ! tag ; x)
   365→}
   366→
   367→func (s *Send) statementNode()       {}
   368→func (s *Send) TokenLiteral() string { return s.Token.Literal }
   369→
   370→// Receive represents a channel receive: c ? x or c ? x ; y
   371→type Receive struct {
   372→	Token        lexer.Token // the ? token
   373→	Channel      string      // channel name
   374→	ChannelIndex Expression  // non-nil for cs[i] ? x
   375→	Variable     string      // variable to receive into (simple receive)
   376→	Variables    []string    // additional variables for sequential receives (c ? x ; y)
   377→}
   378→
   379→func (r *Receive) statementNode()       {}
   380→func (r *Receive) TokenLiteral() string { return r.Token.Literal }
   381→
   382→// AltBlock represents an ALT block (alternation/select)
   383→type AltBlock struct {
   384→	Token lexer.Token // the ALT token
   385→	Cases []AltCase
   386→}
   387→
   388→func (a *AltBlock) statementNode()       {}
   389→func (a *AltBlock) TokenLiteral() string { return a.Token.Literal }
   390→
   391→// AltCase represents a single case in an ALT block
   392→type AltCase struct {
   393→	Guard        Expression // optional guard condition (nil if no guard)
   394→	Channel      string     // channel name
   395→	ChannelIndex Expression // non-nil for cs[i] ? x in ALT
   396→	Variable     string     // variable to receive into
   397→	Body         Statement  // the body to execute
   398→	IsTimer      bool       // true if this is a timer AFTER case
   399→	Timer        string     // timer name (when IsTimer)
   400→	Deadline     Expression // AFTER deadline expression (when IsTimer)
   401→}
   402→
   403→// TimerDecl represents a timer declaration: TIMER tim:
   404→type TimerDecl struct {
   405→	Token lexer.Token // the TIMER token
   406→	Names []string    // timer variable names
   407→}
   408→
   409→func (td *TimerDecl) statementNode()       {}
   410→func (td *TimerDecl) TokenLiteral() string { return td.Token.Literal }
   411→
   412→// TimerRead represents a timer read: tim ? t
   413→type TimerRead struct {
   414→	Token    lexer.Token // the ? token
   415→	Timer    string      // timer name
   416→	Variable string      // variable to receive time into
   417→}
   418→
   419→func (tr *TimerRead) statementNode()       {}
   420→func (tr *TimerRead) TokenLiteral() string { return tr.Token.Literal }
   421→
   422→// ProtocolDecl represents a protocol declaration
   423→type ProtocolDecl struct {
   424→	Token    lexer.Token       // the PROTOCOL token
   425→	Name     string            // protocol name
   426→	Kind     string            // "simple", "sequential", or "variant"
   427→	Types    []string          // element types (simple: len=1, sequential: len>1)
   428→	Variants []ProtocolVariant // only for Kind="variant"
   429→}
   430→
   431→type ProtocolVariant struct {
   432→	Tag   string   // tag name (e.g., "text", "quit")
   433→	Types []string // associated types (empty for no-payload tags)
   434→}
   435→
   436→func (pd *ProtocolDecl) statementNode()       {}
   437→func (pd *ProtocolDecl) TokenLiteral() string { return pd.Token.Literal }
   438→
   439→// VariantReceive represents a variant protocol receive: c ? CASE ...
   440→type VariantReceive struct {
   441→	Token        lexer.Token // the ? token
   442→	Channel      string
   443→	ChannelIndex Expression // non-nil for cs[i] ? CASE ...
   444→	Cases        []VariantCase
   445→}
   446→
   447→type VariantCase struct {
   448→	Tag       string    // variant tag name
   449→	Variables []string  // variables to bind payload fields
   450→	Body      Statement
   451→}
   452→
   453→func (vr *VariantReceive) statementNode()       {}
   454→func (vr *VariantReceive) TokenLiteral() string { return vr.Token.Literal }
   455→
   456→// RecordDecl represents a record type declaration: RECORD POINT { INT x: INT y: }
   457→type RecordDecl struct {
   458→	Token  lexer.Token   // the RECORD token
   459→	Name   string        // record type name
   460→	Fields []RecordField // named fields
   461→}
   462→
   463→type RecordField struct {
   464→	Type string // "INT", "BYTE", "BOOL", "REAL"
   465→	Name string
   466→}
   467→
   468→func (rd *RecordDecl) statementNode()       {}
   469→func (rd *RecordDecl) TokenLiteral() string { return rd.Token.Literal }
   470→
   471→// SliceExpr represents an array slice: [arr FROM start FOR length]
   472→type SliceExpr struct {
   473→	Token  lexer.Token // the [ token
   474→	Array  Expression  // the array being sliced
   475→	Start  Expression  // start index
   476→	Length Expression  // number of elements
   477→}
   478→
   479→func (se *SliceExpr) expressionNode()      {}
   480→func (se *SliceExpr) TokenLiteral() string { return se.Token.Literal }
   481→
   482→// Abbreviation represents an abbreviation: VAL INT x IS 42:, INT y IS z:, or INITIAL INT x IS 42:
   483→type Abbreviation struct {
   484→	Token     lexer.Token // VAL, INITIAL, or type token
   485→	IsVal     bool        // true for VAL abbreviations
   486→	IsInitial bool        // true for INITIAL declarations
   487→	Type      string      // "INT", "BYTE", "BOOL", etc.
   488→	Name      string      // variable name
   489→	Value     Expression  // the expression
   490→}
   491→
   492→func (a *Abbreviation) statementNode()       {}
   493→func (a *Abbreviation) TokenLiteral() string { return a.Token.Literal }
   494→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser.go"
}
     1→package parser
     2→
     3→import (
     4→	"fmt"
     5→	"strconv"
     6→	"strings"
     7→
     8→	"github.com/codeassociates/occam2go/ast"
     9→	"github.com/codeassociates/occam2go/lexer"
    10→)
    11→
    12→// Operator precedence levels
    13→const (
    14→	_ int = iota
    15→	LOWEST
    16→	OR_PREC      // OR
    17→	AND_PREC     // AND
    18→	EQUALS       // =, <>
    19→	LESSGREATER  // <, >, <=, >=
    20→	SUM          // +, -
    21→	PRODUCT      // *, /, \
    22→	PREFIX       // -x, NOT x
    23→	INDEX        // arr[i]
    24→)
    25→
    26→var precedences = map[lexer.TokenType]int{
    27→	lexer.OR:       OR_PREC,
    28→	lexer.AND:      AND_PREC,
    29→	lexer.EQ:       EQUALS,
    30→	lexer.NEQ:      EQUALS,
    31→	lexer.LT:       LESSGREATER,
    32→	lexer.GT:       LESSGREATER,
    33→	lexer.LE:       LESSGREATER,
    34→	lexer.GE:       LESSGREATER,
    35→	lexer.AFTER:    LESSGREATER,
    36→	lexer.PLUS:     SUM,
    37→	lexer.MINUS:    SUM,
    38→	lexer.PLUS_KW:  SUM,
    39→	lexer.MINUS_KW: SUM,
    40→	lexer.MULTIPLY: PRODUCT,
    41→	lexer.DIVIDE:   PRODUCT,
    42→	lexer.MODULO:   PRODUCT,
    43→	lexer.TIMES:    PRODUCT,
    44→	lexer.BITAND:   PRODUCT,
    45→	lexer.LSHIFT:   PRODUCT,
    46→	lexer.RSHIFT:   PRODUCT,
    47→	lexer.BITOR:    SUM,
    48→	lexer.BITXOR:   SUM,
    49→	lexer.LBRACKET: INDEX,
    50→}
    51→
    52→type Parser struct {
    53→	l      *lexer.Lexer
    54→	errors []string
    55→
    56→	curToken  lexer.Token
    57→	peekToken lexer.Token
    58→
    59→	// Track current indentation level
    60→	indentLevel int
    61→
    62→	// Track timer names to distinguish timer reads from channel receives
    63→	timerNames map[string]bool
    64→
    65→	// Track protocol names and definitions
    66→	protocolNames map[string]bool
    67→	protocolDefs  map[string]*ast.ProtocolDecl
    68→
    69→	// Track record type names and definitions
    70→	recordNames map[string]bool
    71→	recordDefs  map[string]*ast.RecordDecl
    72→}
    73→
    74→func New(l *lexer.Lexer) *Parser {
    75→	p := &Parser{
    76→		l:             l,
    77→		errors:        []string{},
    78→		timerNames:    make(map[string]bool),
    79→		protocolNames: make(map[string]bool),
    80→		protocolDefs:  make(map[string]*ast.ProtocolDecl),
    81→		recordNames:   make(map[string]bool),
    82→		recordDefs:    make(map[string]*ast.RecordDecl),
    83→	}
    84→	// Read two tokens to initialize curToken and peekToken
    85→	p.nextToken()
    86→	p.nextToken()
    87→	return p
    88→}
    89→
    90→func (p *Parser) Errors() []string {
    91→	return p.errors
    92→}
    93→
    94→func (p *Parser) addError(msg string) {
    95→	p.errors = append(p.errors, fmt.Sprintf("line %d: %s", p.curToken.Line, msg))
    96→}
    97→
    98→func (p *Parser) nextToken() {
    99→	p.curToken = p.peekToken
   100→	p.peekToken = p.l.NextToken()
   101→
   102→	// Track indentation level
   103→	if p.curToken.Type == lexer.INDENT {
   104→		p.indentLevel++
   105→	} else if p.curToken.Type == lexer.DEDENT {
   106→		p.indentLevel--
   107→	}
   108→}
   109→
   110→func (p *Parser) curTokenIs(t lexer.TokenType) bool {
   111→	return p.curToken.Type == t
   112→}
   113→
   114→func (p *Parser) peekTokenIs(t lexer.TokenType) bool {
   115→	return p.peekToken.Type == t
   116→}
   117→
   118→func (p *Parser) expectPeek(t lexer.TokenType) bool {
   119→	if p.peekTokenIs(t) {
   120→		p.nextToken()
   121→		return true
   122→	}
   123→	p.addError(fmt.Sprintf("expected %s, got %s", t, p.peekToken.Type))
   124→	return false
   125→}
   126→
   127→func (p *Parser) peekPrecedence() int {
   128→	if prec, ok := precedences[p.peekToken.Type]; ok {
   129→		return prec
   130→	}
   131→	return LOWEST
   132→}
   133→
   134→func (p *Parser) curPrecedence() int {
   135→	if prec, ok := precedences[p.curToken.Type]; ok {
   136→		return prec
   137→	}
   138→	return LOWEST
   139→}
   140→
   141→// ParseProgram parses the entire program
   142→func (p *Parser) ParseProgram() *ast.Program {
   143→	program := &ast.Program{}
   144→	program.Statements = []ast.Statement{}
   145→
   146→	for !p.curTokenIs(lexer.EOF) {
   147→		stmt := p.parseStatement()
   148→		if stmt != nil {
   149→			program.Statements = append(program.Statements, stmt)
   150→		}
   151→		p.nextToken()
   152→	}
   153→
   154→	return program
   155→}
   156→
   157→func (p *Parser) parseStatement() ast.Statement {
   158→	// Skip newlines
   159→	for p.curTokenIs(lexer.NEWLINE) {
   160→		p.nextToken()
   161→	}
   162→
   163→	switch p.curToken.Type {
   164→	case lexer.VAL:
   165→		return p.parseAbbreviation()
   166→	case lexer.INITIAL:
   167→		return p.parseInitialDecl()
   168→	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
   169→		if p.peekTokenIs(lexer.FUNCTION) || p.peekTokenIs(lexer.FUNC) || p.peekTokenIs(lexer.COMMA) {
   170→			return p.parseFuncDecl()
   171→		}
   172→		return p.parseVarDeclOrAbbreviation()
   173→	case lexer.LBRACKET:
   174→		return p.parseArrayDecl()
   175→	case lexer.CHAN:
   176→		return p.parseChanDecl()
   177→	case lexer.PROTOCOL:
   178→		return p.parseProtocolDecl()
   179→	case lexer.RECORD:
   180→		return p.parseRecordDecl()
   181→	case lexer.TIMER:
   182→		return p.parseTimerDecl()
   183→	case lexer.SEQ:
   184→		return p.parseSeqBlock()
   185→	case lexer.PAR:
   186→		return p.parseParBlock()
   187→	case lexer.ALT:
   188→		return p.parseAltBlock()
   189→	case lexer.SKIP:
   190→		return &ast.Skip{Token: p.curToken}
   191→	case lexer.STOP:
   192→		return &ast.Stop{Token: p.curToken}
   193→	case lexer.PROC:
   194→		return p.parseProcDecl()
   195→	case lexer.WHILE:
   196→		return p.parseWhileLoop()
   197→	case lexer.IF:
   198→		return p.parseIfStatement()
   199→	case lexer.CASE:
   200→		return p.parseCaseStatement()
   201→	case lexer.IDENT:
   202→		// Check for record variable declaration: TYPENAME var:
   203→		if p.recordNames[p.curToken.Literal] && p.peekTokenIs(lexer.IDENT) {
   204→			return p.parseRecordVarDecl()
   205→		}
   206→		// Could be assignment, indexed assignment, indexed send/receive, send, receive, or procedure call
   207→		if p.peekTokenIs(lexer.LBRACKET) {
   208→			return p.parseIndexedOperation()
   209→		}
   210→		if p.peekTokenIs(lexer.ASSIGN) {
   211→			return p.parseAssignment()
   212→		}
   213→		if p.peekTokenIs(lexer.COMMA) {
   214→			return p.parseMultiAssignment()
   215→		}
   216→		if p.peekTokenIs(lexer.SEND) {
   217→			return p.parseSend()
   218→		}
   219→		if p.peekTokenIs(lexer.RECEIVE) {
   220→			if p.timerNames[p.curToken.Literal] {
   221→				return p.parseTimerRead()
   222→			}
   223→			return p.parseReceive()
   224→		}
   225→		return p.parseProcCall()
   226→	case lexer.INDENT, lexer.DEDENT, lexer.EOF:
   227→		return nil
   228→	default:
   229→		p.addError(fmt.Sprintf("unexpected token: %s", p.curToken.Type))
   230→		return nil
   231→	}
   232→}
   233→
   234→func (p *Parser) parseVarDecl() *ast.VarDecl {
   235→	decl := &ast.VarDecl{
   236→		Token: p.curToken,
   237→		Type:  p.curToken.Literal,
   238→	}
   239→
   240→	// Parse variable names
   241→	for {
   242→		if !p.expectPeek(lexer.IDENT) {
   243→			return nil
   244→		}
   245→		decl.Names = append(decl.Names, p.curToken.Literal)
   246→
   247→		if p.peekTokenIs(lexer.COMMA) {
   248→			p.nextToken() // consume comma
   249→		} else {
   250→			break
   251→		}
   252→	}
   253→
   254→	if !p.expectPeek(lexer.COLON) {
   255→		return nil
   256→	}
   257→
   258→	return decl
   259→}
   260→
   261→// parseVarDeclOrAbbreviation parses either a variable declaration (INT x:)
   262→// or a non-VAL abbreviation (INT x IS expr:). Called when current token is a type keyword.
   263→func (p *Parser) parseVarDeclOrAbbreviation() ast.Statement {
   264→	typeToken := p.curToken
   265→	typeName := p.curToken.Literal
   266→
   267→	// Consume the name
   268→	if !p.expectPeek(lexer.IDENT) {
   269→		return nil
   270→	}
   271→	name := p.curToken.Literal
   272→
   273→	// Check if this is an abbreviation (next token is IS)
   274→	if p.peekTokenIs(lexer.IS) {
   275→		p.nextToken() // consume IS
   276→		p.nextToken() // move to expression
   277→		value := p.parseExpression(LOWEST)
   278→
   279→		if !p.expectPeek(lexer.COLON) {
   280→			return nil
   281→		}
   282→
   283→		return &ast.Abbreviation{
   284→			Token: typeToken,
   285→			IsVal: false,
   286→			Type:  typeName,
   287→			Name:  name,
   288→			Value: value,
   289→		}
   290→	}
   291→
   292→	// Otherwise, it's a regular variable declaration — continue parsing names
   293→	decl := &ast.VarDecl{
   294→		Token: typeToken,
   295→		Type:  typeName,
   296→		Names: []string{name},
   297→	}
   298→
   299→	// Parse additional comma-separated names
   300→	for p.peekTokenIs(lexer.COMMA) {
   301→		p.nextToken() // consume comma
   302→		if !p.expectPeek(lexer.IDENT) {
   303→			return nil
   304→		}
   305→		decl.Names = append(decl.Names, p.curToken.Literal)
   306→	}
   307→
   308→	if !p.expectPeek(lexer.COLON) {
   309→		return nil
   310→	}
   311→
   312→	return decl
   313→}
   314→
   315→// parseAbbreviation parses a VAL abbreviation: VAL INT x IS expr:
   316→// Current token is VAL.
   317→func (p *Parser) parseAbbreviation() *ast.Abbreviation {
   318→	token := p.curToken // VAL token
   319→
   320→	// Expect a type keyword
   321→	p.nextToken()
   322→	if !p.curTokenIs(lexer.INT_TYPE) && !p.curTokenIs(lexer.BYTE_TYPE) &&
   323→		!p.curTokenIs(lexer.BOOL_TYPE) && !p.curTokenIs(lexer.REAL_TYPE) &&
   324→		!p.curTokenIs(lexer.REAL32_TYPE) && !p.curTokenIs(lexer.REAL64_TYPE) {
   325→		p.addError(fmt.Sprintf("expected type after VAL, got %s", p.curToken.Type))
   326→		return nil
   327→	}
   328→	typeName := p.curToken.Literal
   329→
   330→	// Expect name
   331→	if !p.expectPeek(lexer.IDENT) {
   332→		return nil
   333→	}
   334→	name := p.curToken.Literal
   335→
   336→	// Expect IS
   337→	if !p.expectPeek(lexer.IS) {
   338→		return nil
   339→	}
   340→
   341→	// Parse expression
   342→	p.nextToken()
   343→	value := p.parseExpression(LOWEST)
   344→
   345→	// Expect colon
   346→	if !p.expectPeek(lexer.COLON) {
   347→		return nil
   348→	}
   349→
   350→	return &ast.Abbreviation{
   351→		Token: token,
   352→		IsVal: true,
   353→		Type:  typeName,
   354→		Name:  name,
   355→		Value: value,
   356→	}
   357→}
   358→
   359→// parseInitialDecl parses an INITIAL declaration: INITIAL INT x IS expr:
   360→// Current token is INITIAL.
   361→func (p *Parser) parseInitialDecl() *ast.Abbreviation {
   362→	token := p.curToken // INITIAL token
   363→
   364→	// Expect a type keyword
   365→	p.nextToken()
   366→	if !p.curTokenIs(lexer.INT_TYPE) && !p.curTokenIs(lexer.BYTE_TYPE) &&
   367→		!p.curTokenIs(lexer.BOOL_TYPE) && !p.curTokenIs(lexer.REAL_TYPE) &&
   368→		!p.curTokenIs(lexer.REAL32_TYPE) && !p.curTokenIs(lexer.REAL64_TYPE) {
   369→		p.addError(fmt.Sprintf("expected type after INITIAL, got %s", p.curToken.Type))
   370→		return nil
   371→	}
   372→	typeName := p.curToken.Literal
   373→
   374→	// Expect name
   375→	if !p.expectPeek(lexer.IDENT) {
   376→		return nil
   377→	}
   378→	name := p.curToken.Literal
   379→
   380→	// Expect IS
   381→	if !p.expectPeek(lexer.IS) {
   382→		return nil
   383→	}
   384→
   385→	// Parse expression
   386→	p.nextToken()
   387→	value := p.parseExpression(LOWEST)
   388→
   389→	// Expect colon
   390→	if !p.expectPeek(lexer.COLON) {
   391→		return nil
   392→	}
   393→
   394→	return &ast.Abbreviation{
   395→		Token:     token,
   396→		IsInitial: true,
   397→		Type:      typeName,
   398→		Name:      name,
   399→		Value:     value,
   400→	}
   401→}
   402→
   403→func (p *Parser) parseAssignment() *ast.Assignment {
   404→	stmt := &ast.Assignment{
   405→		Name: p.curToken.Literal,
   406→	}
   407→
   408→	p.nextToken() // move to :=
   409→	stmt.Token = p.curToken
   410→
   411→	p.nextToken() // move past :=
   412→	stmt.Value = p.parseExpression(LOWEST)
   413→
   414→	return stmt
   415→}
   416→
   417→func (p *Parser) parseMultiAssignment() *ast.MultiAssignment {
   418→	firstTarget := ast.MultiAssignTarget{Name: p.curToken.Literal}
   419→	return p.parseMultiAssignmentFrom(firstTarget)
   420→}
   421→
   422→// parseMultiAssignmentFrom parses a multi-assignment given the first target already parsed.
   423→// The current token should be on the first target's last token (ident or ']').
   424→// Peek token should be COMMA.
   425→func (p *Parser) parseMultiAssignmentFrom(firstTarget ast.MultiAssignTarget) *ast.MultiAssignment {
   426→	stmt := &ast.MultiAssignment{
   427→		Targets: []ast.MultiAssignTarget{firstTarget},
   428→	}
   429→
   430→	// Parse comma-separated targets: a, b[i], c
   431→	for p.peekTokenIs(lexer.COMMA) {
   432→		p.nextToken() // consume comma
   433→		p.nextToken() // move to next target
   434→		target := ast.MultiAssignTarget{Name: p.curToken.Literal}
   435→		if p.peekTokenIs(lexer.LBRACKET) {
   436→			p.nextToken() // move to [
   437→			p.nextToken() // move past [
   438→			target.Index = p.parseExpression(LOWEST)
   439→			if !p.expectPeek(lexer.RBRACKET) {
   440→				return nil
   441→			}
   442→		}
   443→		stmt.Targets = append(stmt.Targets, target)
   444→	}
   445→
   446→	p.nextToken() // move to :=
   447→	stmt.Token = p.curToken
   448→
   449→	p.nextToken() // move past :=
   450→
   451→	// Parse comma-separated values
   452→	stmt.Values = []ast.Expression{p.parseExpression(LOWEST)}
   453→	for p.peekTokenIs(lexer.COMMA) {
   454→		p.nextToken() // consume comma
   455→		p.nextToken() // move to next expression
   456→		stmt.Values = append(stmt.Values, p.parseExpression(LOWEST))
   457→	}
   458→
   459→	return stmt
   460→}
   461→
   462→func (p *Parser) parseArrayDecl() ast.Statement {
   463→	lbracketToken := p.curToken
   464→
   465→	// Parse size expression after [
   466→	p.nextToken()
   467→	size := p.parseExpression(LOWEST)
   468→
   469→	// Check if this is a slice assignment: [arr FROM start FOR length] := value
   470→	if p.peekTokenIs(lexer.FROM) {
   471→		return p.parseSliceAssignment(lbracketToken, size)
   472→	}
   473→
   474→	// Expect ]
   475→	if !p.expectPeek(lexer.RBRACKET) {
   476→		return nil
   477→	}
   478→
   479→	// Check if this is a channel array: [n]CHAN OF TYPE
   480→	if p.peekTokenIs(lexer.CHAN) {
   481→		p.nextToken() // move to CHAN
   482→		chanDecl := &ast.ChanDecl{
   483→			Token:   p.curToken,
   484→			IsArray: true,
   485→			Size:    size,
   486→		}
   487→
   488→		// Expect OF (optional — CHAN BYTE is shorthand for CHAN OF BYTE)
   489→		if p.peekTokenIs(lexer.OF) {
   490→			p.nextToken() // consume OF
   491→		}
   492→
   493→		// Expect type (INT, BYTE, BOOL, etc.) or protocol name (IDENT)
   494→		p.nextToken()
   495→		if p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
   496→			p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
   497→			p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
   498→			chanDecl.ElemType = p.curToken.Literal
   499→		} else if p.curTokenIs(lexer.IDENT) {
   500→			chanDecl.ElemType = p.curToken.Literal
   501→		} else {
   502→			p.addError(fmt.Sprintf("expected type after CHAN, got %s", p.curToken.Type))
   503→			return nil
   504→		}
   505→
   506→		// Parse channel names
   507→		for {
   508→			if !p.expectPeek(lexer.IDENT) {
   509→				return nil
   510→			}
   511→			chanDecl.Names = append(chanDecl.Names, p.curToken.Literal)
   512→
   513→			if p.peekTokenIs(lexer.COMMA) {
   514→				p.nextToken() // consume comma
   515→			} else {
   516→				break
   517→			}
   518→		}
   519→
   520→		if !p.expectPeek(lexer.COLON) {
   521→			return nil
   522→		}
   523→
   524→		return chanDecl
   525→	}
   526→
   527→	// Regular array declaration
   528→	decl := &ast.ArrayDecl{Token: lbracketToken, Size: size}
   529→
   530→	// Expect type (INT, BYTE, BOOL, REAL, REAL32, REAL64)
   531→	p.nextToken()
   532→	if !p.curTokenIs(lexer.INT_TYPE) && !p.curTokenIs(lexer.BYTE_TYPE) &&
   533→		!p.curTokenIs(lexer.BOOL_TYPE) && !p.curTokenIs(lexer.REAL_TYPE) &&
   534→		!p.curTokenIs(lexer.REAL32_TYPE) && !p.curTokenIs(lexer.REAL64_TYPE) {
   535→		p.addError(fmt.Sprintf("expected type after array size, got %s", p.curToken.Type))
   536→		return nil
   537→	}
   538→	decl.Type = p.curToken.Literal
   539→
   540→	// Parse variable names
   541→	for {
   542→		if !p.expectPeek(lexer.IDENT) {
   543→			return nil
   544→		}
   545→		decl.Names = append(decl.Names, p.curToken.Literal)
   546→
   547→		if p.peekTokenIs(lexer.COMMA) {
   548→			p.nextToken() // consume comma
   549→		} else {
   550→			break
   551→		}
   552→	}
   553→
   554→	if !p.expectPeek(lexer.COLON) {
   555→		return nil
   556→	}
   557→
   558→	return decl
   559→}
   560→
   561→// parseSliceAssignment parses [arr FROM start FOR length] := value
   562→// Called from parseArrayDecl when FROM is detected after the array expression.
   563→// lbracketToken is the [ token, arrayExpr is the already-parsed array expression.
   564→func (p *Parser) parseSliceAssignment(lbracketToken lexer.Token, arrayExpr ast.Expression) ast.Statement {
   565→	p.nextToken() // consume FROM
   566→	p.nextToken() // move to start expression
   567→	startExpr := p.parseExpression(LOWEST)
   568→
   569→	if !p.expectPeek(lexer.FOR) {
   570→		return nil
   571→	}
   572→	p.nextToken() // move to length expression
   573→	lengthExpr := p.parseExpression(LOWEST)
   574→
   575→	if !p.expectPeek(lexer.RBRACKET) {
   576→		return nil
   577→	}
   578→
   579→	if !p.expectPeek(lexer.ASSIGN) {
   580→		return nil
   581→	}
   582→
   583→	assignToken := p.curToken
   584→	p.nextToken() // move past :=
   585→
   586→	value := p.parseExpression(LOWEST)
   587→
   588→	return &ast.Assignment{
   589→		Token: assignToken,
   590→		SliceTarget: &ast.SliceExpr{
   591→			Token:  lbracketToken,
   592→			Array:  arrayExpr,
   593→			Start:  startExpr,
   594→			Length: lengthExpr,
   595→		},
   596→		Value: value,
   597→	}
   598→}
   599→
   600→func (p *Parser) parseIndexedOperation() ast.Statement {
   601→	name := p.curToken.Literal
   602→
   603→	p.nextToken() // move to [
   604→	p.nextToken() // move past [
   605→	index := p.parseExpression(LOWEST)
   606→
   607→	// Expect ]
   608→	if !p.expectPeek(lexer.RBRACKET) {
   609→		return nil
   610→	}
   611→
   612→	// Check what follows ]
   613→	if p.peekTokenIs(lexer.COMMA) {
   614→		// Multi-assignment starting with indexed target: name[index], ... := ...
   615→		firstTarget := ast.MultiAssignTarget{Name: name, Index: index}
   616→		return p.parseMultiAssignmentFrom(firstTarget)
   617→	}
   618→	if p.peekTokenIs(lexer.ASSIGN) {
   619→		// Indexed assignment: name[index] := value
   620→		p.nextToken() // move to :=
   621→		stmt := &ast.Assignment{
   622→			Name:  name,
   623→			Token: p.curToken,
   624→			Index: index,
   625→		}
   626→		p.nextToken() // move past :=
   627→		stmt.Value = p.parseExpression(LOWEST)
   628→		return stmt
   629→	}
   630→
   631→	if p.peekTokenIs(lexer.SEND) {
   632→		// Indexed channel send: cs[i] ! value
   633→		p.nextToken() // move to !
   634→		sendToken := p.curToken
   635→		p.nextToken() // move past !
   636→
   637→		stmt := &ast.Send{
   638→			Token:        sendToken,
   639→			Channel:      name,
   640→			ChannelIndex: index,
   641→		}
   642→
   643→		// Check if this is a variant send: first token is an identifier that is a variant tag
   644→		if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.SEMICOLON) {
   645→			possibleTag := p.curToken.Literal
   646→			if p.isVariantTag(possibleTag) {
   647→				stmt.VariantTag = possibleTag
   648→				p.nextToken() // move to ;
   649→				for p.curTokenIs(lexer.SEMICOLON) {
   650→					p.nextToken() // move past ;
   651→					val := p.parseExpression(LOWEST)
   652→					stmt.Values = append(stmt.Values, val)
   653→				}
   654→				return stmt
   655→			}
   656→		}
   657→
   658→		stmt.Value = p.parseExpression(LOWEST)
   659→
   660→		// Check for sequential send
   661→		for p.peekTokenIs(lexer.SEMICOLON) {
   662→			p.nextToken() // move to ;
   663→			p.nextToken() // move past ;
   664→			val := p.parseExpression(LOWEST)
   665→			stmt.Values = append(stmt.Values, val)
   666→		}
   667→
   668→		return stmt
   669→	}
   670→
   671→	if p.peekTokenIs(lexer.RECEIVE) {
   672→		// Indexed channel receive: cs[i] ? x or cs[i] ? CASE ...
   673→		p.nextToken() // move to ?
   674→		recvToken := p.curToken
   675→
   676→		// Check for variant receive: cs[i] ? CASE
   677→		if p.peekTokenIs(lexer.CASE) {
   678→			p.nextToken() // move to CASE
   679→			return p.parseVariantReceiveWithIndex(name, index, recvToken)
   680→		}
   681→
   682→		stmt := &ast.Receive{
   683→			Token:        recvToken,
   684→			Channel:      name,
   685→			ChannelIndex: index,
   686→		}
   687→
   688→		if !p.expectPeek(lexer.IDENT) {
   689→			return nil
   690→		}
   691→		stmt.Variable = p.curToken.Literal
   692→
   693→		// Check for sequential receive
   694→		for p.peekTokenIs(lexer.SEMICOLON) {
   695→			p.nextToken() // move to ;
   696→			if !p.expectPeek(lexer.IDENT) {
   697→				return nil
   698→			}
   699→			stmt.Variables = append(stmt.Variables, p.curToken.Literal)
   700→		}
   701→
   702→		return stmt
   703→	}
   704→
   705→	// Default: treat as indexed assignment (shouldn't reach here normally)
   706→	p.addError(fmt.Sprintf("expected :=, !, or ? after %s[...], got %s", name, p.peekToken.Type))
   707→	return nil
   708→}
   709→
   710→func (p *Parser) parseIndexExpression(left ast.Expression) *ast.IndexExpr {
   711→	expr := &ast.IndexExpr{
   712→		Token: p.curToken,
   713→		Left:  left,
   714→	}
   715→
   716→	p.nextToken() // move past [
   717→	expr.Index = p.parseExpression(LOWEST)
   718→
   719→	if !p.expectPeek(lexer.RBRACKET) {
   720→		return nil
   721→	}
   722→
   723→	return expr
   724→}
   725→
   726→func (p *Parser) parseChanDecl() *ast.ChanDecl {
   727→	decl := &ast.ChanDecl{Token: p.curToken}
   728→
   729→	// Expect OF (optional — CHAN BYTE is shorthand for CHAN OF BYTE)
   730→	if p.peekTokenIs(lexer.OF) {
   731→		p.nextToken() // consume OF
   732→	}
   733→
   734→	// Expect type (INT, BYTE, BOOL, etc.) or protocol name (IDENT)
   735→	p.nextToken()
   736→	if p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
   737→		p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
   738→		p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
   739→		decl.ElemType = p.curToken.Literal
   740→	} else if p.curTokenIs(lexer.IDENT) {
   741→		decl.ElemType = p.curToken.Literal
   742→	} else {
   743→		p.addError(fmt.Sprintf("expected type after CHAN, got %s", p.curToken.Type))
   744→		return nil
   745→	}
   746→
   747→	// Parse channel names
   748→	for {
   749→		if !p.expectPeek(lexer.IDENT) {
   750→			return nil
   751→		}
   752→		decl.Names = append(decl.Names, p.curToken.Literal)
   753→
   754→		if p.peekTokenIs(lexer.COMMA) {
   755→			p.nextToken() // consume comma
   756→		} else {
   757→			break
   758→		}
   759→	}
   760→
   761→	if !p.expectPeek(lexer.COLON) {
   762→		return nil
   763→	}
   764→
   765→	return decl
   766→}
   767→
   768→func (p *Parser) parseProtocolDecl() *ast.ProtocolDecl {
   769→	decl := &ast.ProtocolDecl{Token: p.curToken}
   770→
   771→	// Expect protocol name
   772→	if !p.expectPeek(lexer.IDENT) {
   773→		return nil
   774→	}
   775→	decl.Name = p.curToken.Literal
   776→
   777→	// Check if this is IS form (simple/sequential) or CASE form (variant)
   778→	if p.peekTokenIs(lexer.NEWLINE) || p.peekTokenIs(lexer.INDENT) {
   779→		// Could be variant: PROTOCOL NAME \n INDENT CASE ...
   780→		// Skip newlines
   781→		for p.peekTokenIs(lexer.NEWLINE) {
   782→			p.nextToken()
   783→		}
   784→
   785→		if p.peekTokenIs(lexer.INDENT) {
   786→			p.nextToken() // consume INDENT
   787→			p.nextToken() // move into block
   788→
   789→			if p.curTokenIs(lexer.CASE) {
   790→				// Variant protocol
   791→				decl.Kind = "variant"
   792→				decl.Variants = p.parseProtocolVariants()
   793→				p.protocolNames[decl.Name] = true
   794→				p.protocolDefs[decl.Name] = decl
   795→				return decl
   796→			}
   797→		}
   798→
   799→		p.addError("expected IS or CASE in protocol declaration")
   800→		return nil
   801→	}
   802→
   803→	// IS form: PROTOCOL NAME IS TYPE [; TYPE]*
   804→	if !p.expectPeek(lexer.IS) {
   805→		return nil
   806→	}
   807→
   808→	// Parse type list
   809→	p.nextToken()
   810→	typeName := p.parseProtocolTypeName()
   811→	if typeName == "" {
   812→		return nil
   813→	}
   814→	decl.Types = append(decl.Types, typeName)
   815→
   816→	// Check for sequential: ; TYPE
   817→	for p.peekTokenIs(lexer.SEMICOLON) {
   818→		p.nextToken() // move to ;
   819→		p.nextToken() // move past ;
   820→		typeName = p.parseProtocolTypeName()
   821→		if typeName == "" {
   822→			return nil
   823→		}
   824→		decl.Types = append(decl.Types, typeName)
   825→	}
   826→
   827→	if len(decl.Types) == 1 {
   828→		decl.Kind = "simple"
   829→	} else {
   830→		decl.Kind = "sequential"
   831→	}
   832→
   833→	p.protocolNames[decl.Name] = true
   834→	p.protocolDefs[decl.Name] = decl
   835→	return decl
   836→}
   837→
   838→func (p *Parser) parseProtocolTypeName() string {
   839→	switch p.curToken.Type {
   840→	case lexer.INT_TYPE:
   841→		return "INT"
   842→	case lexer.BYTE_TYPE:
   843→		return "BYTE"
   844→	case lexer.BOOL_TYPE:
   845→		return "BOOL"
   846→	case lexer.REAL_TYPE:
   847→		return "REAL"
   848→	case lexer.REAL32_TYPE:
   849→		return "REAL32"
   850→	case lexer.REAL64_TYPE:
   851→		return "REAL64"
   852→	case lexer.IDENT:
   853→		return p.curToken.Literal
   854→	default:
   855→		p.addError(fmt.Sprintf("expected type name in protocol, got %s", p.curToken.Type))
   856→		return ""
   857→	}
   858→}
   859→
   860→func (p *Parser) parseProtocolVariants() []ast.ProtocolVariant {
   861→	var variants []ast.ProtocolVariant
   862→
   863→	// Skip to next line after CASE
   864→	for p.peekTokenIs(lexer.NEWLINE) {
   865→		p.nextToken()
   866→	}
   867→
   868→	// Expect INDENT
   869→	if !p.peekTokenIs(lexer.INDENT) {
   870→		p.addError("expected indented block after CASE in protocol")
   871→		return variants
   872→	}
   873→	p.nextToken() // consume INDENT
   874→	startLevel := p.indentLevel
   875→	p.nextToken() // move into block
   876→
   877→	for !p.curTokenIs(lexer.EOF) {
   878→		// Skip newlines
   879→		for p.curTokenIs(lexer.NEWLINE) {
   880→			p.nextToken()
   881→		}
   882→
   883→		// Handle DEDENT tokens
   884→		for p.curTokenIs(lexer.DEDENT) {
   885→			if p.indentLevel < startLevel {
   886→				return variants
   887→			}
   888→			p.nextToken()
   889→		}
   890→
   891→		// Skip any more newlines after DEDENT
   892→		for p.curTokenIs(lexer.NEWLINE) {
   893→			p.nextToken()
   894→		}
   895→
   896→		if p.curTokenIs(lexer.EOF) {
   897→			break
   898→		}
   899→
   900→		if p.indentLevel < startLevel {
   901→			break
   902→		}
   903→
   904→		// Parse variant: tag [; TYPE]*
   905→		if !p.curTokenIs(lexer.IDENT) {
   906→			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
   907→			return variants
   908→		}
   909→
   910→		v := ast.ProtocolVariant{
   911→			Tag: p.curToken.Literal,
   912→		}
   913→
   914→		// Parse optional types after semicolons
   915→		for p.peekTokenIs(lexer.SEMICOLON) {
   916→			p.nextToken() // move to ;
   917→			p.nextToken() // move past ;
   918→			typeName := p.parseProtocolTypeName()
   919→			if typeName == "" {
   920→				return variants
   921→			}
   922→			v.Types = append(v.Types, typeName)
   923→		}
   924→
   925→		variants = append(variants, v)
   926→
   927→		// Advance past newline if needed
   928→		if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
   929→			p.nextToken()
   930→		}
   931→	}
   932→
   933→	return variants
   934→}
   935→
   936→func (p *Parser) parseRecordDecl() *ast.RecordDecl {
   937→	decl := &ast.RecordDecl{Token: p.curToken}
   938→
   939→	// Expect record name
   940→	if !p.expectPeek(lexer.IDENT) {
   941→		return nil
   942→	}
   943→	decl.Name = p.curToken.Literal
   944→
   945→	// Skip newlines
   946→	for p.peekTokenIs(lexer.NEWLINE) {
   947→		p.nextToken()
   948→	}
   949→
   950→	// Expect INDENT for field block
   951→	if !p.peekTokenIs(lexer.INDENT) {
   952→		p.addError("expected indented block after RECORD declaration")
   953→		return nil
   954→	}
   955→	p.nextToken() // consume INDENT
   956→	startLevel := p.indentLevel
   957→	p.nextToken() // move into block
   958→
   959→	// Parse field declarations: TYPE name[, name]*:
   960→	for !p.curTokenIs(lexer.EOF) {
   961→		// Skip newlines
   962→		for p.curTokenIs(lexer.NEWLINE) {
   963→			p.nextToken()
   964→		}
   965→
   966→		// Handle DEDENT tokens
   967→		for p.curTokenIs(lexer.DEDENT) {
   968→			if p.indentLevel < startLevel {
   969→				p.recordNames[decl.Name] = true
   970→				p.recordDefs[decl.Name] = decl
   971→				return decl
   972→			}
   973→			p.nextToken()
   974→		}
   975→
   976→		// Skip any more newlines after DEDENT
   977→		for p.curTokenIs(lexer.NEWLINE) {
   978→			p.nextToken()
   979→		}
   980→
   981→		if p.curTokenIs(lexer.EOF) {
   982→			break
   983→		}
   984→
   985→		if p.indentLevel < startLevel {
   986→			break
   987→		}
   988→
   989→		// Expect a type keyword (INT, BYTE, BOOL, REAL, REAL32, REAL64)
   990→		if !p.curTokenIs(lexer.INT_TYPE) && !p.curTokenIs(lexer.BYTE_TYPE) &&
   991→			!p.curTokenIs(lexer.BOOL_TYPE) && !p.curTokenIs(lexer.REAL_TYPE) &&
   992→			!p.curTokenIs(lexer.REAL32_TYPE) && !p.curTokenIs(lexer.REAL64_TYPE) {
   993→			p.addError(fmt.Sprintf("expected type in record field, got %s", p.curToken.Type))
   994→			return nil
   995→		}
   996→		fieldType := p.curToken.Literal
   997→
   998→		// Parse field names (comma-separated)
   999→		for {
  1000→			if !p.expectPeek(lexer.IDENT) {
  1001→				return nil
  1002→			}
  1003→			decl.Fields = append(decl.Fields, ast.RecordField{
  1004→				Type: fieldType,
  1005→				Name: p.curToken.Literal,
  1006→			})
  1007→
  1008→			if p.peekTokenIs(lexer.COMMA) {
  1009→				p.nextToken() // consume comma
  1010→			} else {
  1011→				break
  1012→			}
  1013→		}
  1014→
  1015→		// Expect colon
  1016→		if !p.expectPeek(lexer.COLON) {
  1017→			return nil
  1018→		}
  1019→
  1020→		// Advance past newline if needed
  1021→		if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1022→			p.nextToken()
  1023→		}
  1024→	}
  1025→
  1026→	p.recordNames[decl.Name] = true
  1027→	p.recordDefs[decl.Name] = decl
  1028→	return decl
  1029→}
  1030→
  1031→func (p *Parser) parseRecordVarDecl() *ast.VarDecl {
  1032→	decl := &ast.VarDecl{
  1033→		Token: p.curToken,
  1034→		Type:  p.curToken.Literal,
  1035→	}
  1036→
  1037→	// Parse variable names
  1038→	for {
  1039→		if !p.expectPeek(lexer.IDENT) {
  1040→			return nil
  1041→		}
  1042→		decl.Names = append(decl.Names, p.curToken.Literal)
  1043→
  1044→		if p.peekTokenIs(lexer.COMMA) {
  1045→			p.nextToken() // consume comma
  1046→		} else {
  1047→			break
  1048→		}
  1049→	}
  1050→
  1051→	if !p.expectPeek(lexer.COLON) {
  1052→		return nil
  1053→	}
  1054→
  1055→	return decl
  1056→}
  1057→
  1058→func (p *Parser) parseTimerDecl() *ast.TimerDecl {
  1059→	decl := &ast.TimerDecl{Token: p.curToken}
  1060→
  1061→	// Parse timer names
  1062→	for {
  1063→		if !p.expectPeek(lexer.IDENT) {
  1064→			return nil
  1065→		}
  1066→		decl.Names = append(decl.Names, p.curToken.Literal)
  1067→		p.timerNames[p.curToken.Literal] = true
  1068→
  1069→		if p.peekTokenIs(lexer.COMMA) {
  1070→			p.nextToken() // consume comma
  1071→		} else {
  1072→			break
  1073→		}
  1074→	}
  1075→
  1076→	if !p.expectPeek(lexer.COLON) {
  1077→		return nil
  1078→	}
  1079→
  1080→	return decl
  1081→}
  1082→
  1083→func (p *Parser) parseTimerRead() *ast.TimerRead {
  1084→	stmt := &ast.TimerRead{
  1085→		Timer: p.curToken.Literal,
  1086→	}
  1087→
  1088→	p.nextToken() // move to ?
  1089→	stmt.Token = p.curToken
  1090→
  1091→	if !p.expectPeek(lexer.IDENT) {
  1092→		return nil
  1093→	}
  1094→	stmt.Variable = p.curToken.Literal
  1095→
  1096→	return stmt
  1097→}
  1098→
  1099→func (p *Parser) parseSend() *ast.Send {
  1100→	stmt := &ast.Send{
  1101→		Channel: p.curToken.Literal,
  1102→	}
  1103→
  1104→	p.nextToken() // move to !
  1105→	stmt.Token = p.curToken
  1106→
  1107→	p.nextToken() // move past !
  1108→
  1109→	// Check if this is a variant send: first token is an identifier that is a variant tag
  1110→	// We detect this by checking if the identifier is followed by SEMICOLON
  1111→	// and the identifier is NOT followed by an operator (i.e., it's a bare tag name)
  1112→	if p.curTokenIs(lexer.IDENT) && p.peekTokenIs(lexer.SEMICOLON) {
  1113→		// Could be variant send (tag ; values) or expression ; values
  1114→		// Check if the identifier is a known protocol variant tag
  1115→		// For simplicity, if IDENT is followed by SEMICOLON and the ident is
  1116→		// not followed by an operator, treat it as a variant tag
  1117→		// We save the ident and check further
  1118→		possibleTag := p.curToken.Literal
  1119→		// Check if this identifier is a protocol variant tag
  1120→		if p.isVariantTag(possibleTag) {
  1121→			stmt.VariantTag = possibleTag
  1122→			p.nextToken() // move to ;
  1123→			// Parse remaining values after the tag
  1124→			for p.curTokenIs(lexer.SEMICOLON) {
  1125→				p.nextToken() // move past ;
  1126→				val := p.parseExpression(LOWEST)
  1127→				stmt.Values = append(stmt.Values, val)
  1128→			}
  1129→			return stmt
  1130→		}
  1131→	}
  1132→
  1133→	stmt.Value = p.parseExpression(LOWEST)
  1134→
  1135→	// Check for sequential send: c ! expr ; expr ; ...
  1136→	for p.peekTokenIs(lexer.SEMICOLON) {
  1137→		p.nextToken() // move to ;
  1138→		p.nextToken() // move past ;
  1139→		val := p.parseExpression(LOWEST)
  1140→		stmt.Values = append(stmt.Values, val)
  1141→	}
  1142→
  1143→	return stmt
  1144→}
  1145→
  1146→func (p *Parser) isVariantTag(name string) bool {
  1147→	for _, proto := range p.protocolDefs {
  1148→		if proto.Kind == "variant" {
  1149→			for _, v := range proto.Variants {
  1150→				if v.Tag == name {
  1151→					return true
  1152→				}
  1153→			}
  1154→		}
  1155→	}
  1156→	return false
  1157→}
  1158→
  1159→func (p *Parser) parseReceive() ast.Statement {
  1160→	channel := p.curToken.Literal
  1161→
  1162→	p.nextToken() // move to ?
  1163→	recvToken := p.curToken
  1164→
  1165→	// Check for variant receive: c ? CASE
  1166→	if p.peekTokenIs(lexer.CASE) {
  1167→		p.nextToken() // move to CASE
  1168→		return p.parseVariantReceive(channel, recvToken)
  1169→	}
  1170→
  1171→	stmt := &ast.Receive{
  1172→		Channel: channel,
  1173→		Token:   recvToken,
  1174→	}
  1175→
  1176→	if !p.expectPeek(lexer.IDENT) {
  1177→		return nil
  1178→	}
  1179→	stmt.Variable = p.curToken.Literal
  1180→
  1181→	// Check for sequential receive: c ? x ; y ; z
  1182→	for p.peekTokenIs(lexer.SEMICOLON) {
  1183→		p.nextToken() // move to ;
  1184→		if !p.expectPeek(lexer.IDENT) {
  1185→			return nil
  1186→		}
  1187→		stmt.Variables = append(stmt.Variables, p.curToken.Literal)
  1188→	}
  1189→
  1190→	return stmt
  1191→}
  1192→
  1193→func (p *Parser) parseVariantReceive(channel string, token lexer.Token) *ast.VariantReceive {
  1194→	stmt := &ast.VariantReceive{
  1195→		Token:   token,
  1196→		Channel: channel,
  1197→	}
  1198→
  1199→	// Skip to next line
  1200→	for p.peekTokenIs(lexer.NEWLINE) {
  1201→		p.nextToken()
  1202→	}
  1203→
  1204→	// Expect INDENT
  1205→	if !p.peekTokenIs(lexer.INDENT) {
  1206→		p.addError("expected indented block after ? CASE")
  1207→		return stmt
  1208→	}
  1209→	p.nextToken() // consume INDENT
  1210→	startLevel := p.indentLevel
  1211→	p.nextToken() // move into block
  1212→
  1213→	// Parse variant cases (similar to parseCaseStatement pattern)
  1214→	for !p.curTokenIs(lexer.EOF) {
  1215→		// Skip newlines
  1216→		for p.curTokenIs(lexer.NEWLINE) {
  1217→			p.nextToken()
  1218→		}
  1219→
  1220→		// Handle DEDENT tokens
  1221→		for p.curTokenIs(lexer.DEDENT) {
  1222→			if p.indentLevel < startLevel {
  1223→				return stmt
  1224→			}
  1225→			p.nextToken()
  1226→		}
  1227→
  1228→		// Skip any more newlines after DEDENT
  1229→		for p.curTokenIs(lexer.NEWLINE) {
  1230→			p.nextToken()
  1231→		}
  1232→
  1233→		if p.curTokenIs(lexer.EOF) {
  1234→			break
  1235→		}
  1236→
  1237→		if p.indentLevel < startLevel {
  1238→			break
  1239→		}
  1240→
  1241→		// Parse a variant case: tag [; var]* \n INDENT body
  1242→		vc := ast.VariantCase{}
  1243→
  1244→		if !p.curTokenIs(lexer.IDENT) {
  1245→			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
  1246→			return stmt
  1247→		}
  1248→		vc.Tag = p.curToken.Literal
  1249→
  1250→		// Parse optional variables after semicolons: tag ; x ; y
  1251→		for p.peekTokenIs(lexer.SEMICOLON) {
  1252→			p.nextToken() // move to ;
  1253→			if !p.expectPeek(lexer.IDENT) {
  1254→				return stmt
  1255→			}
  1256→			vc.Variables = append(vc.Variables, p.curToken.Literal)
  1257→		}
  1258→
  1259→		// Skip newlines and expect INDENT for body
  1260→		for p.peekTokenIs(lexer.NEWLINE) {
  1261→			p.nextToken()
  1262→		}
  1263→
  1264→		if p.peekTokenIs(lexer.INDENT) {
  1265→			p.nextToken() // consume INDENT
  1266→			p.nextToken() // move to body
  1267→			vc.Body = p.parseStatement()
  1268→
  1269→			// Advance past the last token of the statement if needed
  1270→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1271→				p.nextToken()
  1272→			}
  1273→		}
  1274→
  1275→		stmt.Cases = append(stmt.Cases, vc)
  1276→	}
  1277→
  1278→	return stmt
  1279→}
  1280→
  1281→func (p *Parser) parseVariantReceiveWithIndex(channel string, channelIndex ast.Expression, token lexer.Token) *ast.VariantReceive {
  1282→	stmt := &ast.VariantReceive{
  1283→		Token:        token,
  1284→		Channel:      channel,
  1285→		ChannelIndex: channelIndex,
  1286→	}
  1287→
  1288→	// Skip to next line
  1289→	for p.peekTokenIs(lexer.NEWLINE) {
  1290→		p.nextToken()
  1291→	}
  1292→
  1293→	// Expect INDENT
  1294→	if !p.peekTokenIs(lexer.INDENT) {
  1295→		p.addError("expected indented block after ? CASE")
  1296→		return stmt
  1297→	}
  1298→	p.nextToken() // consume INDENT
  1299→	startLevel := p.indentLevel
  1300→	p.nextToken() // move into block
  1301→
  1302→	for !p.curTokenIs(lexer.EOF) {
  1303→		for p.curTokenIs(lexer.NEWLINE) {
  1304→			p.nextToken()
  1305→		}
  1306→
  1307→		for p.curTokenIs(lexer.DEDENT) {
  1308→			if p.indentLevel < startLevel {
  1309→				return stmt
  1310→			}
  1311→			p.nextToken()
  1312→		}
  1313→
  1314→		for p.curTokenIs(lexer.NEWLINE) {
  1315→			p.nextToken()
  1316→		}
  1317→
  1318→		if p.curTokenIs(lexer.EOF) {
  1319→			break
  1320→		}
  1321→
  1322→		if p.indentLevel < startLevel {
  1323→			break
  1324→		}
  1325→
  1326→		vc := ast.VariantCase{}
  1327→
  1328→		if !p.curTokenIs(lexer.IDENT) {
  1329→			p.addError(fmt.Sprintf("expected variant tag name, got %s", p.curToken.Type))
  1330→			return stmt
  1331→		}
  1332→		vc.Tag = p.curToken.Literal
  1333→
  1334→		for p.peekTokenIs(lexer.SEMICOLON) {
  1335→			p.nextToken() // move to ;
  1336→			if !p.expectPeek(lexer.IDENT) {
  1337→				return stmt
  1338→			}
  1339→			vc.Variables = append(vc.Variables, p.curToken.Literal)
  1340→		}
  1341→
  1342→		for p.peekTokenIs(lexer.NEWLINE) {
  1343→			p.nextToken()
  1344→		}
  1345→
  1346→		if p.peekTokenIs(lexer.INDENT) {
  1347→			p.nextToken() // consume INDENT
  1348→			p.nextToken() // move to body
  1349→			vc.Body = p.parseStatement()
  1350→
  1351→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1352→				p.nextToken()
  1353→			}
  1354→		}
  1355→
  1356→		stmt.Cases = append(stmt.Cases, vc)
  1357→	}
  1358→
  1359→	return stmt
  1360→}
  1361→
  1362→func (p *Parser) parseSeqBlock() *ast.SeqBlock {
  1363→	block := &ast.SeqBlock{Token: p.curToken}
  1364→
  1365→	// Check for replicator: SEQ i = start FOR count
  1366→	if p.peekTokenIs(lexer.IDENT) {
  1367→		// Save position to check if it's a replicator
  1368→		p.nextToken() // move to identifier
  1369→		if p.peekTokenIs(lexer.EQ) {
  1370→			// This is a replicator
  1371→			block.Replicator = p.parseReplicator()
  1372→		} else {
  1373→			// Not a replicator, this shouldn't happen in valid Occam
  1374→			// (SEQ followed by identifier at same indentation level)
  1375→			p.addError("unexpected identifier after SEQ")
  1376→			return block
  1377→		}
  1378→	}
  1379→
  1380→	// Skip to next line
  1381→	for p.peekTokenIs(lexer.NEWLINE) {
  1382→		p.nextToken()
  1383→	}
  1384→
  1385→	// Expect INDENT
  1386→	if !p.peekTokenIs(lexer.INDENT) {
  1387→		p.addError("expected indented block after SEQ")
  1388→		return block
  1389→	}
  1390→	p.nextToken() // consume INDENT
  1391→
  1392→	block.Statements = p.parseBlockStatements()
  1393→
  1394→	return block
  1395→}
  1396→
  1397→func (p *Parser) parseParBlock() *ast.ParBlock {
  1398→	block := &ast.ParBlock{Token: p.curToken}
  1399→
  1400→	// Check for replicator: PAR i = start FOR count
  1401→	if p.peekTokenIs(lexer.IDENT) {
  1402→		// Save position to check if it's a replicator
  1403→		p.nextToken() // move to identifier
  1404→		if p.peekTokenIs(lexer.EQ) {
  1405→			// This is a replicator
  1406→			block.Replicator = p.parseReplicator()
  1407→		} else {
  1408→			// Not a replicator, this shouldn't happen in valid Occam
  1409→			// (PAR followed by identifier at same indentation level)
  1410→			p.addError("unexpected identifier after PAR")
  1411→			return block
  1412→		}
  1413→	}
  1414→
  1415→	// Skip to next line
  1416→	for p.peekTokenIs(lexer.NEWLINE) {
  1417→		p.nextToken()
  1418→	}
  1419→
  1420→	// Expect INDENT
  1421→	if !p.peekTokenIs(lexer.INDENT) {
  1422→		p.addError("expected indented block after PAR")
  1423→		return block
  1424→	}
  1425→	p.nextToken() // consume INDENT
  1426→
  1427→	block.Statements = p.parseBlockStatements()
  1428→
  1429→	return block
  1430→}
  1431→
  1432→// parseReplicator parses: variable = start FOR count [STEP step]
  1433→// Assumes the variable identifier has already been consumed and is in curToken
  1434→func (p *Parser) parseReplicator() *ast.Replicator {
  1435→	rep := &ast.Replicator{
  1436→		Variable: p.curToken.Literal,
  1437→	}
  1438→
  1439→	// Expect =
  1440→	if !p.expectPeek(lexer.EQ) {
  1441→		return nil
  1442→	}
  1443→
  1444→	// Parse start expression
  1445→	p.nextToken()
  1446→	rep.Start = p.parseExpression(LOWEST)
  1447→
  1448→	// Expect FOR
  1449→	if !p.expectPeek(lexer.FOR) {
  1450→		return nil
  1451→	}
  1452→
  1453→	// Parse count expression
  1454→	p.nextToken()
  1455→	rep.Count = p.parseExpression(LOWEST)
  1456→
  1457→	// Optional STEP
  1458→	if p.peekTokenIs(lexer.STEP) {
  1459→		p.nextToken() // consume STEP
  1460→		p.nextToken() // move to step expression
  1461→		rep.Step = p.parseExpression(LOWEST)
  1462→	}
  1463→
  1464→	return rep
  1465→}
  1466→
  1467→func (p *Parser) parseAltBlock() *ast.AltBlock {
  1468→	block := &ast.AltBlock{Token: p.curToken}
  1469→
  1470→	// Skip to next line
  1471→	for p.peekTokenIs(lexer.NEWLINE) {
  1472→		p.nextToken()
  1473→	}
  1474→
  1475→	// Expect INDENT
  1476→	if !p.peekTokenIs(lexer.INDENT) {
  1477→		p.addError("expected indented block after ALT")
  1478→		return block
  1479→	}
  1480→	p.nextToken() // consume INDENT
  1481→
  1482→	block.Cases = p.parseAltCases()
  1483→
  1484→	return block
  1485→}
  1486→
  1487→func (p *Parser) parseAltCases() []ast.AltCase {
  1488→	var cases []ast.AltCase
  1489→	startLevel := p.indentLevel
  1490→
  1491→	p.nextToken() // move past INDENT
  1492→
  1493→	for !p.curTokenIs(lexer.EOF) {
  1494→		// Skip newlines
  1495→		for p.curTokenIs(lexer.NEWLINE) {
  1496→			p.nextToken()
  1497→		}
  1498→
  1499→		// Handle DEDENT tokens
  1500→		for p.curTokenIs(lexer.DEDENT) {
  1501→			if p.indentLevel < startLevel {
  1502→				return cases
  1503→			}
  1504→			p.nextToken()
  1505→		}
  1506→
  1507→		// Skip any more newlines after DEDENT
  1508→		for p.curTokenIs(lexer.NEWLINE) {
  1509→			p.nextToken()
  1510→		}
  1511→
  1512→		if p.curTokenIs(lexer.EOF) {
  1513→			break
  1514→		}
  1515→
  1516→		if p.indentLevel < startLevel {
  1517→			break
  1518→		}
  1519→
  1520→		// Parse an ALT case: [guard &] channel ? var
  1521→		altCase := p.parseAltCase()
  1522→		if altCase != nil {
  1523→			cases = append(cases, *altCase)
  1524→		}
  1525→	}
  1526→
  1527→	return cases
  1528→}
  1529→
  1530→func (p *Parser) parseAltCase() *ast.AltCase {
  1531→	altCase := &ast.AltCase{}
  1532→
  1533→	// Check for guard: expression & channel ? var
  1534→	// For now, we expect: channel ? var (no guard support yet)
  1535→	// or: guard & channel ? var
  1536→
  1537→	// First token should be identifier (channel name or guard start)
  1538→	if !p.curTokenIs(lexer.IDENT) && !p.curTokenIs(lexer.TRUE) && !p.curTokenIs(lexer.FALSE) {
  1539→		p.addError(fmt.Sprintf("expected channel name or guard in ALT case, got %s", p.curToken.Type))
  1540→		return nil
  1541→	}
  1542→
  1543→	// Look ahead to determine if this is a guard or channel
  1544→	// If next token is & then we have a guard
  1545→	// If next token is ? then it's a channel/timer receive
  1546→	if p.peekTokenIs(lexer.RECEIVE) {
  1547→		name := p.curToken.Literal
  1548→		if p.timerNames[name] {
  1549→			// Timer case: tim ? AFTER deadline
  1550→			altCase.IsTimer = true
  1551→			altCase.Timer = name
  1552→			p.nextToken() // move to ?
  1553→			if !p.expectPeek(lexer.AFTER) {
  1554→				return nil
  1555→			}
  1556→			p.nextToken() // move past AFTER
  1557→			altCase.Deadline = p.parseExpression(LOWEST)
  1558→		} else {
  1559→			// Simple case: channel ? var
  1560→			altCase.Channel = name
  1561→			p.nextToken() // move to ?
  1562→			if !p.expectPeek(lexer.IDENT) {
  1563→				return nil
  1564→			}
  1565→			altCase.Variable = p.curToken.Literal
  1566→		}
  1567→	} else if p.peekTokenIs(lexer.LBRACKET) {
  1568→		// Indexed channel case: cs[i] ? var
  1569→		name := p.curToken.Literal
  1570→		altCase.Channel = name
  1571→		p.nextToken() // move to [
  1572→		p.nextToken() // move past [
  1573→		altCase.ChannelIndex = p.parseExpression(LOWEST)
  1574→		if !p.expectPeek(lexer.RBRACKET) {
  1575→			return nil
  1576→		}
  1577→		if !p.expectPeek(lexer.RECEIVE) {
  1578→			return nil
  1579→		}
  1580→		if !p.expectPeek(lexer.IDENT) {
  1581→			return nil
  1582→		}
  1583→		altCase.Variable = p.curToken.Literal
  1584→	} else {
  1585→		// Could be a guard followed by & channel ? var
  1586→		// For simplicity, parse expression until we hit &
  1587→		// For now, only support simple TRUE/FALSE or identifier guards
  1588→		guard := p.parseExpression(LOWEST)
  1589→		altCase.Guard = guard
  1590→
  1591→		// Expect &
  1592→		if !p.peekTokenIs(lexer.AMPERSAND) {
  1593→			p.addError("expected & after guard in ALT case")
  1594→			return nil
  1595→		}
  1596→		p.nextToken() // move to &
  1597→		p.nextToken() // move past &
  1598→
  1599→		// Now expect channel ? var or channel[index] ? var
  1600→		if !p.curTokenIs(lexer.IDENT) {
  1601→			p.addError(fmt.Sprintf("expected channel name after guard, got %s", p.curToken.Type))
  1602→			return nil
  1603→		}
  1604→		altCase.Channel = p.curToken.Literal
  1605→
  1606→		if p.peekTokenIs(lexer.LBRACKET) {
  1607→			// Indexed channel with guard: guard & cs[i] ? var
  1608→			p.nextToken() // move to [
  1609→			p.nextToken() // move past [
  1610→			altCase.ChannelIndex = p.parseExpression(LOWEST)
  1611→			if !p.expectPeek(lexer.RBRACKET) {
  1612→				return nil
  1613→			}
  1614→		}
  1615→
  1616→		if !p.expectPeek(lexer.RECEIVE) {
  1617→			return nil
  1618→		}
  1619→		if !p.expectPeek(lexer.IDENT) {
  1620→			return nil
  1621→		}
  1622→		altCase.Variable = p.curToken.Literal
  1623→	}
  1624→
  1625→	// Skip to next line for the body
  1626→	for p.peekTokenIs(lexer.NEWLINE) {
  1627→		p.nextToken()
  1628→	}
  1629→
  1630→	// Expect INDENT for body
  1631→	if !p.peekTokenIs(lexer.INDENT) {
  1632→		p.addError("expected indented body after ALT case")
  1633→		return altCase
  1634→	}
  1635→	p.nextToken() // consume INDENT
  1636→	p.nextToken() // move into body
  1637→
  1638→	// Parse the body (first statement)
  1639→	altCase.Body = p.parseStatement()
  1640→
  1641→	// Skip to end of body block
  1642→	for !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1643→		p.nextToken()
  1644→	}
  1645→
  1646→	return altCase
  1647→}
  1648→
  1649→func (p *Parser) parseBlockStatements() []ast.Statement {
  1650→	var statements []ast.Statement
  1651→	startLevel := p.indentLevel
  1652→
  1653→	p.nextToken() // move past INDENT
  1654→
  1655→	for !p.curTokenIs(lexer.EOF) {
  1656→		// Skip newlines
  1657→		for p.curTokenIs(lexer.NEWLINE) {
  1658→			p.nextToken()
  1659→		}
  1660→
  1661→		// Handle DEDENT tokens
  1662→		// If we're at a DEDENT and indentLevel has dropped below startLevel,
  1663→		// this DEDENT ends our block - stop parsing
  1664→		// If indentLevel >= startLevel, this DEDENT is from a nested block - skip it
  1665→		for p.curTokenIs(lexer.DEDENT) {
  1666→			if p.indentLevel < startLevel {
  1667→				return statements
  1668→			}
  1669→			p.nextToken() // skip nested block's DEDENT
  1670→		}
  1671→
  1672→		// Skip any more newlines after DEDENT
  1673→		for p.curTokenIs(lexer.NEWLINE) {
  1674→			p.nextToken()
  1675→		}
  1676→
  1677→		if p.curTokenIs(lexer.EOF) {
  1678→			break
  1679→		}
  1680→
  1681→		// Double-check we haven't gone below our level
  1682→		if p.indentLevel < startLevel {
  1683→			break
  1684→		}
  1685→
  1686→		stmt := p.parseStatement()
  1687→		if stmt != nil {
  1688→			statements = append(statements, stmt)
  1689→		}
  1690→
  1691→		// After parsing a statement, we need to advance.
  1692→		// But if we're already at NEWLINE/DEDENT/EOF, the next iteration will handle it.
  1693→		// Only advance if we're still on the last token of the statement.
  1694→		if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1695→			p.nextToken()
  1696→		}
  1697→	}
  1698→
  1699→	return statements
  1700→}
  1701→
  1702→func (p *Parser) parseProcDecl() *ast.ProcDecl {
  1703→	proc := &ast.ProcDecl{Token: p.curToken}
  1704→
  1705→	if !p.expectPeek(lexer.IDENT) {
  1706→		return nil
  1707→	}
  1708→	proc.Name = p.curToken.Literal
  1709→
  1710→	if !p.expectPeek(lexer.LPAREN) {
  1711→		return nil
  1712→	}
  1713→
  1714→	proc.Params = p.parseProcParams()
  1715→
  1716→	if !p.expectPeek(lexer.RPAREN) {
  1717→		return nil
  1718→	}
  1719→
  1720→	// Skip to next line and expect indented body
  1721→	for p.peekTokenIs(lexer.NEWLINE) {
  1722→		p.nextToken()
  1723→	}
  1724→
  1725→	if !p.peekTokenIs(lexer.INDENT) {
  1726→		p.addError("expected indented body after PROC declaration")
  1727→		return proc
  1728→	}
  1729→	p.nextToken() // consume INDENT
  1730→
  1731→	// Parse all statements in the body (local declarations + body process)
  1732→	bodyLevel := p.indentLevel
  1733→	p.nextToken()
  1734→
  1735→	for !p.curTokenIs(lexer.EOF) {
  1736→		// Skip newlines
  1737→		for p.curTokenIs(lexer.NEWLINE) {
  1738→			p.nextToken()
  1739→		}
  1740→
  1741→		// Handle DEDENTs
  1742→		for p.curTokenIs(lexer.DEDENT) {
  1743→			if p.indentLevel < bodyLevel {
  1744→				goto procBodyDone
  1745→			}
  1746→			p.nextToken()
  1747→		}
  1748→
  1749→		// Skip more newlines after DEDENT
  1750→		for p.curTokenIs(lexer.NEWLINE) {
  1751→			p.nextToken()
  1752→		}
  1753→
  1754→		if p.curTokenIs(lexer.EOF) || p.indentLevel < bodyLevel {
  1755→			break
  1756→		}
  1757→
  1758→		stmt := p.parseStatement()
  1759→		if stmt != nil {
  1760→			proc.Body = append(proc.Body, stmt)
  1761→		}
  1762→
  1763→		if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  1764→			p.nextToken()
  1765→		}
  1766→	}
  1767→procBodyDone:
  1768→
  1769→	// Optionally consume KRoC-style colon terminator
  1770→	if p.peekTokenIs(lexer.COLON) {
  1771→		p.nextToken()
  1772→	}
  1773→
  1774→	return proc
  1775→}
  1776→
  1777→func (p *Parser) parseProcParams() []ast.ProcParam {
  1778→	var params []ast.ProcParam
  1779→
  1780→	if p.peekTokenIs(lexer.RPAREN) {
  1781→		return params
  1782→	}
  1783→
  1784→	p.nextToken()
  1785→
  1786→	for {
  1787→		param := ast.ProcParam{}
  1788→
  1789→		// Check for VAL keyword
  1790→		if p.curTokenIs(lexer.VAL) {
  1791→			param.IsVal = true
  1792→			p.nextToken()
  1793→		}
  1794→
  1795→		// Check for []CHAN OF <type> or []TYPE (open array parameter)
  1796→		if p.curTokenIs(lexer.LBRACKET) && p.peekTokenIs(lexer.RBRACKET) {
  1797→			p.nextToken() // consume ]
  1798→			p.nextToken() // move past ]
  1799→			if p.curTokenIs(lexer.CHAN) {
  1800→				// []CHAN OF <type> or []CHAN <type> (channel array parameter)
  1801→				param.IsChan = true
  1802→				param.IsChanArray = true
  1803→				if p.peekTokenIs(lexer.OF) {
  1804→					p.nextToken() // consume OF
  1805→				}
  1806→				p.nextToken() // move to element type
  1807→				if p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
  1808→					p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
  1809→					p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
  1810→					param.ChanElemType = p.curToken.Literal
  1811→				} else if p.curTokenIs(lexer.IDENT) {
  1812→					param.ChanElemType = p.curToken.Literal
  1813→				} else {
  1814→					p.addError(fmt.Sprintf("expected type after []CHAN, got %s", p.curToken.Type))
  1815→					return params
  1816→				}
  1817→				p.nextToken()
  1818→			} else if p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
  1819→				p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
  1820→				p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
  1821→				// []TYPE (open array parameter)
  1822→				param.IsOpenArray = true
  1823→				param.Type = p.curToken.Literal
  1824→				p.nextToken()
  1825→			} else if p.curTokenIs(lexer.IDENT) && p.recordNames[p.curToken.Literal] {
  1826→				// []RECORD (open array of record type)
  1827→				param.IsOpenArray = true
  1828→				param.Type = p.curToken.Literal
  1829→				p.nextToken()
  1830→			} else {
  1831→				p.addError(fmt.Sprintf("expected type after [], got %s", p.curToken.Type))
  1832→				return params
  1833→			}
  1834→		} else if p.curTokenIs(lexer.CHAN) {
  1835→			// Check for CHAN OF <type> or CHAN <type>
  1836→			param.IsChan = true
  1837→			if p.peekTokenIs(lexer.OF) {
  1838→				p.nextToken() // consume OF
  1839→			}
  1840→			p.nextToken() // move to element type
  1841→			if p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
  1842→				p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
  1843→				p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
  1844→				param.ChanElemType = p.curToken.Literal
  1845→			} else if p.curTokenIs(lexer.IDENT) {
  1846→				param.ChanElemType = p.curToken.Literal
  1847→			} else {
  1848→				p.addError(fmt.Sprintf("expected type after CHAN, got %s", p.curToken.Type))
  1849→				return params
  1850→			}
  1851→			p.nextToken()
  1852→		} else if p.curTokenIs(lexer.IDENT) && p.recordNames[p.curToken.Literal] {
  1853→			// Record type parameter
  1854→			param.Type = p.curToken.Literal
  1855→			p.nextToken()
  1856→		} else {
  1857→			// Expect scalar type
  1858→			if !p.curTokenIs(lexer.INT_TYPE) && !p.curTokenIs(lexer.BYTE_TYPE) &&
  1859→				!p.curTokenIs(lexer.BOOL_TYPE) && !p.curTokenIs(lexer.REAL_TYPE) &&
  1860→				!p.curTokenIs(lexer.REAL32_TYPE) && !p.curTokenIs(lexer.REAL64_TYPE) {
  1861→				p.addError(fmt.Sprintf("expected type in parameter, got %s", p.curToken.Type))
  1862→				return params
  1863→			}
  1864→			param.Type = p.curToken.Literal
  1865→			p.nextToken()
  1866→		}
  1867→
  1868→		// Expect identifier
  1869→		if !p.curTokenIs(lexer.IDENT) {
  1870→			p.addError(fmt.Sprintf("expected parameter name, got %s", p.curToken.Type))
  1871→			return params
  1872→		}
  1873→		param.Name = p.curToken.Literal
  1874→
  1875→		// Check for channel direction marker (? or !)
  1876→		if (param.IsChan || param.IsChanArray) && (p.peekTokenIs(lexer.RECEIVE) || p.peekTokenIs(lexer.SEND)) {
  1877→			p.nextToken()
  1878→			param.ChanDir = p.curToken.Literal
  1879→		}
  1880→
  1881→		params = append(params, param)
  1882→
  1883→		if !p.peekTokenIs(lexer.COMMA) {
  1884→			break
  1885→		}
  1886→		p.nextToken() // consume comma
  1887→		p.nextToken() // move to next param
  1888→	}
  1889→
  1890→	return params
  1891→}
  1892→
  1893→func (p *Parser) parseProcCall() *ast.ProcCall {
  1894→	call := &ast.ProcCall{
  1895→		Token: p.curToken,
  1896→		Name:  p.curToken.Literal,
  1897→	}
  1898→
  1899→	if !p.peekTokenIs(lexer.LPAREN) {
  1900→		// No arguments
  1901→		return call
  1902→	}
  1903→
  1904→	p.nextToken() // consume (
  1905→
  1906→	if p.peekTokenIs(lexer.RPAREN) {
  1907→		p.nextToken() // consume )
  1908→		return call
  1909→	}
  1910→
  1911→	p.nextToken() // move to first arg
  1912→	call.Args = append(call.Args, p.parseExpression(LOWEST))
  1913→
  1914→	for p.peekTokenIs(lexer.COMMA) {
  1915→		p.nextToken() // consume comma
  1916→		p.nextToken() // move to next arg
  1917→		call.Args = append(call.Args, p.parseExpression(LOWEST))
  1918→	}
  1919→
  1920→	if !p.expectPeek(lexer.RPAREN) {
  1921→		return nil
  1922→	}
  1923→
  1924→	return call
  1925→}
  1926→
  1927→func (p *Parser) parseFuncDecl() *ast.FuncDecl {
  1928→	fn := &ast.FuncDecl{
  1929→		Token:       p.curToken,
  1930→		ReturnTypes: []string{p.curToken.Literal},
  1931→	}
  1932→
  1933→	// Parse additional return types for multi-result functions: INT, INT FUNCTION
  1934→	for p.peekTokenIs(lexer.COMMA) {
  1935→		p.nextToken() // consume comma
  1936→		p.nextToken() // move to next type
  1937→		fn.ReturnTypes = append(fn.ReturnTypes, p.curToken.Literal)
  1938→	}
  1939→
  1940→	// Consume FUNCTION keyword
  1941→	p.nextToken()
  1942→
  1943→	if !p.expectPeek(lexer.IDENT) {
  1944→		return nil
  1945→	}
  1946→	fn.Name = p.curToken.Literal
  1947→
  1948→	if !p.expectPeek(lexer.LPAREN) {
  1949→		return nil
  1950→	}
  1951→
  1952→	fn.Params = p.parseProcParams()
  1953→
  1954→	if !p.expectPeek(lexer.RPAREN) {
  1955→		return nil
  1956→	}
  1957→
  1958→	// Force all params to IsVal = true (occam FUNCTION params are always VAL)
  1959→	for i := range fn.Params {
  1960→		fn.Params[i].IsVal = true
  1961→	}
  1962→
  1963→	// Skip newlines, expect INDENT
  1964→	for p.peekTokenIs(lexer.NEWLINE) {
  1965→		p.nextToken()
  1966→	}
  1967→
  1968→	if !p.peekTokenIs(lexer.INDENT) {
  1969→		p.addError("expected indented body after FUNCTION declaration")
  1970→		return fn
  1971→	}
  1972→	funcLevel := p.indentLevel
  1973→	p.nextToken() // consume INDENT
  1974→	p.nextToken() // move into body
  1975→
  1976→	// IS form: simple expression return
  1977→	if p.curTokenIs(lexer.IS) {
  1978→		p.nextToken() // move past IS
  1979→		fn.ResultExprs = []ast.Expression{p.parseExpression(LOWEST)}
  1980→
  1981→		// Consume remaining tokens and DEDENTs back to function's indentation level
  1982→		for !p.curTokenIs(lexer.EOF) {
  1983→			if p.curTokenIs(lexer.DEDENT) && p.indentLevel <= funcLevel {
  1984→				break
  1985→			}
  1986→			p.nextToken()
  1987→		}
  1988→
  1989→		// Optionally consume KRoC-style colon terminator
  1990→		if p.peekTokenIs(lexer.COLON) {
  1991→			p.nextToken()
  1992→		}
  1993→		return fn
  1994→	}
  1995→
  1996→	// VALOF form: local declarations, then VALOF keyword, then body, then RESULT
  1997→	// Parse local declarations (type keywords before VALOF)
  1998→	for p.curTokenIs(lexer.INT_TYPE) || p.curTokenIs(lexer.BYTE_TYPE) ||
  1999→		p.curTokenIs(lexer.BOOL_TYPE) || p.curTokenIs(lexer.REAL_TYPE) ||
  2000→		p.curTokenIs(lexer.REAL32_TYPE) || p.curTokenIs(lexer.REAL64_TYPE) {
  2001→		stmt := p.parseVarDecl()
  2002→		if stmt != nil {
  2003→			fn.Body = append(fn.Body, stmt)
  2004→		}
  2005→		// Advance past NEWLINE
  2006→		for p.peekTokenIs(lexer.NEWLINE) {
  2007→			p.nextToken()
  2008→		}
  2009→		p.nextToken()
  2010→	}
  2011→
  2012→	// Expect VALOF keyword
  2013→	if !p.curTokenIs(lexer.VALOF) {
  2014→		p.addError(fmt.Sprintf("expected VALOF or IS in function body, got %s", p.curToken.Type))
  2015→		return fn
  2016→	}
  2017→
  2018→	// Skip newlines and expect INDENT for VALOF body
  2019→	for p.peekTokenIs(lexer.NEWLINE) {
  2020→		p.nextToken()
  2021→	}
  2022→
  2023→	if !p.peekTokenIs(lexer.INDENT) {
  2024→		p.addError("expected indented block after VALOF")
  2025→		return fn
  2026→	}
  2027→	p.nextToken() // consume INDENT
  2028→	p.nextToken() // move into VALOF body
  2029→
  2030→	// Parse the body statement (e.g., SEQ, IF, etc.)
  2031→	bodyStmt := p.parseStatement()
  2032→	if bodyStmt != nil {
  2033→		fn.Body = append(fn.Body, bodyStmt)
  2034→	}
  2035→
  2036→	// Advance past nested DEDENTs/newlines to RESULT
  2037→	for !p.curTokenIs(lexer.RESULT) && !p.curTokenIs(lexer.EOF) {
  2038→		p.nextToken()
  2039→	}
  2040→
  2041→	// Parse RESULT expression(s) — comma-separated for multi-result functions
  2042→	if p.curTokenIs(lexer.RESULT) {
  2043→		p.nextToken() // move past RESULT
  2044→		fn.ResultExprs = []ast.Expression{p.parseExpression(LOWEST)}
  2045→		for p.peekTokenIs(lexer.COMMA) {
  2046→			p.nextToken() // consume comma
  2047→			p.nextToken() // move to next expression
  2048→			fn.ResultExprs = append(fn.ResultExprs, p.parseExpression(LOWEST))
  2049→		}
  2050→	}
  2051→
  2052→	// Consume remaining tokens and DEDENTs back to function's indentation level
  2053→	for !p.curTokenIs(lexer.EOF) {
  2054→		if p.curTokenIs(lexer.DEDENT) && p.indentLevel <= funcLevel {
  2055→			break
  2056→		}
  2057→		p.nextToken()
  2058→	}
  2059→
  2060→	// Optionally consume KRoC-style colon terminator
  2061→	if p.peekTokenIs(lexer.COLON) {
  2062→		p.nextToken()
  2063→	}
  2064→
  2065→	return fn
  2066→}
  2067→
  2068→// parseByteLiteralValue processes the raw content of a byte literal (between single quotes),
  2069→// handling occam escape sequences (* prefix), and returns the resulting byte value.
  2070→func (p *Parser) parseByteLiteralValue(raw string) (byte, error) {
  2071→	if len(raw) == 0 {
  2072→		return 0, fmt.Errorf("empty byte literal")
  2073→	}
  2074→	if raw[0] == '*' {
  2075→		if len(raw) != 2 {
  2076→			return 0, fmt.Errorf("invalid escape sequence in byte literal: '*%s'", raw[1:])
  2077→		}
  2078→		switch raw[1] {
  2079→		case 'n':
  2080→			return '\n', nil
  2081→		case 'c':
  2082→			return '\r', nil
  2083→		case 't':
  2084→			return '\t', nil
  2085→		case 's':
  2086→			return ' ', nil
  2087→		case '*':
  2088→			return '*', nil
  2089→		case '\'':
  2090→			return '\'', nil
  2091→		case '"':
  2092→			return '"', nil
  2093→		default:
  2094→			return 0, fmt.Errorf("unknown escape sequence in byte literal: '*%c'", raw[1])
  2095→		}
  2096→	}
  2097→	if len(raw) != 1 {
  2098→		return 0, fmt.Errorf("byte literal must be a single character, got %q", raw)
  2099→	}
  2100→	return raw[0], nil
  2101→}
  2102→
  2103→func (p *Parser) parseFuncCallExpr() *ast.FuncCall {
  2104→	call := &ast.FuncCall{
  2105→		Token: p.curToken,
  2106→		Name:  p.curToken.Literal,
  2107→	}
  2108→
  2109→	p.nextToken() // consume (
  2110→
  2111→	if p.peekTokenIs(lexer.RPAREN) {
  2112→		p.nextToken() // consume )
  2113→		return call
  2114→	}
  2115→
  2116→	p.nextToken() // move to first arg
  2117→	call.Args = append(call.Args, p.parseExpression(LOWEST))
  2118→
  2119→	for p.peekTokenIs(lexer.COMMA) {
  2120→		p.nextToken() // consume comma
  2121→		p.nextToken() // move to next arg
  2122→		call.Args = append(call.Args, p.parseExpression(LOWEST))
  2123→	}
  2124→
  2125→	if !p.expectPeek(lexer.RPAREN) {
  2126→		return nil
  2127→	}
  2128→
  2129→	return call
  2130→}
  2131→
  2132→func (p *Parser) parseWhileLoop() *ast.WhileLoop {
  2133→	loop := &ast.WhileLoop{Token: p.curToken}
  2134→
  2135→	p.nextToken()
  2136→	loop.Condition = p.parseExpression(LOWEST)
  2137→
  2138→	// Skip to next line
  2139→	for p.peekTokenIs(lexer.NEWLINE) {
  2140→		p.nextToken()
  2141→	}
  2142→
  2143→	// Expect INDENT
  2144→	if !p.peekTokenIs(lexer.INDENT) {
  2145→		p.addError("expected indented block after WHILE condition")
  2146→		return loop
  2147→	}
  2148→	p.nextToken() // consume INDENT
  2149→	p.nextToken() // move to first statement
  2150→
  2151→	loop.Body = p.parseStatement()
  2152→
  2153→	// Consume until DEDENT
  2154→	for !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  2155→		p.nextToken()
  2156→	}
  2157→
  2158→	return loop
  2159→}
  2160→
  2161→func (p *Parser) parseIfStatement() *ast.IfStatement {
  2162→	stmt := &ast.IfStatement{Token: p.curToken}
  2163→
  2164→	// Check for replicator: IF i = start FOR count
  2165→	if p.peekTokenIs(lexer.IDENT) {
  2166→		p.nextToken() // move to identifier
  2167→		if p.peekTokenIs(lexer.EQ) {
  2168→			stmt.Replicator = p.parseReplicator()
  2169→		} else {
  2170→			p.addError("unexpected identifier after IF")
  2171→			return stmt
  2172→		}
  2173→	}
  2174→
  2175→	// Skip to next line
  2176→	for p.peekTokenIs(lexer.NEWLINE) {
  2177→		p.nextToken()
  2178→	}
  2179→
  2180→	// Expect INDENT
  2181→	if !p.peekTokenIs(lexer.INDENT) {
  2182→		p.addError("expected indented block after IF")
  2183→		return stmt
  2184→	}
  2185→	p.nextToken() // consume INDENT
  2186→	startLevel := p.indentLevel
  2187→	p.nextToken() // move into block
  2188→
  2189→	// Parse if choices (condition -> body pairs)
  2190→	for !p.curTokenIs(lexer.EOF) {
  2191→		// Skip newlines
  2192→		for p.curTokenIs(lexer.NEWLINE) {
  2193→			p.nextToken()
  2194→		}
  2195→
  2196→		// Handle DEDENT tokens
  2197→		for p.curTokenIs(lexer.DEDENT) {
  2198→			if p.indentLevel < startLevel {
  2199→				return stmt
  2200→			}
  2201→			p.nextToken()
  2202→		}
  2203→
  2204→		// Skip any more newlines after DEDENT
  2205→		for p.curTokenIs(lexer.NEWLINE) {
  2206→			p.nextToken()
  2207→		}
  2208→
  2209→		if p.curTokenIs(lexer.EOF) {
  2210→			break
  2211→		}
  2212→
  2213→		if p.indentLevel < startLevel {
  2214→			break
  2215→		}
  2216→
  2217→		choice := ast.IfChoice{}
  2218→		choice.Condition = p.parseExpression(LOWEST)
  2219→
  2220→		// Skip newlines and expect INDENT for body
  2221→		for p.peekTokenIs(lexer.NEWLINE) {
  2222→			p.nextToken()
  2223→		}
  2224→
  2225→		if p.peekTokenIs(lexer.INDENT) {
  2226→			p.nextToken() // consume INDENT
  2227→			p.nextToken() // move to body
  2228→			choice.Body = p.parseStatement()
  2229→
  2230→			// Advance past the last token of the statement if needed
  2231→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  2232→				p.nextToken()
  2233→			}
  2234→		}
  2235→
  2236→		stmt.Choices = append(stmt.Choices, choice)
  2237→	}
  2238→
  2239→	return stmt
  2240→}
  2241→
  2242→func (p *Parser) parseCaseStatement() *ast.CaseStatement {
  2243→	stmt := &ast.CaseStatement{Token: p.curToken}
  2244→
  2245→	// Parse selector expression on the same line
  2246→	p.nextToken()
  2247→	stmt.Selector = p.parseExpression(LOWEST)
  2248→
  2249→	// Skip to next line
  2250→	for p.peekTokenIs(lexer.NEWLINE) {
  2251→		p.nextToken()
  2252→	}
  2253→
  2254→	// Expect INDENT
  2255→	if !p.peekTokenIs(lexer.INDENT) {
  2256→		p.addError("expected indented block after CASE")
  2257→		return stmt
  2258→	}
  2259→	p.nextToken() // consume INDENT
  2260→	startLevel := p.indentLevel
  2261→	p.nextToken() // move into block
  2262→
  2263→	// Parse case choices
  2264→	for !p.curTokenIs(lexer.EOF) {
  2265→		// Skip newlines
  2266→		for p.curTokenIs(lexer.NEWLINE) {
  2267→			p.nextToken()
  2268→		}
  2269→
  2270→		// Handle DEDENT tokens
  2271→		for p.curTokenIs(lexer.DEDENT) {
  2272→			if p.indentLevel < startLevel {
  2273→				return stmt
  2274→			}
  2275→			p.nextToken()
  2276→		}
  2277→
  2278→		// Skip any more newlines after DEDENT
  2279→		for p.curTokenIs(lexer.NEWLINE) {
  2280→			p.nextToken()
  2281→		}
  2282→
  2283→		if p.curTokenIs(lexer.EOF) {
  2284→			break
  2285→		}
  2286→
  2287→		if p.indentLevel < startLevel {
  2288→			break
  2289→		}
  2290→
  2291→		choice := ast.CaseChoice{}
  2292→
  2293→		if p.curTokenIs(lexer.ELSE) {
  2294→			choice.IsElse = true
  2295→		} else {
  2296→			// Parse value expression
  2297→			choice.Values = append(choice.Values, p.parseExpression(LOWEST))
  2298→		}
  2299→
  2300→		// Skip newlines and expect INDENT for body
  2301→		for p.peekTokenIs(lexer.NEWLINE) {
  2302→			p.nextToken()
  2303→		}
  2304→
  2305→		if p.peekTokenIs(lexer.INDENT) {
  2306→			p.nextToken() // consume INDENT
  2307→			p.nextToken() // move to body
  2308→			choice.Body = p.parseStatement()
  2309→
  2310→			// Advance past the last token of the statement if needed
  2311→			if !p.curTokenIs(lexer.NEWLINE) && !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
  2312→				p.nextToken()
  2313→			}
  2314→		}
  2315→
  2316→		stmt.Choices = append(stmt.Choices, choice)
  2317→	}
  2318→
  2319→	return stmt
  2320→}
  2321→
  2322→// Expression parsing using Pratt parsing
  2323→
  2324→func (p *Parser) parseExpression(precedence int) ast.Expression {
  2325→	var left ast.Expression
  2326→
  2327→	switch p.curToken.Type {
  2328→	case lexer.IDENT:
  2329→		if p.peekTokenIs(lexer.LPAREN) {
  2330→			left = p.parseFuncCallExpr()
  2331→		} else {
  2332→			left = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}
  2333→		}
  2334→	case lexer.INT:
  2335→		base := 10
  2336→		literal := p.curToken.Literal
  2337→		if strings.HasPrefix(literal, "0x") || strings.HasPrefix(literal, "0X") {
  2338→			base = 16
  2339→			literal = literal[2:]
  2340→		}
  2341→		val, err := strconv.ParseInt(literal, base, 64)
  2342→		if err != nil {
  2343→			p.addError(fmt.Sprintf("could not parse %q as integer", p.curToken.Literal))
  2344→			return nil
  2345→		}
  2346→		left = &ast.IntegerLiteral{Token: p.curToken, Value: val}
  2347→	case lexer.TRUE:
  2348→		left = &ast.BooleanLiteral{Token: p.curToken, Value: true}
  2349→	case lexer.FALSE:
  2350→		left = &ast.BooleanLiteral{Token: p.curToken, Value: false}
  2351→	case lexer.STRING:
  2352→		left = &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal}
  2353→	case lexer.BYTE_LIT:
  2354→		b, err := p.parseByteLiteralValue(p.curToken.Literal)
  2355→		if err != nil {
  2356→			p.addError(err.Error())
  2357→			return nil
  2358→		}
  2359→		left = &ast.ByteLiteral{Token: p.curToken, Value: b}
  2360→	case lexer.LPAREN:
  2361→		p.nextToken()
  2362→		left = p.parseExpression(LOWEST)
  2363→		if !p.expectPeek(lexer.RPAREN) {
  2364→			return nil
  2365→		}
  2366→	case lexer.MINUS:
  2367→		token := p.curToken
  2368→		p.nextToken()
  2369→		left = &ast.UnaryExpr{
  2370→			Token:    token,
  2371→			Operator: "-",
  2372→			Right:    p.parseExpression(PREFIX),
  2373→		}
  2374→	case lexer.NOT:
  2375→		token := p.curToken
  2376→		p.nextToken()
  2377→		left = &ast.UnaryExpr{
  2378→			Token:    token,
  2379→			Operator: "NOT",
  2380→			Right:    p.parseExpression(PREFIX),
  2381→		}
  2382→	case lexer.BITNOT:
  2383→		token := p.curToken
  2384→		p.nextToken()
  2385→		left = &ast.UnaryExpr{
  2386→			Token:    token,
  2387→			Operator: "~",
  2388→			Right:    p.parseExpression(PREFIX),
  2389→		}
  2390→	case lexer.LBRACKET:
  2391→		// Slice expression: [arr FROM start FOR length]
  2392→		lbracket := p.curToken
  2393→		p.nextToken() // move past [
  2394→		arrayExpr := p.parseExpression(LOWEST)
  2395→		if !p.expectPeek(lexer.FROM) {
  2396→			return nil
  2397→		}
  2398→		p.nextToken() // move past FROM
  2399→		startExpr := p.parseExpression(LOWEST)
  2400→		if !p.expectPeek(lexer.FOR) {
  2401→			return nil
  2402→		}
  2403→		p.nextToken() // move past FOR
  2404→		lengthExpr := p.parseExpression(LOWEST)
  2405→		if !p.expectPeek(lexer.RBRACKET) {
  2406→			return nil
  2407→		}
  2408→		left = &ast.SliceExpr{
  2409→			Token:  lbracket,
  2410→			Array:  arrayExpr,
  2411→			Start:  startExpr,
  2412→			Length: lengthExpr,
  2413→		}
  2414→	case lexer.SIZE_KW:
  2415→		token := p.curToken
  2416→		p.nextToken()
  2417→		left = &ast.SizeExpr{
  2418→			Token: token,
  2419→			Expr:  p.parseExpression(PREFIX),
  2420→		}
  2421→	case lexer.MOSTNEG_KW, lexer.MOSTPOS_KW:
  2422→		token := p.curToken
  2423→		isNeg := token.Type == lexer.MOSTNEG_KW
  2424→		// Expect a type name next
  2425→		if !p.peekTokenIs(lexer.INT_TYPE) && !p.peekTokenIs(lexer.BYTE_TYPE) &&
  2426→			!p.peekTokenIs(lexer.BOOL_TYPE) && !p.peekTokenIs(lexer.REAL_TYPE) &&
  2427→			!p.peekTokenIs(lexer.REAL32_TYPE) && !p.peekTokenIs(lexer.REAL64_TYPE) {
  2428→			p.addError(fmt.Sprintf("expected type after %s, got %s", token.Literal, p.peekToken.Type))
  2429→			return nil
  2430→		}
  2431→		p.nextToken()
  2432→		left = &ast.MostExpr{
  2433→			Token:    token,
  2434→			ExprType: p.curToken.Literal,
  2435→			IsNeg:    isNeg,
  2436→		}
  2437→	case lexer.INT_TYPE, lexer.BYTE_TYPE, lexer.BOOL_TYPE, lexer.REAL_TYPE, lexer.REAL32_TYPE, lexer.REAL64_TYPE:
  2438→		token := p.curToken
  2439→		p.nextToken()
  2440→		left = &ast.TypeConversion{
  2441→			Token:      token,
  2442→			TargetType: token.Literal,
  2443→			Expr:       p.parseExpression(PREFIX),
  2444→		}
  2445→	default:
  2446→		p.addError(fmt.Sprintf("unexpected token in expression: %s", p.curToken.Type))
  2447→		return nil
  2448→	}
  2449→
  2450→	// Parse infix expressions
  2451→	for !p.peekTokenIs(lexer.NEWLINE) && !p.peekTokenIs(lexer.EOF) &&
  2452→		precedence < p.peekPrecedence() {
  2453→
  2454→		switch p.peekToken.Type {
  2455→		case lexer.PLUS, lexer.MINUS, lexer.MULTIPLY, lexer.DIVIDE, lexer.MODULO,
  2456→			lexer.PLUS_KW, lexer.MINUS_KW, lexer.TIMES,
  2457→			lexer.EQ, lexer.NEQ, lexer.LT, lexer.GT, lexer.LE, lexer.GE,
  2458→			lexer.AND, lexer.OR, lexer.AFTER,
  2459→			lexer.BITAND, lexer.BITOR, lexer.BITXOR, lexer.LSHIFT, lexer.RSHIFT:
  2460→			p.nextToken()
  2461→			left = p.parseBinaryExpr(left)
  2462→		case lexer.LBRACKET:
  2463→			p.nextToken()
  2464→			left = p.parseIndexExpression(left)
  2465→		default:
  2466→			return left
  2467→		}
  2468→	}
  2469→
  2470→	return left
  2471→}
  2472→
  2473→func (p *Parser) parseBinaryExpr(left ast.Expression) ast.Expression {
  2474→	expr := &ast.BinaryExpr{
  2475→		Token:    p.curToken,
  2476→		Left:     left,
  2477→		Operator: p.curToken.Literal,
  2478→	}
  2479→
  2480→	prec := p.curPrecedence()
  2481→	p.nextToken()
  2482→	expr.Right = p.parseExpression(prec)
  2483→
  2484→	return expr
  2485→}
  2486→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go"
}
     1→package codegen
     2→
     3→import (
     4→	"fmt"
     5→	"strings"
     6→
     7→	"github.com/codeassociates/occam2go/ast"
     8→)
     9→
    10→// Generator converts an AST to Go code
    11→type Generator struct {
    12→	indent   int
    13→	builder  strings.Builder
    14→	needSync bool // track if we need sync package import
    15→	needFmt  bool // track if we need fmt package import
    16→	needTime bool // track if we need time package import
    17→	needOs   bool // track if we need os package import
    18→	needMath bool // track if we need math package import
    19→
    20→	// Track procedure signatures for proper pointer handling
    21→	procSigs map[string][]ast.ProcParam
    22→	// Track current procedure's reference parameters
    23→	refParams map[string]bool
    24→
    25→	// Protocol support
    26→	protocolDefs  map[string]*ast.ProtocolDecl
    27→	chanProtocols map[string]string // channel name → protocol name
    28→	tmpCounter    int               // for unique temp variable names
    29→
    30→	// Record support
    31→	recordDefs map[string]*ast.RecordDecl
    32→	recordVars map[string]string // variable name → record type name
    33→
    34→	// Nesting level: 0 = package level, >0 = inside a function
    35→	nestingLevel int
    36→}
    37→
    38→// Built-in print procedures
    39→var printBuiltins = map[string]bool{
    40→	"print.int":     true,
    41→	"print.string":  true,
    42→	"print.bool":    true,
    43→	"print.newline": true,
    44→}
    45→
    46→// New creates a new code generator
    47→func New() *Generator {
    48→	return &Generator{}
    49→}
    50→
    51→// Generate produces Go code from the AST
    52→func (g *Generator) Generate(program *ast.Program) string {
    53→	g.builder.Reset()
    54→	g.needSync = false
    55→	g.needFmt = false
    56→	g.needTime = false
    57→	g.needOs = false
    58→	g.needMath = false
    59→	g.procSigs = make(map[string][]ast.ProcParam)
    60→	g.refParams = make(map[string]bool)
    61→	g.protocolDefs = make(map[string]*ast.ProtocolDecl)
    62→	g.chanProtocols = make(map[string]string)
    63→	g.tmpCounter = 0
    64→	g.recordDefs = make(map[string]*ast.RecordDecl)
    65→	g.recordVars = make(map[string]string)
    66→
    67→	// First pass: collect procedure signatures, protocols, and check for PAR/print
    68→	for _, stmt := range program.Statements {
    69→		if g.containsPar(stmt) {
    70→			g.needSync = true
    71→		}
    72→		if g.containsPrint(stmt) {
    73→			g.needFmt = true
    74→		}
    75→		if g.containsTimer(stmt) {
    76→			g.needTime = true
    77→		}
    78→		if g.containsStop(stmt) {
    79→			g.needOs = true
    80→			g.needFmt = true
    81→		}
    82→		if g.containsMostExpr(stmt) {
    83→			g.needMath = true
    84→		}
    85→		if proc, ok := stmt.(*ast.ProcDecl); ok {
    86→			g.procSigs[proc.Name] = proc.Params
    87→			g.collectNestedProcSigs(proc.Body)
    88→		}
    89→		if fn, ok := stmt.(*ast.FuncDecl); ok {
    90→			g.procSigs[fn.Name] = fn.Params
    91→		}
    92→		if proto, ok := stmt.(*ast.ProtocolDecl); ok {
    93→			g.protocolDefs[proto.Name] = proto
    94→		}
    95→		if rec, ok := stmt.(*ast.RecordDecl); ok {
    96→			g.recordDefs[rec.Name] = rec
    97→		}
    98→		g.collectChanProtocols(stmt)
    99→		g.collectRecordVars(stmt)
   100→	}
   101→
   102→	// Write package declaration
   103→	g.writeLine("package main")
   104→	g.writeLine("")
   105→
   106→	// Write imports
   107→	if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath {
   108→		g.writeLine("import (")
   109→		g.indent++
   110→		if g.needFmt {
   111→			g.writeLine(`"fmt"`)
   112→		}
   113→		if g.needMath {
   114→			g.writeLine(`"math"`)
   115→		}
   116→		if g.needOs {
   117→			g.writeLine(`"os"`)
   118→		}
   119→		if g.needSync {
   120→			g.writeLine(`"sync"`)
   121→		}
   122→		if g.needTime {
   123→			g.writeLine(`"time"`)
   124→		}
   125→		g.indent--
   126→		g.writeLine(")")
   127→		g.writeLine("")
   128→	}
   129→
   130→	// Separate protocol, record, procedure declarations from other statements
   131→	var typeDecls []ast.Statement
   132→	var procDecls []ast.Statement
   133→	var mainStatements []ast.Statement
   134→
   135→	for _, stmt := range program.Statements {
   136→		switch stmt.(type) {
   137→		case *ast.ProtocolDecl, *ast.RecordDecl:
   138→			typeDecls = append(typeDecls, stmt)
   139→		case *ast.ProcDecl, *ast.FuncDecl:
   140→			procDecls = append(procDecls, stmt)
   141→		default:
   142→			mainStatements = append(mainStatements, stmt)
   143→		}
   144→	}
   145→
   146→	// Generate type definitions first (at package level)
   147→	for _, stmt := range typeDecls {
   148→		g.generateStatement(stmt)
   149→	}
   150→
   151→	// Generate procedure declarations (at package level)
   152→	for _, stmt := range procDecls {
   153→		g.generateStatement(stmt)
   154→	}
   155→
   156→	// Generate main function with other statements
   157→	if len(mainStatements) > 0 {
   158→		g.writeLine("func main() {")
   159→		g.indent++
   160→		g.nestingLevel++
   161→		for _, stmt := range mainStatements {
   162→			g.generateStatement(stmt)
   163→		}
   164→		g.nestingLevel--
   165→		g.indent--
   166→		g.writeLine("}")
   167→	}
   168→
   169→	return g.builder.String()
   170→}
   171→
   172→// collectNestedProcSigs recursively collects procedure/function signatures
   173→// from nested declarations inside PROC bodies.
   174→func (g *Generator) collectNestedProcSigs(stmts []ast.Statement) {
   175→	for _, stmt := range stmts {
   176→		if proc, ok := stmt.(*ast.ProcDecl); ok {
   177→			g.procSigs[proc.Name] = proc.Params
   178→			g.collectNestedProcSigs(proc.Body)
   179→		}
   180→		if fn, ok := stmt.(*ast.FuncDecl); ok {
   181→			g.procSigs[fn.Name] = fn.Params
   182→		}
   183→	}
   184→}
   185→
   186→func (g *Generator) containsPar(stmt ast.Statement) bool {
   187→	switch s := stmt.(type) {
   188→	case *ast.ParBlock:
   189→		return true
   190→	case *ast.SeqBlock:
   191→		for _, inner := range s.Statements {
   192→			if g.containsPar(inner) {
   193→				return true
   194→			}
   195→		}
   196→	case *ast.AltBlock:
   197→		for _, c := range s.Cases {
   198→			if c.Body != nil && g.containsPar(c.Body) {
   199→				return true
   200→			}
   201→		}
   202→	case *ast.ProcDecl:
   203→		for _, inner := range s.Body {
   204→			if g.containsPar(inner) {
   205→				return true
   206→			}
   207→		}
   208→	case *ast.FuncDecl:
   209→		for _, inner := range s.Body {
   210→			if g.containsPar(inner) {
   211→				return true
   212→			}
   213→		}
   214→	case *ast.WhileLoop:
   215→		if s.Body != nil && g.containsPar(s.Body) {
   216→			return true
   217→		}
   218→	case *ast.IfStatement:
   219→		for _, choice := range s.Choices {
   220→			if choice.Body != nil && g.containsPar(choice.Body) {
   221→				return true
   222→			}
   223→		}
   224→	case *ast.CaseStatement:
   225→		for _, choice := range s.Choices {
   226→			if choice.Body != nil && g.containsPar(choice.Body) {
   227→				return true
   228→			}
   229→		}
   230→	case *ast.VariantReceive:
   231→		for _, c := range s.Cases {
   232→			if c.Body != nil && g.containsPar(c.Body) {
   233→				return true
   234→			}
   235→		}
   236→	}
   237→	return false
   238→}
   239→
   240→func (g *Generator) containsPrint(stmt ast.Statement) bool {
   241→	switch s := stmt.(type) {
   242→	case *ast.ProcCall:
   243→		return printBuiltins[s.Name]
   244→	case *ast.SeqBlock:
   245→		for _, inner := range s.Statements {
   246→			if g.containsPrint(inner) {
   247→				return true
   248→			}
   249→		}
   250→	case *ast.ParBlock:
   251→		for _, inner := range s.Statements {
   252→			if g.containsPrint(inner) {
   253→				return true
   254→			}
   255→		}
   256→	case *ast.AltBlock:
   257→		for _, c := range s.Cases {
   258→			if c.Body != nil && g.containsPrint(c.Body) {
   259→				return true
   260→			}
   261→		}
   262→	case *ast.ProcDecl:
   263→		for _, inner := range s.Body {
   264→			if g.containsPrint(inner) {
   265→				return true
   266→			}
   267→		}
   268→	case *ast.FuncDecl:
   269→		for _, inner := range s.Body {
   270→			if g.containsPrint(inner) {
   271→				return true
   272→			}
   273→		}
   274→	case *ast.WhileLoop:
   275→		if s.Body != nil && g.containsPrint(s.Body) {
   276→			return true
   277→		}
   278→	case *ast.IfStatement:
   279→		for _, choice := range s.Choices {
   280→			if choice.Body != nil && g.containsPrint(choice.Body) {
   281→				return true
   282→			}
   283→		}
   284→	case *ast.CaseStatement:
   285→		for _, choice := range s.Choices {
   286→			if choice.Body != nil && g.containsPrint(choice.Body) {
   287→				return true
   288→			}
   289→		}
   290→	case *ast.VariantReceive:
   291→		for _, c := range s.Cases {
   292→			if c.Body != nil && g.containsPrint(c.Body) {
   293→				return true
   294→			}
   295→		}
   296→	}
   297→	return false
   298→}
   299→
   300→func (g *Generator) containsTimer(stmt ast.Statement) bool {
   301→	switch s := stmt.(type) {
   302→	case *ast.TimerDecl, *ast.TimerRead:
   303→		return true
   304→	case *ast.AltBlock:
   305→		for _, c := range s.Cases {
   306→			if c.IsTimer {
   307→				return true
   308→			}
   309→			if c.Body != nil && g.containsTimer(c.Body) {
   310→				return true
   311→			}
   312→		}
   313→	case *ast.SeqBlock:
   314→		for _, inner := range s.Statements {
   315→			if g.containsTimer(inner) {
   316→				return true
   317→			}
   318→		}
   319→	case *ast.ParBlock:
   320→		for _, inner := range s.Statements {
   321→			if g.containsTimer(inner) {
   322→				return true
   323→			}
   324→		}
   325→	case *ast.ProcDecl:
   326→		for _, inner := range s.Body {
   327→			if g.containsTimer(inner) {
   328→				return true
   329→			}
   330→		}
   331→	case *ast.FuncDecl:
   332→		for _, inner := range s.Body {
   333→			if g.containsTimer(inner) {
   334→				return true
   335→			}
   336→		}
   337→	case *ast.WhileLoop:
   338→		if s.Body != nil && g.containsTimer(s.Body) {
   339→			return true
   340→		}
   341→	case *ast.IfStatement:
   342→		for _, choice := range s.Choices {
   343→			if choice.Body != nil && g.containsTimer(choice.Body) {
   344→				return true
   345→			}
   346→		}
   347→	case *ast.CaseStatement:
   348→		for _, choice := range s.Choices {
   349→			if choice.Body != nil && g.containsTimer(choice.Body) {
   350→				return true
   351→			}
   352→		}
   353→	case *ast.VariantReceive:
   354→		for _, c := range s.Cases {
   355→			if c.Body != nil && g.containsTimer(c.Body) {
   356→				return true
   357→			}
   358→		}
   359→	}
   360→	return false
   361→}
   362→
   363→func (g *Generator) containsStop(stmt ast.Statement) bool {
   364→	switch s := stmt.(type) {
   365→	case *ast.Stop:
   366→		return true
   367→	case *ast.SeqBlock:
   368→		for _, inner := range s.Statements {
   369→			if g.containsStop(inner) {
   370→				return true
   371→			}
   372→		}
   373→	case *ast.ParBlock:
   374→		for _, inner := range s.Statements {
   375→			if g.containsStop(inner) {
   376→				return true
   377→			}
   378→		}
   379→	case *ast.AltBlock:
   380→		for _, c := range s.Cases {
   381→			if c.Body != nil && g.containsStop(c.Body) {
   382→				return true
   383→			}
   384→		}
   385→	case *ast.ProcDecl:
   386→		for _, inner := range s.Body {
   387→			if g.containsStop(inner) {
   388→				return true
   389→			}
   390→		}
   391→	case *ast.FuncDecl:
   392→		for _, inner := range s.Body {
   393→			if g.containsStop(inner) {
   394→				return true
   395→			}
   396→		}
   397→	case *ast.WhileLoop:
   398→		if s.Body != nil && g.containsStop(s.Body) {
   399→			return true
   400→		}
   401→	case *ast.IfStatement:
   402→		for _, choice := range s.Choices {
   403→			if choice.Body != nil && g.containsStop(choice.Body) {
   404→				return true
   405→			}
   406→		}
   407→	case *ast.CaseStatement:
   408→		for _, choice := range s.Choices {
   409→			if choice.Body != nil && g.containsStop(choice.Body) {
   410→				return true
   411→			}
   412→		}
   413→	case *ast.VariantReceive:
   414→		for _, c := range s.Cases {
   415→			if c.Body != nil && g.containsStop(c.Body) {
   416→				return true
   417→			}
   418→		}
   419→	}
   420→	return false
   421→}
   422→
   423→func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
   424→	switch s := stmt.(type) {
   425→	case *ast.Assignment:
   426→		return g.exprNeedsMath(s.Value) || g.exprNeedsMath(s.Index)
   427→	case *ast.MultiAssignment:
   428→		for _, t := range s.Targets {
   429→			if g.exprNeedsMath(t.Index) {
   430→				return true
   431→			}
   432→		}
   433→		for _, v := range s.Values {
   434→			if g.exprNeedsMath(v) {
   435→				return true
   436→			}
   437→		}
   438→	case *ast.Abbreviation:
   439→		return g.exprNeedsMath(s.Value)
   440→	case *ast.SeqBlock:
   441→		for _, inner := range s.Statements {
   442→			if g.containsMostExpr(inner) {
   443→				return true
   444→			}
   445→		}
   446→	case *ast.ParBlock:
   447→		for _, inner := range s.Statements {
   448→			if g.containsMostExpr(inner) {
   449→				return true
   450→			}
   451→		}
   452→	case *ast.ProcDecl:
   453→		for _, inner := range s.Body {
   454→			if g.containsMostExpr(inner) {
   455→				return true
   456→			}
   457→		}
   458→	case *ast.FuncDecl:
   459→		for _, inner := range s.Body {
   460→			if g.containsMostExpr(inner) {
   461→				return true
   462→			}
   463→		}
   464→	case *ast.WhileLoop:
   465→		if g.exprNeedsMath(s.Condition) {
   466→			return true
   467→		}
   468→		if s.Body != nil && g.containsMostExpr(s.Body) {
   469→			return true
   470→		}
   471→	case *ast.IfStatement:
   472→		for _, choice := range s.Choices {
   473→			if g.exprNeedsMath(choice.Condition) {
   474→				return true
   475→			}
   476→			if choice.Body != nil && g.containsMostExpr(choice.Body) {
   477→				return true
   478→			}
   479→		}
   480→	case *ast.CaseStatement:
   481→		if g.exprNeedsMath(s.Selector) {
   482→			return true
   483→		}
   484→		for _, choice := range s.Choices {
   485→			for _, v := range choice.Values {
   486→				if g.exprNeedsMath(v) {
   487→					return true
   488→				}
   489→			}
   490→			if choice.Body != nil && g.containsMostExpr(choice.Body) {
   491→				return true
   492→			}
   493→		}
   494→	case *ast.Send:
   495→		if g.exprNeedsMath(s.Value) {
   496→			return true
   497→		}
   498→		for _, v := range s.Values {
   499→			if g.exprNeedsMath(v) {
   500→				return true
   501→			}
   502→		}
   503→	case *ast.ProcCall:
   504→		for _, arg := range s.Args {
   505→			if g.exprNeedsMath(arg) {
   506→				return true
   507→			}
   508→		}
   509→	case *ast.AltBlock:
   510→		for _, c := range s.Cases {
   511→			if c.Body != nil && g.containsMostExpr(c.Body) {
   512→				return true
   513→			}
   514→		}
   515→	case *ast.VariantReceive:
   516→		for _, c := range s.Cases {
   517→			if c.Body != nil && g.containsMostExpr(c.Body) {
   518→				return true
   519→			}
   520→		}
   521→	}
   522→	return false
   523→}
   524→
   525→func (g *Generator) exprNeedsMath(expr ast.Expression) bool {
   526→	if expr == nil {
   527→		return false
   528→	}
   529→	switch e := expr.(type) {
   530→	case *ast.MostExpr:
   531→		// BYTE uses literal 0/255, doesn't need math
   532→		return e.ExprType != "BYTE"
   533→	case *ast.BinaryExpr:
   534→		return g.exprNeedsMath(e.Left) || g.exprNeedsMath(e.Right)
   535→	case *ast.UnaryExpr:
   536→		return g.exprNeedsMath(e.Right)
   537→	case *ast.ParenExpr:
   538→		return g.exprNeedsMath(e.Expr)
   539→	case *ast.TypeConversion:
   540→		return g.exprNeedsMath(e.Expr)
   541→	case *ast.SizeExpr:
   542→		return g.exprNeedsMath(e.Expr)
   543→	case *ast.IndexExpr:
   544→		return g.exprNeedsMath(e.Left) || g.exprNeedsMath(e.Index)
   545→	case *ast.FuncCall:
   546→		for _, arg := range e.Args {
   547→			if g.exprNeedsMath(arg) {
   548→				return true
   549→			}
   550→		}
   551→	case *ast.SliceExpr:
   552→		return g.exprNeedsMath(e.Array) || g.exprNeedsMath(e.Start) || g.exprNeedsMath(e.Length)
   553→	}
   554→	return false
   555→}
   556→
   557→func (g *Generator) generateMostExpr(e *ast.MostExpr) {
   558→	switch e.ExprType {
   559→	case "INT":
   560→		if e.IsNeg {
   561→			g.write("math.MinInt")
   562→		} else {
   563→			g.write("math.MaxInt")
   564→		}
   565→	case "BYTE":
   566→		if e.IsNeg {
   567→			g.write("0")
   568→		} else {
   569→			g.write("255")
   570→		}
   571→	case "REAL32":
   572→		if e.IsNeg {
   573→			g.write("-math.MaxFloat32")
   574→		} else {
   575→			g.write("math.MaxFloat32")
   576→		}
   577→	case "REAL64":
   578→		if e.IsNeg {
   579→			g.write("-math.MaxFloat64")
   580→		} else {
   581→			g.write("math.MaxFloat64")
   582→		}
   583→	}
   584→}
   585→
   586→func (g *Generator) writeLine(s string) {
   587→	if s == "" {
   588→		g.builder.WriteString("\n")
   589→		return
   590→	}
   591→	g.builder.WriteString(strings.Repeat("\t", g.indent))
   592→	g.builder.WriteString(s)
   593→	g.builder.WriteString("\n")
   594→}
   595→
   596→func (g *Generator) write(s string) {
   597→	g.builder.WriteString(s)
   598→}
   599→
   600→func (g *Generator) generateStatement(stmt ast.Statement) {
   601→	switch s := stmt.(type) {
   602→	case *ast.VarDecl:
   603→		g.generateVarDecl(s)
   604→	case *ast.ArrayDecl:
   605→		g.generateArrayDecl(s)
   606→	case *ast.ChanDecl:
   607→		g.generateChanDecl(s)
   608→	case *ast.Assignment:
   609→		g.generateAssignment(s)
   610→	case *ast.Send:
   611→		g.generateSend(s)
   612→	case *ast.Receive:
   613→		g.generateReceive(s)
   614→	case *ast.SeqBlock:
   615→		g.generateSeqBlock(s)
   616→	case *ast.ParBlock:
   617→		g.generateParBlock(s)
   618→	case *ast.AltBlock:
   619→		g.generateAltBlock(s)
   620→	case *ast.Skip:
   621→		g.writeLine("// SKIP")
   622→	case *ast.Stop:
   623→		g.writeLine(`fmt.Fprintln(os.Stderr, "STOP encountered")`)
   624→		g.writeLine("select {}")
   625→	case *ast.ProcDecl:
   626→		g.generateProcDecl(s)
   627→	case *ast.FuncDecl:
   628→		g.generateFuncDecl(s)
   629→	case *ast.ProcCall:
   630→		g.generateProcCall(s)
   631→	case *ast.WhileLoop:
   632→		g.generateWhileLoop(s)
   633→	case *ast.IfStatement:
   634→		g.generateIfStatement(s)
   635→	case *ast.CaseStatement:
   636→		g.generateCaseStatement(s)
   637→	case *ast.TimerDecl:
   638→		g.generateTimerDecl(s)
   639→	case *ast.TimerRead:
   640→		g.generateTimerRead(s)
   641→	case *ast.ProtocolDecl:
   642→		g.generateProtocolDecl(s)
   643→	case *ast.VariantReceive:
   644→		g.generateVariantReceive(s)
   645→	case *ast.RecordDecl:
   646→		g.generateRecordDecl(s)
   647→	case *ast.Abbreviation:
   648→		g.generateAbbreviation(s)
   649→	case *ast.MultiAssignment:
   650→		g.generateMultiAssignment(s)
   651→	}
   652→}
   653→
   654→func (g *Generator) generateVarDecl(decl *ast.VarDecl) {
   655→	goType := g.occamTypeToGo(decl.Type)
   656→	g.writeLine(fmt.Sprintf("var %s %s", strings.Join(decl.Names, ", "), goType))
   657→}
   658→
   659→func (g *Generator) generateAbbreviation(abbr *ast.Abbreviation) {
   660→	g.builder.WriteString(strings.Repeat("\t", g.indent))
   661→	g.write(fmt.Sprintf("%s := ", abbr.Name))
   662→	g.generateExpression(abbr.Value)
   663→	g.write("\n")
   664→}
   665→
   666→func (g *Generator) generateChanDecl(decl *ast.ChanDecl) {
   667→	goType := g.occamTypeToGo(decl.ElemType)
   668→	if decl.IsArray {
   669→		for _, name := range decl.Names {
   670→			g.builder.WriteString(strings.Repeat("\t", g.indent))
   671→			g.write(fmt.Sprintf("%s := make([]chan %s, ", name, goType))
   672→			g.generateExpression(decl.Size)
   673→			g.write(")\n")
   674→			g.builder.WriteString(strings.Repeat("\t", g.indent))
   675→			g.write(fmt.Sprintf("for _i := range %s { %s[_i] = make(chan %s) }\n", name, name, goType))
   676→		}
   677→	} else {
   678→		for _, name := range decl.Names {
   679→			g.writeLine(fmt.Sprintf("%s := make(chan %s)", name, goType))
   680→		}
   681→	}
   682→}
   683→
   684→func (g *Generator) generateTimerDecl(decl *ast.TimerDecl) {
   685→	for _, name := range decl.Names {
   686→		g.writeLine(fmt.Sprintf("// TIMER %s", name))
   687→	}
   688→}
   689→
   690→func (g *Generator) generateTimerRead(tr *ast.TimerRead) {
   691→	g.writeLine(fmt.Sprintf("%s = int(time.Now().UnixMicro())", tr.Variable))
   692→}
   693→
   694→func (g *Generator) generateArrayDecl(decl *ast.ArrayDecl) {
   695→	goType := g.occamTypeToGo(decl.Type)
   696→	for _, name := range decl.Names {
   697→		g.builder.WriteString(strings.Repeat("\t", g.indent))
   698→		g.write(fmt.Sprintf("%s := make([]%s, ", name, goType))
   699→		g.generateExpression(decl.Size)
   700→		g.write(")\n")
   701→	}
   702→}
   703→
   704→func (g *Generator) generateSend(send *ast.Send) {
   705→	g.builder.WriteString(strings.Repeat("\t", g.indent))
   706→	g.write(send.Channel)
   707→	if send.ChannelIndex != nil {
   708→		g.write("[")
   709→		g.generateExpression(send.ChannelIndex)
   710→		g.write("]")
   711→	}
   712→	g.write(" <- ")
   713→
   714→	protoName := g.chanProtocols[send.Channel]
   715→	proto := g.protocolDefs[protoName]
   716→
   717→	if send.VariantTag != "" && proto != nil && proto.Kind == "variant" {
   718→		// Variant send with explicit tag: c <- _proto_NAME_tag{values...}
   719→		g.write(fmt.Sprintf("_proto_%s_%s{", protoName, send.VariantTag))
   720→		for i, val := range send.Values {
   721→			if i > 0 {
   722→				g.write(", ")
   723→			}
   724→			g.generateExpression(val)
   725→		}
   726→		g.write("}")
   727→	} else if proto != nil && proto.Kind == "variant" && send.Value != nil && len(send.Values) == 0 {
   728→		// Check if the send value is a bare identifier matching a variant tag
   729→		if ident, ok := send.Value.(*ast.Identifier); ok && g.isVariantTag(protoName, ident.Value) {
   730→			g.write(fmt.Sprintf("_proto_%s_%s{}", protoName, ident.Value))
   731→		} else {
   732→			g.generateExpression(send.Value)
   733→		}
   734→	} else if len(send.Values) > 0 && proto != nil && proto.Kind == "sequential" {
   735→		// Sequential send: c <- _proto_NAME{val1, val2, ...}
   736→		g.write(fmt.Sprintf("_proto_%s{", protoName))
   737→		g.generateExpression(send.Value)
   738→		for _, val := range send.Values {
   739→			g.write(", ")
   740→			g.generateExpression(val)
   741→		}
   742→		g.write("}")
   743→	} else {
   744→		// Simple send
   745→		g.generateExpression(send.Value)
   746→	}
   747→	g.write("\n")
   748→}
   749→
   750→func (g *Generator) generateReceive(recv *ast.Receive) {
   751→	chanRef := recv.Channel
   752→	if recv.ChannelIndex != nil {
   753→		var buf strings.Builder
   754→		buf.WriteString(recv.Channel)
   755→		buf.WriteString("[")
   756→		// Generate the index expression into a temporary buffer
   757→		oldBuilder := g.builder
   758→		g.builder = strings.Builder{}
   759→		g.generateExpression(recv.ChannelIndex)
   760→		buf.WriteString(g.builder.String())
   761→		g.builder = oldBuilder
   762→		buf.WriteString("]")
   763→		chanRef = buf.String()
   764→	}
   765→
   766→	if len(recv.Variables) > 0 {
   767→		// Sequential receive: _tmpN := <-c; x = _tmpN._0; y = _tmpN._1
   768→		tmpName := fmt.Sprintf("_tmp%d", g.tmpCounter)
   769→		g.tmpCounter++
   770→		g.writeLine(fmt.Sprintf("%s := <-%s", tmpName, chanRef))
   771→		g.writeLine(fmt.Sprintf("%s = %s._0", recv.Variable, tmpName))
   772→		for i, v := range recv.Variables {
   773→			g.writeLine(fmt.Sprintf("%s = %s._%d", v, tmpName, i+1))
   774→		}
   775→	} else {
   776→		g.writeLine(fmt.Sprintf("%s = <-%s", recv.Variable, chanRef))
   777→	}
   778→}
   779→
   780→func (g *Generator) generateProtocolDecl(proto *ast.ProtocolDecl) {
   781→	switch proto.Kind {
   782→	case "simple":
   783→		goType := g.occamTypeToGoBase(proto.Types[0])
   784→		g.writeLine(fmt.Sprintf("type _proto_%s = %s", proto.Name, goType))
   785→		g.writeLine("")
   786→	case "sequential":
   787→		g.writeLine(fmt.Sprintf("type _proto_%s struct {", proto.Name))
   788→		g.indent++
   789→		for i, t := range proto.Types {
   790→			goType := g.occamTypeToGoBase(t)
   791→			g.writeLine(fmt.Sprintf("_%d %s", i, goType))
   792→		}
   793→		g.indent--
   794→		g.writeLine("}")
   795→		g.writeLine("")
   796→	case "variant":
   797→		// Interface type
   798→		g.writeLine(fmt.Sprintf("type _proto_%s interface {", proto.Name))
   799→		g.indent++
   800→		g.writeLine(fmt.Sprintf("_is_%s()", proto.Name))
   801→		g.indent--
   802→		g.writeLine("}")
   803→		g.writeLine("")
   804→		// Concrete types for each variant
   805→		for _, v := range proto.Variants {
   806→			if len(v.Types) == 0 {
   807→				// No-payload variant: empty struct
   808→				g.writeLine(fmt.Sprintf("type _proto_%s_%s struct{}", proto.Name, v.Tag))
   809→			} else {
   810→				g.writeLine(fmt.Sprintf("type _proto_%s_%s struct {", proto.Name, v.Tag))
   811→				g.indent++
   812→				for i, t := range v.Types {
   813→					goType := g.occamTypeToGoBase(t)
   814→					g.writeLine(fmt.Sprintf("_%d %s", i, goType))
   815→				}
   816→				g.indent--
   817→				g.writeLine("}")
   818→			}
   819→			g.writeLine(fmt.Sprintf("func (_proto_%s_%s) _is_%s() {}", proto.Name, v.Tag, proto.Name))
   820→			g.writeLine("")
   821→		}
   822→	}
   823→}
   824→
   825→func (g *Generator) generateVariantReceive(vr *ast.VariantReceive) {
   826→	protoName := g.chanProtocols[vr.Channel]
   827→	chanRef := vr.Channel
   828→	if vr.ChannelIndex != nil {
   829→		var buf strings.Builder
   830→		buf.WriteString(vr.Channel)
   831→		buf.WriteString("[")
   832→		oldBuilder := g.builder
   833→		g.builder = strings.Builder{}
   834→		g.generateExpression(vr.ChannelIndex)
   835→		buf.WriteString(g.builder.String())
   836→		g.builder = oldBuilder
   837→		buf.WriteString("]")
   838→		chanRef = buf.String()
   839→	}
   840→	g.writeLine(fmt.Sprintf("switch _v := (<-%s).(type) {", chanRef))
   841→	for _, vc := range vr.Cases {
   842→		g.writeLine(fmt.Sprintf("case _proto_%s_%s:", protoName, vc.Tag))
   843→		g.indent++
   844→		for i, v := range vc.Variables {
   845→			g.writeLine(fmt.Sprintf("%s = _v._%d", v, i))
   846→		}
   847→		if vc.Body != nil {
   848→			g.generateStatement(vc.Body)
   849→		}
   850→		g.indent--
   851→	}
   852→	g.writeLine("}")
   853→}
   854→
   855→func (g *Generator) isVariantTag(protoName, tagName string) bool {
   856→	proto := g.protocolDefs[protoName]
   857→	if proto == nil {
   858→		return false
   859→	}
   860→	for _, v := range proto.Variants {
   861→		if v.Tag == tagName {
   862→			return true
   863→		}
   864→	}
   865→	return false
   866→}
   867→
   868→func (g *Generator) collectChanProtocols(stmt ast.Statement) {
   869→	switch s := stmt.(type) {
   870→	case *ast.ChanDecl:
   871→		if _, ok := g.protocolDefs[s.ElemType]; ok {
   872→			for _, name := range s.Names {
   873→				g.chanProtocols[name] = s.ElemType
   874→			}
   875→		}
   876→	case *ast.SeqBlock:
   877→		for _, inner := range s.Statements {
   878→			g.collectChanProtocols(inner)
   879→		}
   880→	case *ast.ParBlock:
   881→		for _, inner := range s.Statements {
   882→			g.collectChanProtocols(inner)
   883→		}
   884→	case *ast.ProcDecl:
   885→		// Register PROC param channels (including channel array params)
   886→		for _, p := range s.Params {
   887→			if p.IsChan || p.IsChanArray {
   888→				if _, ok := g.protocolDefs[p.ChanElemType]; ok {
   889→					g.chanProtocols[p.Name] = p.ChanElemType
   890→				}
   891→			}
   892→		}
   893→		for _, inner := range s.Body {
   894→			g.collectChanProtocols(inner)
   895→		}
   896→	case *ast.FuncDecl:
   897→		for _, inner := range s.Body {
   898→			g.collectChanProtocols(inner)
   899→		}
   900→	case *ast.WhileLoop:
   901→		if s.Body != nil {
   902→			g.collectChanProtocols(s.Body)
   903→		}
   904→	case *ast.IfStatement:
   905→		for _, choice := range s.Choices {
   906→			if choice.Body != nil {
   907→				g.collectChanProtocols(choice.Body)
   908→			}
   909→		}
   910→	case *ast.CaseStatement:
   911→		for _, choice := range s.Choices {
   912→			if choice.Body != nil {
   913→				g.collectChanProtocols(choice.Body)
   914→			}
   915→		}
   916→	case *ast.AltBlock:
   917→		for _, c := range s.Cases {
   918→			if c.Body != nil {
   919→				g.collectChanProtocols(c.Body)
   920→			}
   921→		}
   922→	}
   923→}
   924→
   925→func (g *Generator) collectRecordVars(stmt ast.Statement) {
   926→	switch s := stmt.(type) {
   927→	case *ast.VarDecl:
   928→		if _, ok := g.recordDefs[s.Type]; ok {
   929→			for _, name := range s.Names {
   930→				g.recordVars[name] = s.Type
   931→			}
   932→		}
   933→	case *ast.SeqBlock:
   934→		for _, inner := range s.Statements {
   935→			g.collectRecordVars(inner)
   936→		}
   937→	case *ast.ParBlock:
   938→		for _, inner := range s.Statements {
   939→			g.collectRecordVars(inner)
   940→		}
   941→	case *ast.ProcDecl:
   942→		for _, p := range s.Params {
   943→			if !p.IsChan {
   944→				if _, ok := g.recordDefs[p.Type]; ok {
   945→					g.recordVars[p.Name] = p.Type
   946→				}
   947→			}
   948→		}
   949→		for _, inner := range s.Body {
   950→			g.collectRecordVars(inner)
   951→		}
   952→	case *ast.FuncDecl:
   953→		for _, inner := range s.Body {
   954→			g.collectRecordVars(inner)
   955→		}
   956→	case *ast.WhileLoop:
   957→		if s.Body != nil {
   958→			g.collectRecordVars(s.Body)
   959→		}
   960→	case *ast.IfStatement:
   961→		for _, choice := range s.Choices {
   962→			if choice.Body != nil {
   963→				g.collectRecordVars(choice.Body)
   964→			}
   965→		}
   966→	case *ast.CaseStatement:
   967→		for _, choice := range s.Choices {
   968→			if choice.Body != nil {
   969→				g.collectRecordVars(choice.Body)
   970→			}
   971→		}
   972→	case *ast.AltBlock:
   973→		for _, c := range s.Cases {
   974→			if c.Body != nil {
   975→				g.collectRecordVars(c.Body)
   976→			}
   977→		}
   978→	}
   979→}
   980→
   981→func (g *Generator) generateRecordDecl(rec *ast.RecordDecl) {
   982→	g.writeLine(fmt.Sprintf("type %s struct {", rec.Name))
   983→	g.indent++
   984→	for _, f := range rec.Fields {
   985→		goType := g.occamTypeToGoBase(f.Type)
   986→		g.writeLine(fmt.Sprintf("%s %s", f.Name, goType))
   987→	}
   988→	g.indent--
   989→	g.writeLine("}")
   990→	g.writeLine("")
   991→}
   992→
   993→// occamTypeToGoBase converts a type name without checking protocol defs
   994→// (used inside protocol generation to avoid infinite recursion)
   995→func (g *Generator) occamTypeToGoBase(occamType string) string {
   996→	switch occamType {
   997→	case "INT":
   998→		return "int"
   999→	case "BYTE":
  1000→		return "byte"
  1001→	case "BOOL":
  1002→		return "bool"
  1003→	case "REAL":
  1004→		return "float64"
  1005→	case "REAL32":
  1006→		return "float32"
  1007→	case "REAL64":
  1008→		return "float64"
  1009→	default:
  1010→		return occamType
  1011→	}
  1012→}
  1013→
  1014→func (g *Generator) occamTypeToGo(occamType string) string {
  1015→	switch occamType {
  1016→	case "INT":
  1017→		return "int"
  1018→	case "BYTE":
  1019→		return "byte"
  1020→	case "BOOL":
  1021→		return "bool"
  1022→	case "REAL":
  1023→		return "float64"
  1024→	case "REAL32":
  1025→		return "float32"
  1026→	case "REAL64":
  1027→		return "float64"
  1028→	default:
  1029→		// Check if it's a protocol name
  1030→		if _, ok := g.protocolDefs[occamType]; ok {
  1031→			return "_proto_" + occamType
  1032→		}
  1033→		// Check if it's a record type name
  1034→		if _, ok := g.recordDefs[occamType]; ok {
  1035→			return occamType
  1036→		}
  1037→		return occamType // pass through unknown types
  1038→	}
  1039→}
  1040→
  1041→func (g *Generator) generateAssignment(assign *ast.Assignment) {
  1042→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1043→
  1044→	if assign.SliceTarget != nil {
  1045→		// Slice assignment: [arr FROM start FOR length] := value
  1046→		// Maps to: copy(arr[start : start + length], value)
  1047→		g.write("copy(")
  1048→		g.generateExpression(assign.SliceTarget.Array)
  1049→		g.write("[")
  1050→		g.generateExpression(assign.SliceTarget.Start)
  1051→		g.write(" : ")
  1052→		g.generateExpression(assign.SliceTarget.Start)
  1053→		g.write(" + ")
  1054→		g.generateExpression(assign.SliceTarget.Length)
  1055→		g.write("], ")
  1056→		g.generateExpression(assign.Value)
  1057→		g.write(")\n")
  1058→		return
  1059→	}
  1060→
  1061→	if assign.Index != nil {
  1062→		// Check if this is a record field access
  1063→		if _, ok := g.recordVars[assign.Name]; ok {
  1064→			if ident, ok := assign.Index.(*ast.Identifier); ok {
  1065→				// Record field: p.x = value (Go auto-dereferences pointers)
  1066→				g.write(assign.Name)
  1067→				g.write(".")
  1068→				g.write(ident.Value)
  1069→				g.write(" = ")
  1070→				g.generateExpression(assign.Value)
  1071→				g.write("\n")
  1072→				return
  1073→			}
  1074→		}
  1075→		// Array index: dereference if ref param
  1076→		if g.refParams[assign.Name] {
  1077→			g.write("*")
  1078→		}
  1079→		g.write(assign.Name)
  1080→		g.write("[")
  1081→		g.generateExpression(assign.Index)
  1082→		g.write("]")
  1083→	} else {
  1084→		// Simple assignment: dereference if ref param
  1085→		if g.refParams[assign.Name] {
  1086→			g.write("*")
  1087→		}
  1088→		g.write(assign.Name)
  1089→	}
  1090→	g.write(" = ")
  1091→	g.generateExpression(assign.Value)
  1092→	g.write("\n")
  1093→}
  1094→
  1095→func (g *Generator) generateSeqBlock(seq *ast.SeqBlock) {
  1096→	if seq.Replicator != nil {
  1097→		if seq.Replicator.Step != nil {
  1098→			// Replicated SEQ with STEP: counter-based loop
  1099→			v := seq.Replicator.Variable
  1100→			counter := "_repl_" + v
  1101→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1102→			g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
  1103→			g.generateExpression(seq.Replicator.Count)
  1104→			g.write(fmt.Sprintf("; %s++ {\n", counter))
  1105→			g.indent++
  1106→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1107→			g.write(fmt.Sprintf("%s := ", v))
  1108→			g.generateExpression(seq.Replicator.Start)
  1109→			g.write(fmt.Sprintf(" + %s * ", counter))
  1110→			g.generateExpression(seq.Replicator.Step)
  1111→			g.write("\n")
  1112→		} else {
  1113→			// Replicated SEQ: SEQ i = start FOR count becomes a for loop
  1114→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1115→			g.write(fmt.Sprintf("for %s := ", seq.Replicator.Variable))
  1116→			g.generateExpression(seq.Replicator.Start)
  1117→			g.write(fmt.Sprintf("; %s < ", seq.Replicator.Variable))
  1118→			g.generateExpression(seq.Replicator.Start)
  1119→			g.write(" + ")
  1120→			g.generateExpression(seq.Replicator.Count)
  1121→			g.write(fmt.Sprintf("; %s++ {\n", seq.Replicator.Variable))
  1122→			g.indent++
  1123→		}
  1124→		for _, stmt := range seq.Statements {
  1125→			g.generateStatement(stmt)
  1126→		}
  1127→		g.indent--
  1128→		g.writeLine("}")
  1129→	} else {
  1130→		// SEQ just becomes sequential Go code (Go's default)
  1131→		for _, stmt := range seq.Statements {
  1132→			g.generateStatement(stmt)
  1133→		}
  1134→	}
  1135→}
  1136→
  1137→func (g *Generator) generateParBlock(par *ast.ParBlock) {
  1138→	if par.Replicator != nil {
  1139→		// Replicated PAR: PAR i = start FOR count becomes goroutines in a loop
  1140→		g.writeLine("var wg sync.WaitGroup")
  1141→		g.builder.WriteString(strings.Repeat("\t", g.indent))
  1142→		g.write("wg.Add(int(")
  1143→		g.generateExpression(par.Replicator.Count)
  1144→		g.write("))\n")
  1145→
  1146→		v := par.Replicator.Variable
  1147→		if par.Replicator.Step != nil {
  1148→			counter := "_repl_" + v
  1149→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1150→			g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
  1151→			g.generateExpression(par.Replicator.Count)
  1152→			g.write(fmt.Sprintf("; %s++ {\n", counter))
  1153→			g.indent++
  1154→			// Compute loop variable from counter — also serves as closure capture
  1155→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1156→			g.write(fmt.Sprintf("%s := ", v))
  1157→			g.generateExpression(par.Replicator.Start)
  1158→			g.write(fmt.Sprintf(" + %s * ", counter))
  1159→			g.generateExpression(par.Replicator.Step)
  1160→			g.write("\n")
  1161→		} else {
  1162→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1163→			g.write(fmt.Sprintf("for %s := ", v))
  1164→			g.generateExpression(par.Replicator.Start)
  1165→			g.write(fmt.Sprintf("; %s < ", v))
  1166→			g.generateExpression(par.Replicator.Start)
  1167→			g.write(" + ")
  1168→			g.generateExpression(par.Replicator.Count)
  1169→			g.write(fmt.Sprintf("; %s++ {\n", v))
  1170→			g.indent++
  1171→			// Capture loop variable to avoid closure issues
  1172→			g.writeLine(fmt.Sprintf("%s := %s", v, v))
  1173→		}
  1174→		g.writeLine("go func() {")
  1175→		g.indent++
  1176→		g.writeLine("defer wg.Done()")
  1177→		for _, stmt := range par.Statements {
  1178→			g.generateStatement(stmt)
  1179→		}
  1180→		g.indent--
  1181→		g.writeLine("}()")
  1182→
  1183→		g.indent--
  1184→		g.writeLine("}")
  1185→		g.writeLine("wg.Wait()")
  1186→	} else {
  1187→		// PAR becomes goroutines with WaitGroup
  1188→		g.writeLine("var wg sync.WaitGroup")
  1189→		g.writeLine(fmt.Sprintf("wg.Add(%d)", len(par.Statements)))
  1190→
  1191→		for _, stmt := range par.Statements {
  1192→			g.writeLine("go func() {")
  1193→			g.indent++
  1194→			g.writeLine("defer wg.Done()")
  1195→			g.generateStatement(stmt)
  1196→			g.indent--
  1197→			g.writeLine("}()")
  1198→		}
  1199→
  1200→		g.writeLine("wg.Wait()")
  1201→	}
  1202→}
  1203→
  1204→func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
  1205→	// ALT becomes Go select statement
  1206→	// For guards, we use a pattern with nil channels
  1207→
  1208→	// Check if any cases have guards
  1209→	hasGuards := false
  1210→	for _, c := range alt.Cases {
  1211→		if c.Guard != nil {
  1212→			hasGuards = true
  1213→			break
  1214→		}
  1215→	}
  1216→
  1217→	if hasGuards {
  1218→		// Generate channel variables for guarded cases
  1219→		for i, c := range alt.Cases {
  1220→			if c.Guard != nil {
  1221→				g.builder.WriteString(strings.Repeat("\t", g.indent))
  1222→				g.write(fmt.Sprintf("var _alt%d chan ", i))
  1223→				// We don't know the channel type here, so use interface{}
  1224→				// Actually, we should use the same type as the original channel
  1225→				// For now, let's just reference the original channel conditionally
  1226→				g.write(fmt.Sprintf("int = nil\n")) // Assuming int for now
  1227→				g.builder.WriteString(strings.Repeat("\t", g.indent))
  1228→				g.write(fmt.Sprintf("if "))
  1229→				g.generateExpression(c.Guard)
  1230→				g.write(fmt.Sprintf(" { _alt%d = %s }\n", i, c.Channel))
  1231→			}
  1232→		}
  1233→	}
  1234→
  1235→	g.writeLine("select {")
  1236→	for i, c := range alt.Cases {
  1237→		g.builder.WriteString(strings.Repeat("\t", g.indent))
  1238→		if c.IsTimer {
  1239→			g.write("case <-time.After(time.Duration(")
  1240→			g.generateExpression(c.Deadline)
  1241→			g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
  1242→		} else if c.Guard != nil {
  1243→			g.write(fmt.Sprintf("case %s = <-_alt%d:\n", c.Variable, i))
  1244→		} else if c.ChannelIndex != nil {
  1245→			g.write(fmt.Sprintf("case %s = <-%s[", c.Variable, c.Channel))
  1246→			g.generateExpression(c.ChannelIndex)
  1247→			g.write("]:\n")
  1248→		} else {
  1249→			g.write(fmt.Sprintf("case %s = <-%s:\n", c.Variable, c.Channel))
  1250→		}
  1251→		g.indent++
  1252→		if c.Body != nil {
  1253→			g.generateStatement(c.Body)
  1254→		}
  1255→		g.indent--
  1256→	}
  1257→	g.writeLine("}")
  1258→}
  1259→
  1260→func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
  1261→	// Track reference parameters for this procedure
  1262→	oldRefParams := g.refParams
  1263→	newRefParams := make(map[string]bool)
  1264→	// Inherit parent's ref params for closure captures when nested
  1265→	if g.nestingLevel > 0 {
  1266→		for k, v := range oldRefParams {
  1267→			newRefParams[k] = v
  1268→		}
  1269→	}
  1270→	for _, p := range proc.Params {
  1271→		if !p.IsVal && !p.IsChan && !p.IsChanArray && !p.IsOpenArray {
  1272→			newRefParams[p.Name] = true
  1273→		} else {
  1274→			// Own param shadows any inherited ref param with same name
  1275→			delete(newRefParams, p.Name)
  1276→		}
  1277→		// Register chan params with protocol mappings
  1278→		if p.IsChan || p.IsChanArray {
  1279→			if _, ok := g.protocolDefs[p.ChanElemType]; ok {
  1280→				g.chanProtocols[p.Name] = p.ChanElemType
  1281→			}
  1282→		}
  1283→		// Register record-typed params
  1284→		if !p.IsChan {
  1285→			if _, ok := g.recordDefs[p.Type]; ok {
  1286→				g.recordVars[p.Name] = p.Type
  1287→			}
  1288→		}
  1289→	}
  1290→	g.refParams = newRefParams
  1291→
  1292→	// Generate function signature
  1293→	params := g.generateProcParams(proc.Params)
  1294→	if g.nestingLevel > 0 {
  1295→		// Nested PROC: generate as Go closure
  1296→		g.writeLine(fmt.Sprintf("%s := func(%s) {", proc.Name, params))
  1297→	} else {
  1298→		g.writeLine(fmt.Sprintf("func %s(%s) {", proc.Name, params))
  1299→	}
  1300→	g.indent++
  1301→	g.nestingLevel++
  1302→
  1303→	for _, stmt := range proc.Body {
  1304→		g.generateStatement(stmt)
  1305→	}
  1306→
  1307→	g.nestingLevel--
  1308→	g.indent--
  1309→	g.writeLine("}")
  1310→	g.writeLine("")
  1311→
  1312→	// Restore previous context
  1313→	g.refParams = oldRefParams
  1314→}
  1315→
  1316→func (g *Generator) generateProcParams(params []ast.ProcParam) string {
  1317→	var parts []string
  1318→	for _, p := range params {
  1319→		var goType string
  1320→		if p.IsChanArray {
  1321→			goType = "[]" + chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)
  1322→		} else if p.IsChan {
  1323→			goType = chanDirPrefix(p.ChanDir) + g.occamTypeToGo(p.ChanElemType)
  1324→		} else if p.IsOpenArray {
  1325→			goType = "[]" + g.occamTypeToGo(p.Type)
  1326→		} else {
  1327→			goType = g.occamTypeToGo(p.Type)
  1328→			if !p.IsVal {
  1329→				// Non-VAL parameters are pass by reference in Occam
  1330→				goType = "*" + goType
  1331→			}
  1332→		}
  1333→		parts = append(parts, fmt.Sprintf("%s %s", p.Name, goType))
  1334→	}
  1335→	return strings.Join(parts, ", ")
  1336→}
  1337→
  1338→func chanDirPrefix(dir string) string {
  1339→	switch dir {
  1340→	case "?":
  1341→		return "<-chan " // input/receive-only
  1342→	case "!":
  1343→		return "chan<- " // output/send-only
  1344→	default:
  1345→		return "chan " // bidirectional
  1346→	}
  1347→}
  1348→
  1349→func (g *Generator) generateProcCall(call *ast.ProcCall) {
  1350→	// Handle built-in print procedures
  1351→	if printBuiltins[call.Name] {
  1352→		g.generatePrintCall(call)
  1353→		return
  1354→	}
  1355→
  1356→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1357→	g.write(call.Name)
  1358→	g.write("(")
  1359→
  1360→	// Look up procedure signature to determine which args need address-of
  1361→	params := g.procSigs[call.Name]
  1362→
  1363→	for i, arg := range call.Args {
  1364→		if i > 0 {
  1365→			g.write(", ")
  1366→		}
  1367→		// If this parameter is not VAL (i.e., pass by reference), take address
  1368→		// Channels and channel arrays are already reference types, so no & needed
  1369→		if i < len(params) && !params[i].IsVal && !params[i].IsChan && !params[i].IsChanArray && !params[i].IsOpenArray {
  1370→			g.write("&")
  1371→		}
  1372→		g.generateExpression(arg)
  1373→	}
  1374→	g.write(")")
  1375→	g.write("\n")
  1376→}
  1377→
  1378→func (g *Generator) generateFuncDecl(fn *ast.FuncDecl) {
  1379→	params := g.generateProcParams(fn.Params)
  1380→
  1381→	// Build return type string
  1382→	var returnTypeStr string
  1383→	if len(fn.ReturnTypes) == 1 {
  1384→		returnTypeStr = g.occamTypeToGo(fn.ReturnTypes[0])
  1385→	} else {
  1386→		goTypes := make([]string, len(fn.ReturnTypes))
  1387→		for i, rt := range fn.ReturnTypes {
  1388→			goTypes[i] = g.occamTypeToGo(rt)
  1389→		}
  1390→		returnTypeStr = "(" + strings.Join(goTypes, ", ") + ")"
  1391→	}
  1392→
  1393→	if g.nestingLevel > 0 {
  1394→		// Nested FUNCTION: generate as Go closure
  1395→		g.writeLine(fmt.Sprintf("%s := func(%s) %s {", fn.Name, params, returnTypeStr))
  1396→	} else {
  1397→		g.writeLine(fmt.Sprintf("func %s(%s) %s {", fn.Name, params, returnTypeStr))
  1398→	}
  1399→	g.indent++
  1400→	g.nestingLevel++
  1401→
  1402→	for _, stmt := range fn.Body {
  1403→		g.generateStatement(stmt)
  1404→	}
  1405→
  1406→	if len(fn.ResultExprs) > 0 {
  1407→		g.builder.WriteString(strings.Repeat("\t", g.indent))
  1408→		g.write("return ")
  1409→		for i, expr := range fn.ResultExprs {
  1410→			if i > 0 {
  1411→				g.write(", ")
  1412→			}
  1413→			g.generateExpression(expr)
  1414→		}
  1415→		g.write("\n")
  1416→	}
  1417→
  1418→	g.nestingLevel--
  1419→	g.indent--
  1420→	g.writeLine("}")
  1421→	g.writeLine("")
  1422→}
  1423→
  1424→func (g *Generator) generateFuncCallExpr(call *ast.FuncCall) {
  1425→	g.write(call.Name)
  1426→	g.write("(")
  1427→	for i, arg := range call.Args {
  1428→		if i > 0 {
  1429→			g.write(", ")
  1430→		}
  1431→		g.generateExpression(arg)
  1432→	}
  1433→	g.write(")")
  1434→}
  1435→
  1436→func (g *Generator) generateMultiAssignment(stmt *ast.MultiAssignment) {
  1437→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1438→	for i, target := range stmt.Targets {
  1439→		if i > 0 {
  1440→			g.write(", ")
  1441→		}
  1442→		if target.Index != nil {
  1443→			// Check if this is a record field access
  1444→			if _, ok := g.recordVars[target.Name]; ok {
  1445→				if ident, ok := target.Index.(*ast.Identifier); ok {
  1446→					g.write(target.Name)
  1447→					g.write(".")
  1448→					g.write(ident.Value)
  1449→					continue
  1450→				}
  1451→			}
  1452→			if g.refParams[target.Name] {
  1453→				g.write("(*")
  1454→				g.write(target.Name)
  1455→				g.write(")")
  1456→			} else {
  1457→				g.write(target.Name)
  1458→			}
  1459→			g.write("[")
  1460→			g.generateExpression(target.Index)
  1461→			g.write("]")
  1462→		} else {
  1463→			if g.refParams[target.Name] {
  1464→				g.write("*")
  1465→			}
  1466→			g.write(target.Name)
  1467→		}
  1468→	}
  1469→	g.write(" = ")
  1470→	for i, val := range stmt.Values {
  1471→		if i > 0 {
  1472→			g.write(", ")
  1473→		}
  1474→		g.generateExpression(val)
  1475→	}
  1476→	g.write("\n")
  1477→}
  1478→
  1479→func (g *Generator) generatePrintCall(call *ast.ProcCall) {
  1480→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1481→
  1482→	switch call.Name {
  1483→	case "print.int", "print.string", "print.bool":
  1484→		g.write("fmt.Println(")
  1485→		if len(call.Args) > 0 {
  1486→			g.generateExpression(call.Args[0])
  1487→		}
  1488→		g.write(")")
  1489→	case "print.newline":
  1490→		g.write("fmt.Println()")
  1491→	}
  1492→
  1493→	g.write("\n")
  1494→}
  1495→
  1496→func (g *Generator) generateWhileLoop(loop *ast.WhileLoop) {
  1497→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1498→	g.write("for ")
  1499→	g.generateExpression(loop.Condition)
  1500→	g.write(" {\n")
  1501→	g.indent++
  1502→
  1503→	if loop.Body != nil {
  1504→		g.generateStatement(loop.Body)
  1505→	}
  1506→
  1507→	g.indent--
  1508→	g.writeLine("}")
  1509→}
  1510→
  1511→func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
  1512→	if stmt.Replicator != nil {
  1513→		// Replicated IF: IF i = start FOR count → for loop with break on first match
  1514→		v := stmt.Replicator.Variable
  1515→		if stmt.Replicator.Step != nil {
  1516→			counter := "_repl_" + v
  1517→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1518→			g.write(fmt.Sprintf("for %s := 0; %s < ", counter, counter))
  1519→			g.generateExpression(stmt.Replicator.Count)
  1520→			g.write(fmt.Sprintf("; %s++ {\n", counter))
  1521→			g.indent++
  1522→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1523→			g.write(fmt.Sprintf("%s := ", v))
  1524→			g.generateExpression(stmt.Replicator.Start)
  1525→			g.write(fmt.Sprintf(" + %s * ", counter))
  1526→			g.generateExpression(stmt.Replicator.Step)
  1527→			g.write("\n")
  1528→		} else {
  1529→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1530→			g.write(fmt.Sprintf("for %s := ", v))
  1531→			g.generateExpression(stmt.Replicator.Start)
  1532→			g.write(fmt.Sprintf("; %s < ", v))
  1533→			g.generateExpression(stmt.Replicator.Start)
  1534→			g.write(" + ")
  1535→			g.generateExpression(stmt.Replicator.Count)
  1536→			g.write(fmt.Sprintf("; %s++ {\n", v))
  1537→			g.indent++
  1538→		}
  1539→
  1540→		for i, choice := range stmt.Choices {
  1541→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1542→			if i == 0 {
  1543→				g.write("if ")
  1544→			} else {
  1545→				g.write("} else if ")
  1546→			}
  1547→			g.generateExpression(choice.Condition)
  1548→			g.write(" {\n")
  1549→			g.indent++
  1550→
  1551→			if choice.Body != nil {
  1552→				g.generateStatement(choice.Body)
  1553→			}
  1554→			g.writeLine("break")
  1555→
  1556→			g.indent--
  1557→		}
  1558→		g.writeLine("}")
  1559→
  1560→		g.indent--
  1561→		g.writeLine("}")
  1562→	} else {
  1563→		for i, choice := range stmt.Choices {
  1564→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1565→			if i == 0 {
  1566→				g.write("if ")
  1567→			} else {
  1568→				g.write("} else if ")
  1569→			}
  1570→			g.generateExpression(choice.Condition)
  1571→			g.write(" {\n")
  1572→			g.indent++
  1573→
  1574→			if choice.Body != nil {
  1575→				g.generateStatement(choice.Body)
  1576→			}
  1577→
  1578→			g.indent--
  1579→		}
  1580→		g.writeLine("}")
  1581→	}
  1582→}
  1583→
  1584→func (g *Generator) generateCaseStatement(stmt *ast.CaseStatement) {
  1585→	g.builder.WriteString(strings.Repeat("\t", g.indent))
  1586→	g.write("switch ")
  1587→	g.generateExpression(stmt.Selector)
  1588→	g.write(" {\n")
  1589→
  1590→	for _, choice := range stmt.Choices {
  1591→		if choice.IsElse {
  1592→			g.writeLine("default:")
  1593→		} else {
  1594→			g.builder.WriteString(strings.Repeat("\t", g.indent))
  1595→			g.write("case ")
  1596→			for i, val := range choice.Values {
  1597→				if i > 0 {
  1598→					g.write(", ")
  1599→				}
  1600→				g.generateExpression(val)
  1601→			}
  1602→			g.write(":\n")
  1603→		}
  1604→		g.indent++
  1605→		if choice.Body != nil {
  1606→			g.generateStatement(choice.Body)
  1607→		}
  1608→		g.indent--
  1609→	}
  1610→
  1611→	g.writeLine("}")
  1612→}
  1613→
  1614→func (g *Generator) generateExpression(expr ast.Expression) {
  1615→	switch e := expr.(type) {
  1616→	case *ast.Identifier:
  1617→		g.write(e.Value)
  1618→	case *ast.IntegerLiteral:
  1619→		g.write(fmt.Sprintf("%d", e.Value))
  1620→	case *ast.StringLiteral:
  1621→		g.write(fmt.Sprintf("%q", e.Value))
  1622→	case *ast.ByteLiteral:
  1623→		g.write(fmt.Sprintf("byte(%d)", e.Value))
  1624→	case *ast.BooleanLiteral:
  1625→		if e.Value {
  1626→			g.write("true")
  1627→		} else {
  1628→			g.write("false")
  1629→		}
  1630→	case *ast.BinaryExpr:
  1631→		g.generateBinaryExpr(e)
  1632→	case *ast.UnaryExpr:
  1633→		g.generateUnaryExpr(e)
  1634→	case *ast.SizeExpr:
  1635→		g.write("len(")
  1636→		g.generateExpression(e.Expr)
  1637→		g.write(")")
  1638→	case *ast.ParenExpr:
  1639→		g.write("(")
  1640→		g.generateExpression(e.Expr)
  1641→		g.write(")")
  1642→	case *ast.IndexExpr:
  1643→		// Check if this is a record field access
  1644→		if ident, ok := e.Left.(*ast.Identifier); ok {
  1645→			if _, ok := g.recordVars[ident.Value]; ok {
  1646→				if field, ok := e.Index.(*ast.Identifier); ok {
  1647→					g.generateExpression(e.Left)
  1648→					g.write(".")
  1649→					g.write(field.Value)
  1650→					break
  1651→				}
  1652→			}
  1653→		}
  1654→		g.generateExpression(e.Left)
  1655→		g.write("[")
  1656→		g.generateExpression(e.Index)
  1657→		g.write("]")
  1658→	case *ast.SliceExpr:
  1659→		g.generateExpression(e.Array)
  1660→		g.write("[")
  1661→		g.generateExpression(e.Start)
  1662→		g.write(" : ")
  1663→		g.generateExpression(e.Start)
  1664→		g.write(" + ")
  1665→		g.generateExpression(e.Length)
  1666→		g.write("]")
  1667→	case *ast.FuncCall:
  1668→		g.generateFuncCallExpr(e)
  1669→	case *ast.TypeConversion:
  1670→		g.write(g.occamTypeToGo(e.TargetType))
  1671→		g.write("(")
  1672→		g.generateExpression(e.Expr)
  1673→		g.write(")")
  1674→	case *ast.MostExpr:
  1675→		g.generateMostExpr(e)
  1676→	}
  1677→}
  1678→
  1679→func (g *Generator) generateBinaryExpr(expr *ast.BinaryExpr) {
  1680→	g.write("(")
  1681→	g.generateExpression(expr.Left)
  1682→	g.write(" ")
  1683→	g.write(g.occamOpToGo(expr.Operator))
  1684→	g.write(" ")
  1685→	g.generateExpression(expr.Right)
  1686→	g.write(")")
  1687→}
  1688→
  1689→func (g *Generator) generateUnaryExpr(expr *ast.UnaryExpr) {
  1690→	op := g.occamOpToGo(expr.Operator)
  1691→	g.write(op)
  1692→	if op == "!" || op == "^" {
  1693→		// Go's logical NOT and bitwise NOT don't need space
  1694→	} else {
  1695→		g.write(" ")
  1696→	}
  1697→	g.generateExpression(expr.Right)
  1698→}
  1699→
  1700→func (g *Generator) occamOpToGo(op string) string {
  1701→	switch op {
  1702→	case "=":
  1703→		return "=="
  1704→	case "<>":
  1705→		return "!="
  1706→	case "AND":
  1707→		return "&&"
  1708→	case "OR":
  1709→		return "||"
  1710→	case "NOT":
  1711→		return "!"
  1712→	case "\\":
  1713→		return "%"
  1714→	case "AFTER":
  1715→		return ">"
  1716→	case "/\\":
  1717→		return "&"
  1718→	case "\\/":
  1719→		return "|"
  1720→	case "><":
  1721→		return "^"
  1722→	case "~":
  1723→		return "^"
  1724→	case "<<":
  1725→		return "<<"
  1726→	case ">>":
  1727→		return ">>"
  1728→	case "PLUS":
  1729→		return "+"
  1730→	case "MINUS":
  1731→		return "-"
  1732→	case "TIMES":
  1733→		return "*"
  1734→	default:
  1735→		return op // +, -, *, /, <, >, <=, >= are the same
  1736→	}
  1737→}
  1738→
Thinking

Now I have a clear picture of all the files. Let me create task items and implement the changes step by step.

Let me start implementing:

  1. AST changes: Change Body from Statement to []Statement in WhileLoop, IfChoice, CaseChoice, AltCase
  2. Parser changes: Add parseBodyStatements() and update call sites, handle channel direction at call sites
  3. Codegen changes: Update to iterate over body slices
  4. Test updates

Let me also check the test files to understand what existing tests look like.

Now let me create the task list and start implementing. Let me also check the test files.

TaskCreate
Change WhileLoop.Body, IfChoice.Body, CaseChoice.Body, AltCase.Body from Statement to []Statement
{
  "subject": "Change Body fields to []Statement in AST",
  "activeForm": "Updating AST types"
}
Task #1 created successfully: Change Body fields to []Statement in AST
TaskCreate
Add parseBodyStatements() helper, update parseWhileLoop, parseIfStatement, parseCaseStatement, parseAltCase. Also handle channel direction at call sites in parseProcCall.
{
  "subject": "Add parseBodyStatements() and update parser call sites",
  "activeForm": "Updating parser"
}
Task #2 created successfully: Add parseBodyStatements() and update parser call sites
TaskCreate
Update generators and scanner functions to iterate over []Statement body slices
{
  "subject": "Update codegen for body slices",
  "activeForm": "Updating codegen"
}
Task #3 created successfully: Update codegen for body slices
TaskCreate
Update existing tests that access .Body, add parser test for multi-statement IF body, channel direction at call site, and e2e tests
{
  "subject": "Update tests and add new tests",
  "activeForm": "Updating tests"
}
Task #4 created successfully: Update tests and add new tests
TaskCreate
Run go test ./... to verify everything passes
{
  "subject": "Run tests and verify",
  "activeForm": "Running tests"
}
Task #5 created successfully: Run tests and verify
TaskUpdate
{
  "taskId": "1",
  "status": "in_progress"
}
Updated task #1 status

Step 1: AST changes

✏️ Edit ast.go
/home/david/projects/code-associates/occam2go/ast/ast.go
// WhileLoop represents a WHILE loop
type WhileLoop struct {
	Token     lexer.Token // the WHILE token
	Condition Expression
	Body      Statement
}
+
// WhileLoop represents a WHILE loop
type WhileLoop struct {
	Token     lexer.Token // the WHILE token
	Condition Expression
	Body      []Statement
}
The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.
✏️ Edit ast.go
/home/david/projects/code-associates/occam2go/ast/ast.go
type IfChoice struct {
	Condition Expression
	Body      Statement
}
+
type IfChoice struct {
	Condition Expression
	Body      []Statement
}
The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.
✏️ Edit ast.go
/home/david/projects/code-associates/occam2go/ast/ast.go
type CaseChoice struct {
	Values []Expression // nil/empty for ELSE
	IsElse bool
	Body   Statement
}
+
type CaseChoice struct {
	Values []Expression // nil/empty for ELSE
	IsElse bool
	Body   []Statement
}
The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.
✏️ Edit ast.go
/home/david/projects/code-associates/occam2go/ast/ast.go
// 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
	ChannelIndex Expression // non-nil for cs[i] ? x in ALT
	Variable     string     // variable to receive into
	Body         Statement  // the body to execute
	IsTimer      bool       // true if this is a timer AFTER case
	Timer        string     // timer name (when IsTimer)
	Deadline     Expression // AFTER deadline expression (when IsTimer)
}
+
// 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
	ChannelIndex Expression  // non-nil for cs[i] ? x in ALT
	Variable     string      // variable to receive into
	Body         []Statement // the body to execute
	IsTimer      bool        // true if this is a timer AFTER case
	Timer        string      // timer name (when IsTimer)
	Deadline     Expression  // AFTER deadline expression (when IsTimer)
}
The file /home/david/projects/code-associates/occam2go/ast/ast.go has been updated successfully.
TaskUpdate
{
  "taskId": "1",
  "status": "completed"
}
Updated task #1 status
TaskUpdate
{
  "taskId": "2",
  "status": "in_progress"
}
Updated task #2 status

Step 2: Parser changes

First, add the parseBodyStatements() helper after parseBlockStatements():

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
func (p *Parser) parseProcDecl() *ast.ProcDecl {
+
// parseBodyStatements parses multiple statements inside a branch body
// (IF choice, CASE choice, ALT case, WHILE). Called after the caller has
// consumed the INDENT token and advanced into the body.
// Returns all statements found at this indentation level.
func (p *Parser) parseBodyStatements() []ast.Statement {
	var statements []ast.Statement
	startLevel := p.indentLevel

	for !p.curTokenIs(lexer.EOF) {
		// Skip newlines
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		// Handle DEDENT tokens
		for p.curTokenIs(lexer.DEDENT) {
			if p.indentLevel < startLevel {
				return statements
			}
			p.nextToken()
		}

		// Skip any more newlines after DEDENT
		for p.curTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.curTokenIs(lexer.EOF) {
			break
		}

		if p.indentLevel < startLevel {
			break
		}

		// Safety guard: record position before parsing to detect no-progress
		prevToken := p.curToken
		prevPeek := p.peekToken

		stmt := p.parseStatement()
		if stmt != nil {
			statements = append(statements, stmt)
		}

		// 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()
		}

		// No-progress guard: if we haven't moved, break to prevent infinite loop
		if p.curToken == prevToken && p.peekToken == prevPeek {
			break
		}
	}

	return statements
}

func (p *Parser) parseProcDecl() *ast.ProcDecl {
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now update parseWhileLoop():

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
func (p *Parser) parseWhileLoop() *ast.WhileLoop {
	loop := &ast.WhileLoop{Token: p.curToken}

	p.nextToken()
	loop.Condition = p.parseExpression(LOWEST)

	// Skip to next line
	for p.peekTokenIs(lexer.NEWLINE) {
		p.nextToken()
	}

	// Expect INDENT
	if !p.peekTokenIs(lexer.INDENT) {
		p.addError("expected indented block after WHILE condition")
		return loop
	}
	p.nextToken() // consume INDENT
	p.nextToken() // move to first statement

	loop.Body = p.parseStatement()

	// Consume until DEDENT
	for !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
		p.nextToken()
	}

	return loop
}
+
func (p *Parser) parseWhileLoop() *ast.WhileLoop {
	loop := &ast.WhileLoop{Token: p.curToken}

	p.nextToken()
	loop.Condition = p.parseExpression(LOWEST)

	// Skip to next line
	for p.peekTokenIs(lexer.NEWLINE) {
		p.nextToken()
	}

	// Expect INDENT
	if !p.peekTokenIs(lexer.INDENT) {
		p.addError("expected indented block after WHILE condition")
		return loop
	}
	p.nextToken() // consume INDENT
	p.nextToken() // move to first statement

	loop.Body = p.parseBodyStatements()

	return loop
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now update parseIfStatement() — change the body parsing inside the choices loop:

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
		choice := ast.IfChoice{}
		choice.Condition = p.parseExpression(LOWEST)

		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			choice.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.Choices = append(stmt.Choices, choice)
	}

	return stmt
}
+
		choice := ast.IfChoice{}
		choice.Condition = p.parseExpression(LOWEST)

		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			choice.Body = p.parseBodyStatements()
		}

		stmt.Choices = append(stmt.Choices, choice)
	}

	return stmt
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now update parseCaseStatement():

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			choice.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.Choices = append(stmt.Choices, choice)
	}

	return stmt
}

// Expression parsing using Pratt parsing
+
		// Skip newlines and expect INDENT for body
		for p.peekTokenIs(lexer.NEWLINE) {
			p.nextToken()
		}

		if p.peekTokenIs(lexer.INDENT) {
			p.nextToken() // consume INDENT
			p.nextToken() // move to body
			choice.Body = p.parseBodyStatements()
		}

		stmt.Choices = append(stmt.Choices, choice)
	}

	return stmt
}

// Expression parsing using Pratt parsing
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now update parseAltCase():

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	// Expect INDENT for body
	if !p.peekTokenIs(lexer.INDENT) {
		p.addError("expected indented body after ALT case")
		return altCase
	}
	p.nextToken() // consume INDENT
	p.nextToken() // move into body

	// Parse the body (first statement)
	altCase.Body = p.parseStatement()

	// Skip to end of body block
	for !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
		p.nextToken()
	}

	return altCase
}
+
	// Expect INDENT for body
	if !p.peekTokenIs(lexer.INDENT) {
		p.addError("expected indented body after ALT case")
		return altCase
	}
	p.nextToken() // consume INDENT
	p.nextToken() // move into body

	altCase.Body = p.parseBodyStatements()

	return altCase
}
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.

Now handle channel direction annotations at call sites in parseProcCall():

✏️ Edit parser.go
/home/david/projects/code-associates/occam2go/parser/parser.go
	p.nextToken() // move to first arg
	call.Args = append(call.Args, p.parseExpression(LOWEST))

	for p.peekTokenIs(lexer.COMMA) {
		p.nextToken() // consume comma
		p.nextToken() // move to next arg
		call.Args = append(call.Args, p.parseExpression(LOWEST))
	}

	if !p.expectPeek(lexer.RPAREN) {
		return nil
	}

	return call
}

func (p *Parser) parseFuncDecl() *ast.FuncDecl {
+
	p.nextToken() // move to first arg
	call.Args = append(call.Args, p.parseExpression(LOWEST))
	// Consume optional channel direction annotation at call site (e.g., out!)
	if p.peekTokenIs(lexer.SEND) || p.peekTokenIs(lexer.RECEIVE) {
		p.nextToken()
	}

	for p.peekTokenIs(lexer.COMMA) {
		p.nextToken() // consume comma
		p.nextToken() // move to next arg
		call.Args = append(call.Args, p.parseExpression(LOWEST))
		// Consume optional channel direction annotation at call site
		if p.peekTokenIs(lexer.SEND) || p.peekTokenIs(lexer.RECEIVE) {
			p.nextToken()
		}
	}

	if !p.expectPeek(lexer.RPAREN) {
		return nil
	}

	return call
}

func (p *Parser) parseFuncDecl() *ast.FuncDecl {
The file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
TaskUpdate
{
  "taskId": "2",
  "status": "completed"
}
Updated task #2 status
TaskUpdate
{
  "taskId": "3",
  "status": "in_progress"
}
Updated task #3 status

Step 3: Codegen changes

Now I need to update all the places in codegen that access .Body as a single statement to iterate over the slice. Let me create a helper function pattern and update each location.

First, the generators — generateWhileLoop, generateIfStatement, generateCaseStatement, generateAltBlock:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	if loop.Body != nil {
		g.generateStatement(loop.Body)
	}

	g.indent--
	g.writeLine("}")
}
+
	for _, s := range loop.Body {
		g.generateStatement(s)
	}

	g.indent--
	g.writeLine("}")
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update generateIfStatement() — there are two places (replicated and non-replicated):

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
		for i, choice := range stmt.Choices {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++

			if choice.Body != nil {
				g.generateStatement(choice.Body)
			}
			g.writeLine("break")

			g.indent--
		}
		g.writeLine("}")

		g.indent--
		g.writeLine("}")
	} else {
		for i, choice := range stmt.Choices {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++

			if choice.Body != nil {
				g.generateStatement(choice.Body)
			}

			g.indent--
		}
		g.writeLine("}")
	}
}
+
		for i, choice := range stmt.Choices {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++

			for _, s := range choice.Body {
				g.generateStatement(s)
			}
			g.writeLine("break")

			g.indent--
		}
		g.writeLine("}")

		g.indent--
		g.writeLine("}")
	} else {
		for i, choice := range stmt.Choices {
			g.builder.WriteString(strings.Repeat("\t", g.indent))
			if i == 0 {
				g.write("if ")
			} else {
				g.write("} else if ")
			}
			g.generateExpression(choice.Condition)
			g.write(" {\n")
			g.indent++

			for _, s := range choice.Body {
				g.generateStatement(s)
			}

			g.indent--
		}
		g.writeLine("}")
	}
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update generateCaseStatement():

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
		g.indent++
		if choice.Body != nil {
			g.generateStatement(choice.Body)
		}
		g.indent--
	}

	g.writeLine("}")
}
+
		g.indent++
		for _, s := range choice.Body {
			g.generateStatement(s)
		}
		g.indent--
	}

	g.writeLine("}")
}
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update generateAltBlock():

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
		g.indent++
		if c.Body != nil {
			g.generateStatement(c.Body)
		}
		g.indent--
	}
	g.writeLine("}")
}

func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
+
		g.indent++
		for _, s := range c.Body {
			g.generateStatement(s)
		}
		g.indent--
	}
	g.writeLine("}")
}

func (g *Generator) generateProcDecl(proc *ast.ProcDecl) {
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update all the scanner functions. Let me update containsPar:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsPar(c.Body) {
				return true
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsPar(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsPar(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		if s.Body != nil && g.containsPar(s.Body) {
			return true
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsPar(choice.Body) {
				return true
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsPar(choice.Body) {
				return true
			}
		}
	case *ast.VariantReceive:
+
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				if g.containsPar(inner) {
					return true
				}
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsPar(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsPar(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		for _, inner := range s.Body {
			if g.containsPar(inner) {
				return true
			}
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPar(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPar(inner) {
					return true
				}
			}
		}
	case *ast.VariantReceive:
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update containsPrint:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsPrint(c.Body) {
				return true
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsPrint(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsPrint(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		if s.Body != nil && g.containsPrint(s.Body) {
			return true
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsPrint(choice.Body) {
				return true
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsPrint(choice.Body) {
				return true
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsPrint(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) containsTimer
+
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				if g.containsPrint(inner) {
					return true
				}
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsPrint(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsPrint(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		for _, inner := range s.Body {
			if g.containsPrint(inner) {
				return true
			}
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPrint(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsPrint(inner) {
					return true
				}
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsPrint(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) containsTimer
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update containsTimer:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.IsTimer {
				return true
			}
			if c.Body != nil && g.containsTimer(c.Body) {
				return true
			}
		}
	case *ast.SeqBlock:
		for _, inner := range s.Statements {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.ParBlock:
		for _, inner := range s.Statements {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		if s.Body != nil && g.containsTimer(s.Body) {
			return true
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsTimer(choice.Body) {
				return true
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsTimer(choice.Body) {
				return true
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsTimer(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) containsStop
+
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.IsTimer {
				return true
			}
			for _, inner := range c.Body {
				if g.containsTimer(inner) {
					return true
				}
			}
		}
	case *ast.SeqBlock:
		for _, inner := range s.Statements {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.ParBlock:
		for _, inner := range s.Statements {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		for _, inner := range s.Body {
			if g.containsTimer(inner) {
				return true
			}
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsTimer(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsTimer(inner) {
					return true
				}
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsTimer(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) containsStop
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update containsStop:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsStop(c.Body) {
				return true
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsStop(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsStop(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		if s.Body != nil && g.containsStop(s.Body) {
			return true
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsStop(choice.Body) {
				return true
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil && g.containsStop(choice.Body) {
				return true
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsStop(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) containsMostExpr
+
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				if g.containsStop(inner) {
					return true
				}
			}
		}
	case *ast.ProcDecl:
		for _, inner := range s.Body {
			if g.containsStop(inner) {
				return true
			}
		}
	case *ast.FuncDecl:
		for _, inner := range s.Body {
			if g.containsStop(inner) {
				return true
			}
		}
	case *ast.WhileLoop:
		for _, inner := range s.Body {
			if g.containsStop(inner) {
				return true
			}
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsStop(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				if g.containsStop(inner) {
					return true
				}
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsStop(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) containsMostExpr
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update containsMostExpr:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.WhileLoop:
		if g.exprNeedsMath(s.Condition) {
			return true
		}
		if s.Body != nil && g.containsMostExpr(s.Body) {
			return true
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if g.exprNeedsMath(choice.Condition) {
				return true
			}
			if choice.Body != nil && g.containsMostExpr(choice.Body) {
				return true
			}
		}
	case *ast.CaseStatement:
		if g.exprNeedsMath(s.Selector) {
			return true
		}
		for _, choice := range s.Choices {
			for _, v := range choice.Values {
				if g.exprNeedsMath(v) {
					return true
				}
			}
			if choice.Body != nil && g.containsMostExpr(choice.Body) {
				return true
			}
		}
	case *ast.Send:
+
	case *ast.WhileLoop:
		if g.exprNeedsMath(s.Condition) {
			return true
		}
		for _, inner := range s.Body {
			if g.containsMostExpr(inner) {
				return true
			}
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if g.exprNeedsMath(choice.Condition) {
				return true
			}
			for _, inner := range choice.Body {
				if g.containsMostExpr(inner) {
					return true
				}
			}
		}
	case *ast.CaseStatement:
		if g.exprNeedsMath(s.Selector) {
			return true
		}
		for _, choice := range s.Choices {
			for _, v := range choice.Values {
				if g.exprNeedsMath(v) {
					return true
				}
			}
			for _, inner := range choice.Body {
				if g.containsMostExpr(inner) {
					return true
				}
			}
		}
	case *ast.Send:
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update containsMostExpr for AltBlock:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsMostExpr(c.Body) {
				return true
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsMostExpr(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) exprNeedsMath
+
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				if g.containsMostExpr(inner) {
					return true
				}
			}
		}
	case *ast.VariantReceive:
		for _, c := range s.Cases {
			if c.Body != nil && g.containsMostExpr(c.Body) {
				return true
			}
		}
	}
	return false
}

func (g *Generator) exprNeedsMath
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update collectChanProtocols:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.WhileLoop:
		if s.Body != nil {
			g.collectChanProtocols(s.Body)
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil {
				g.collectChanProtocols(choice.Body)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil {
				g.collectChanProtocols(choice.Body)
			}
		}
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.Body != nil {
				g.collectChanProtocols(c.Body)
			}
		}
	}
}

func (g *Generator) collectRecordVars
+
	case *ast.WhileLoop:
		for _, inner := range s.Body {
			g.collectChanProtocols(inner)
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectChanProtocols(inner)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectChanProtocols(inner)
			}
		}
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				g.collectChanProtocols(inner)
			}
		}
	}
}

func (g *Generator) collectRecordVars
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Now update collectRecordVars:

✏️ Edit codegen.go
/home/david/projects/code-associates/occam2go/codegen/codegen.go
	case *ast.WhileLoop:
		if s.Body != nil {
			g.collectRecordVars(s.Body)
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil {
				g.collectRecordVars(choice.Body)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			if choice.Body != nil {
				g.collectRecordVars(choice.Body)
			}
		}
	case *ast.AltBlock:
		for _, c := range s.Cases {
			if c.Body != nil {
				g.collectRecordVars(c.Body)
			}
		}
	}
}

func (g *Generator) generateRecordDecl
+
	case *ast.WhileLoop:
		for _, inner := range s.Body {
			g.collectRecordVars(inner)
		}
	case *ast.IfStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectRecordVars(inner)
			}
		}
	case *ast.CaseStatement:
		for _, choice := range s.Choices {
			for _, inner := range choice.Body {
				g.collectRecordVars(inner)
			}
		}
	case *ast.AltBlock:
		for _, c := range s.Cases {
			for _, inner := range c.Body {
				g.collectRecordVars(inner)
			}
		}
	}
}

func (g *Generator) generateRecordDecl
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.

Let me try to build now to see if there are any remaining compile errors:

$ Bash
Check for compile errors
go build ./... 2>&1

Clean compile. Now let me update the tests.

TaskUpdate
{
  "taskId": "3",
  "status": "completed"
}
Updated task #3 status
TaskUpdate
{
  "taskId": "4",
  "status": "in_progress"
}
Updated task #4 status

Step 4: Update tests

Let me find all test references to .Body in the test files:

Grep
{
  "pattern": "\\.Body",
  "glob": "*_test.go",
  "output_mode": "content"
}
parser/parser_test.go:374:	if loop.Body == nil {
parser/parser_test.go:408:	if ifStmt.Choices[0].Body == nil {
parser/parser_test.go:416:	if ifStmt.Choices[1].Body == nil {
parser/parser_test.go:580:	if ifStmt.Choices[0].Body == nil {
parser/parser_test.go:780:	if len(fn.Body) != 0 {
parser/parser_test.go:781:		t.Errorf("expected empty body for IS form, got %d statements", len(fn.Body))
parser/parser_test.go:827:	if len(fn.Body) < 1 {
parser/parser_test.go:871:	if len(fn.Body) < 1 {
parser/parser_test.go:1020:	if caseStmt.Choices[0].Body == nil {
parser/parser_test.go:1028:	if caseStmt.Choices[1].Body == nil {
parser/parser_test.go:1036:	if caseStmt.Choices[2].Body == nil {
parser/parser_test.go:2608:	if len(proc.Body) != 3 {
parser/parser_test.go:2609:		t.Fatalf("expected 3 body statements, got %d", len(proc.Body))
parser/parser_test.go:2613:	if _, ok := proc.Body[0].(*ast.VarDecl); !ok {
parser/parser_test.go:2614:		t.Errorf("expected VarDecl as first body statement, got %T", proc.Body[0])
parser/parser_test.go:2618:	nestedProc, ok := proc.Body[1].(*ast.ProcDecl)
parser/parser_test.go:2620:		t.Fatalf("expected nested ProcDecl, got %T", proc.Body[1])
parser/parser_test.go:2627:	if _, ok := proc.Body[2].(*ast.SeqBlock); !ok {
parser/parser_test.go:2628:		t.Errorf("expected SeqBlock as third body statement, got %T", proc.Body[2])
parser/parser_test.go:2654:	if len(proc.Body) != 2 {
parser/parser_test.go:2655:		t.Fatalf("expected 2 body statements, got %d", len(proc.Body))
parser/parser_test.go:2658:	fn, ok := proc.Body[0].(*ast.FuncDecl)
parser/parser_test.go:2660:		t.Fatalf("expected nested FuncDecl, got %T", proc.Body[0])
parser/parser_test.go:2666:	if _, ok := proc.Body[1].(*ast.SeqBlock); !ok {
parser/parser_test.go:2667:		t.Errorf("expected SeqBlock as second body statement, got %T", proc.Body[1])
parser/parser_test.go:2694:	if len(proc.Body) != 3 {
parser/parser_test.go:2695:		t.Fatalf("expected 3 body statements, got %d", len(proc.Body))
parser/parser_test.go:2699:		if _, ok := proc.Body[i].(*ast.VarDecl); !ok {
parser/parser_test.go:2700:			t.Errorf("expected VarDecl at index %d, got %T", i, proc.Body[i])
parser/parser_test.go:2704:	if _, ok := proc.Body[2].(*ast.SeqBlock); !ok {
parser/parser_test.go:2705:		t.Errorf("expected SeqBlock at index 2, got %T", proc.Body[2])
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "offset": 370,
  "limit": 60
}
   370→	if loop.Condition == nil {
   371→		t.Error("expected condition")
   372→	}
   373→
   374→	if loop.Body == nil {
   375→		t.Error("expected body")
   376→	}
   377→}
   378→
   379→func TestIfStatement(t *testing.T) {
   380→	input := `IF
   381→  x > 0
   382→    y := 1
   383→  x = 0
   384→    y := 0
   385→`
   386→	l := lexer.New(input)
   387→	p := New(l)
   388→	program := p.ParseProgram()
   389→	checkParserErrors(t, p)
   390→
   391→	if len(program.Statements) != 1 {
   392→		t.Fatalf("expected 1 statement, got %d", len(program.Statements))
   393→	}
   394→
   395→	ifStmt, ok := program.Statements[0].(*ast.IfStatement)
   396→	if !ok {
   397→		t.Fatalf("expected IfStatement, got %T", program.Statements[0])
   398→	}
   399→
   400→	if len(ifStmt.Choices) != 2 {
   401→		t.Fatalf("expected 2 choices, got %d", len(ifStmt.Choices))
   402→	}
   403→
   404→	if ifStmt.Choices[0].Condition == nil {
   405→		t.Error("expected condition on first choice")
   406→	}
   407→
   408→	if ifStmt.Choices[0].Body == nil {
   409→		t.Error("expected body on first choice")
   410→	}
   411→
   412→	if ifStmt.Choices[1].Condition == nil {
   413→		t.Error("expected condition on second choice")
   414→	}
   415→
   416→	if ifStmt.Choices[1].Body == nil {
   417→		t.Error("expected body on second choice")
   418→	}
   419→}
   420→
   421→func TestReplicatedSeq(t *testing.T) {
   422→	input := `SEQ i = 0 FOR 5
   423→  print.int(i)
   424→`
   425→	l := lexer.New(input)
   426→	p := New(l)
   427→	program := p.ParseProgram()
   428→	checkParserErrors(t, p)
   429→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "offset": 570,
  "limit": 25
}
   570→	}
   571→
   572→	if len(ifStmt.Choices) != 1 {
   573→		t.Fatalf("expected 1 choice, got %d", len(ifStmt.Choices))
   574→	}
   575→
   576→	if ifStmt.Choices[0].Condition == nil {
   577→		t.Error("expected condition on choice")
   578→	}
   579→
   580→	if ifStmt.Choices[0].Body == nil {
   581→		t.Error("expected body on choice")
   582→	}
   583→}
   584→
   585→func TestArrayDecl(t *testing.T) {
   586→	input := `[5]INT arr:
   587→`
   588→	l := lexer.New(input)
   589→	p := New(l)
   590→	program := p.ParseProgram()
   591→	checkParserErrors(t, p)
   592→
   593→	if len(program.Statements) != 1 {
   594→		t.Fatalf("expected 1 statement, got %d", len(program.Statements))
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "offset": 1010,
  "limit": 40
}
  1010→		t.Fatalf("expected 3 choices, got %d", len(caseStmt.Choices))
  1011→	}
  1012→
  1013→	// First choice: value 1
  1014→	if caseStmt.Choices[0].IsElse {
  1015→		t.Error("first choice should not be ELSE")
  1016→	}
  1017→	if len(caseStmt.Choices[0].Values) != 1 {
  1018→		t.Fatalf("expected 1 value in first choice, got %d", len(caseStmt.Choices[0].Values))
  1019→	}
  1020→	if caseStmt.Choices[0].Body == nil {
  1021→		t.Error("expected body on first choice")
  1022→	}
  1023→
  1024→	// Second choice: value 2
  1025→	if caseStmt.Choices[1].IsElse {
  1026→		t.Error("second choice should not be ELSE")
  1027→	}
  1028→	if caseStmt.Choices[1].Body == nil {
  1029→		t.Error("expected body on second choice")
  1030→	}
  1031→
  1032→	// Third choice: ELSE
  1033→	if !caseStmt.Choices[2].IsElse {
  1034→		t.Error("third choice should be ELSE")
  1035→	}
  1036→	if caseStmt.Choices[2].Body == nil {
  1037→		t.Error("expected body on ELSE choice")
  1038→	}
  1039→}
  1040→
  1041→func TestTimerDecl(t *testing.T) {
  1042→	input := `TIMER tim:
  1043→`
  1044→	l := lexer.New(input)
  1045→	p := New(l)
  1046→	program := p.ParseProgram()
  1047→	checkParserErrors(t, p)
  1048→
  1049→	if len(program.Statements) != 1 {

Now I need to update all Body == nil checks to len(Body) == 0 checks. Let me do them all:

✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
	if loop.Body == nil {
		t.Error("expected body")
	}
+
	if len(loop.Body) == 0 {
		t.Error("expected body")
	}
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
	if ifStmt.Choices[0].Body == nil {
		t.Error("expected body on first choice")
	}

	if ifStmt.Choices[1].Condition == nil {
		t.Error("expected condition on second choice")
	}

	if ifStmt.Choices[1].Body == nil {
		t.Error("expected body on second choice")
	}
+
	if len(ifStmt.Choices[0].Body) == 0 {
		t.Error("expected body on first choice")
	}

	if ifStmt.Choices[1].Condition == nil {
		t.Error("expected condition on second choice")
	}

	if len(ifStmt.Choices[1].Body) == 0 {
		t.Error("expected body on second choice")
	}
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
	if ifStmt.Choices[0].Body == nil {
		t.Error("expected body on choice")
	}
}
+
	if len(ifStmt.Choices[0].Body) == 0 {
		t.Error("expected body on choice")
	}
}
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.
✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
	if caseStmt.Choices[0].Body == nil {
		t.Error("expected body on first choice")
	}

	// Second choice: value 2
	if caseStmt.Choices[1].IsElse {
		t.Error("second choice should not be ELSE")
	}
	if caseStmt.Choices[1].Body == nil {
		t.Error("expected body on second choice")
	}

	// Third choice: ELSE
	if !caseStmt.Choices[2].IsElse {
		t.Error("third choice should be ELSE")
	}
	if caseStmt.Choices[2].Body == nil {
		t.Error("expected body on ELSE choice")
	}
+
	if len(caseStmt.Choices[0].Body) == 0 {
		t.Error("expected body on first choice")
	}

	// Second choice: value 2
	if caseStmt.Choices[1].IsElse {
		t.Error("second choice should not be ELSE")
	}
	if len(caseStmt.Choices[1].Body) == 0 {
		t.Error("expected body on second choice")
	}

	// Third choice: ELSE
	if !caseStmt.Choices[2].IsElse {
		t.Error("third choice should be ELSE")
	}
	if len(caseStmt.Choices[2].Body) == 0 {
		t.Error("expected body on ELSE choice")
	}
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.

Now let me add the new tests. Let me find the end of the parser test file:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/parser/parser_test.go",
  "offset": 2690
}
  2690→		t.Fatalf("expected ProcDecl, got %T", program.Statements[0])
  2691→	}
  2692→
  2693→	// Body should have 3 statements: 2 VarDecls + SeqBlock
  2694→	if len(proc.Body) != 3 {
  2695→		t.Fatalf("expected 3 body statements, got %d", len(proc.Body))
  2696→	}
  2697→
  2698→	for i := 0; i < 2; i++ {
  2699→		if _, ok := proc.Body[i].(*ast.VarDecl); !ok {
  2700→			t.Errorf("expected VarDecl at index %d, got %T", i, proc.Body[i])
  2701→		}
  2702→	}
  2703→
  2704→	if _, ok := proc.Body[2].(*ast.SeqBlock); !ok {
  2705→		t.Errorf("expected SeqBlock at index 2, got %T", proc.Body[2])
  2706→	}
  2707→}
  2708→
  2709→func TestCheckedArithmeticOperators(t *testing.T) {
  2710→	tests := []struct {
  2711→		input    string
  2712→		operator string
  2713→	}{
  2714→		{"x := a PLUS b\n", "PLUS"},
  2715→		{"x := a MINUS b\n", "MINUS"},
  2716→		{"x := a TIMES b\n", "TIMES"},
  2717→	}
  2718→
  2719→	for _, tt := range tests {
  2720→		l := lexer.New(tt.input)
  2721→		p := New(l)
  2722→		program := p.ParseProgram()
  2723→		checkParserErrors(t, p)
  2724→
  2725→		if len(program.Statements) != 1 {
  2726→			t.Fatalf("[%s] expected 1 statement, got %d", tt.operator, len(program.Statements))
  2727→		}
  2728→
  2729→		assign, ok := program.Statements[0].(*ast.Assignment)
  2730→		if !ok {
  2731→			t.Fatalf("[%s] expected Assignment, got %T", tt.operator, program.Statements[0])
  2732→		}
  2733→
  2734→		binExpr, ok := assign.Value.(*ast.BinaryExpr)
  2735→		if !ok {
  2736→			t.Fatalf("[%s] expected BinaryExpr, got %T", tt.operator, assign.Value)
  2737→		}
  2738→
  2739→		if binExpr.Operator != tt.operator {
  2740→			t.Errorf("[%s] expected operator %q, got %q", tt.operator, tt.operator, binExpr.Operator)
  2741→		}
  2742→	}
  2743→}
  2744→
  2745→func TestCheckedArithmeticPrecedence(t *testing.T) {
  2746→	// a PLUS b TIMES c should parse as a PLUS (b TIMES c)
  2747→	input := "x := a PLUS b TIMES c\n"
  2748→	l := lexer.New(input)
  2749→	p := New(l)
  2750→	program := p.ParseProgram()
  2751→	checkParserErrors(t, p)
  2752→
  2753→	assign := program.Statements[0].(*ast.Assignment)
  2754→	binExpr := assign.Value.(*ast.BinaryExpr)
  2755→
  2756→	if binExpr.Operator != "PLUS" {
  2757→		t.Errorf("expected top-level operator PLUS, got %s", binExpr.Operator)
  2758→	}
  2759→
  2760→	rightBin, ok := binExpr.Right.(*ast.BinaryExpr)
  2761→	if !ok {
  2762→		t.Fatalf("expected right side to be BinaryExpr, got %T", binExpr.Right)
  2763→	}
  2764→	if rightBin.Operator != "TIMES" {
  2765→		t.Errorf("expected right operator TIMES, got %s", rightBin.Operator)
  2766→	}
  2767→}
  2768→
  2769→func TestCheckedAndSymbolMixed(t *testing.T) {
  2770→	// a + (b TIMES c) should work with mixed operators
  2771→	input := "x := a + (b TIMES c)\n"
  2772→	l := lexer.New(input)
  2773→	p := New(l)
  2774→	program := p.ParseProgram()
  2775→	checkParserErrors(t, p)
  2776→
  2777→	assign := program.Statements[0].(*ast.Assignment)
  2778→	binExpr := assign.Value.(*ast.BinaryExpr)
  2779→
  2780→	if binExpr.Operator != "+" {
  2781→		t.Errorf("expected top-level operator +, got %s", binExpr.Operator)
  2782→	}
  2783→}
  2784→

Now let me add the new tests at the end of the file:

✏️ Edit parser_test.go
/home/david/projects/code-associates/occam2go/parser/parser_test.go
func TestCheckedAndSymbolMixed(t *testing.T) {
	// a + (b TIMES c) should work with mixed operators
	input := "x := a + (b TIMES c)\n"
	l := lexer.New(input)
	p := New(l)
	program := p.ParseProgram()
	checkParserErrors(t, p)

	assign := program.Statements[0].(*ast.Assignment)
	binExpr := assign.Value.(*ast.BinaryExpr)

	if binExpr.Operator != "+" {
		t.Errorf("expected top-level operator +, got %s", binExpr.Operator)
	}
}
+
func TestCheckedAndSymbolMixed(t *testing.T) {
	// a + (b TIMES c) should work with mixed operators
	input := "x := a + (b TIMES c)\n"
	l := lexer.New(input)
	p := New(l)
	program := p.ParseProgram()
	checkParserErrors(t, p)

	assign := program.Statements[0].(*ast.Assignment)
	binExpr := assign.Value.(*ast.BinaryExpr)

	if binExpr.Operator != "+" {
		t.Errorf("expected top-level operator +, got %s", binExpr.Operator)
	}
}

func TestMultiStatementIfBody(t *testing.T) {
	input := `IF
  x > 0
    INT y:
    y := 42
    print.int(y)
  TRUE
    SKIP
`
	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))
	}

	ifStmt, ok := program.Statements[0].(*ast.IfStatement)
	if !ok {
		t.Fatalf("expected IfStatement, got %T", program.Statements[0])
	}

	if len(ifStmt.Choices) != 2 {
		t.Fatalf("expected 2 choices, got %d", len(ifStmt.Choices))
	}

	// First choice should have 3 body statements: VarDecl, Assignment, ProcCall
	if len(ifStmt.Choices[0].Body) != 3 {
		t.Fatalf("expected 3 body statements in first choice, got %d", len(ifStmt.Choices[0].Body))
	}
	if _, ok := ifStmt.Choices[0].Body[0].(*ast.VarDecl); !ok {
		t.Errorf("expected VarDecl as first body stmt, got %T", ifStmt.Choices[0].Body[0])
	}
	if _, ok := ifStmt.Choices[0].Body[1].(*ast.Assignment); !ok {
		t.Errorf("expected Assignment as second body stmt, got %T", ifStmt.Choices[0].Body[1])
	}
	if _, ok := ifStmt.Choices[0].Body[2].(*ast.ProcCall); !ok {
		t.Errorf("expected ProcCall as third body stmt, got %T", ifStmt.Choices[0].Body[2])
	}

	// Second choice should have 1 body statement: Skip
	if len(ifStmt.Choices[1].Body) != 1 {
		t.Fatalf("expected 1 body statement in second choice, got %d", len(ifStmt.Choices[1].Body))
	}
	if _, ok := ifStmt.Choices[1].Body[0].(*ast.Skip); !ok {
		t.Errorf("expected Skip, got %T", ifStmt.Choices[1].Body[0])
	}
}

func TestChannelDirAtCallSite(t *testing.T) {
	input := `foo(out!, in?)
`
	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))
	}

	call, ok := program.Statements[0].(*ast.ProcCall)
	if !ok {
		t.Fatalf("expected ProcCall, got %T", program.Statements[0])
	}

	if call.Name != "foo" {
		t.Errorf("expected proc name 'foo', got %q", call.Name)
	}

	if len(call.Args) != 2 {
		t.Fatalf("expected 2 args, got %d", len(call.Args))
	}

	arg0, ok := call.Args[0].(*ast.Identifier)
	if !ok {
		t.Fatalf("expected Identifier for arg 0, got %T", call.Args[0])
	}
	if arg0.Value != "out" {
		t.Errorf("expected arg 0 = 'out', got %q", arg0.Value)
	}

	arg1, ok := call.Args[1].(*ast.Identifier)
	if !ok {
		t.Fatalf("expected Identifier for arg 1, got %T", call.Args[1])
	}
	if arg1.Value != "in" {
		t.Errorf("expected arg 1 = 'in', got %q", arg1.Value)
	}
}
The file /home/david/projects/code-associates/occam2go/parser/parser_test.go has been updated successfully.

Now let me add e2e tests. Let me check the existing e2e test file:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_test.go",
  "offset": 1,
  "limit": 30
}
File does not exist.
Glob
{
  "pattern": "codegen/*_test.go"
}
/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go
/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_protocol_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_array_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_basic_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_types_test.go
/home/david/projects/code-associates/occam2go/codegen/codegen_test.go
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go"
}
     1→package codegen
     2→
     3→import (
     4→	"os"
     5→	"os/exec"
     6→	"path/filepath"
     7→	"testing"
     8→
     9→	"github.com/codeassociates/occam2go/lexer"
    10→	"github.com/codeassociates/occam2go/parser"
    11→	"github.com/codeassociates/occam2go/preproc"
    12→)
    13→
    14→// transpileCompileRun takes Occam source, transpiles to Go, compiles, runs,
    15→// and returns the stdout output
    16→func transpileCompileRun(t *testing.T, occamSource string) string {
    17→	t.Helper()
    18→
    19→	// Transpile
    20→	l := lexer.New(occamSource)
    21→	p := parser.New(l)
    22→	program := p.ParseProgram()
    23→
    24→	if len(p.Errors()) > 0 {
    25→		for _, err := range p.Errors() {
    26→			t.Errorf("parser error: %s", err)
    27→		}
    28→		t.FailNow()
    29→	}
    30→
    31→	gen := New()
    32→	goCode := gen.Generate(program)
    33→
    34→	// Create temp directory for this test
    35→	tmpDir, err := os.MkdirTemp("", "occam2go-test-*")
    36→	if err != nil {
    37→		t.Fatalf("failed to create temp dir: %v", err)
    38→	}
    39→	defer os.RemoveAll(tmpDir)
    40→
    41→	// Write Go source
    42→	goFile := filepath.Join(tmpDir, "main.go")
    43→	if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil {
    44→		t.Fatalf("failed to write Go file: %v", err)
    45→	}
    46→
    47→	// Compile
    48→	binFile := filepath.Join(tmpDir, "main")
    49→	compileCmd := exec.Command("go", "build", "-o", binFile, goFile)
    50→	compileOutput, err := compileCmd.CombinedOutput()
    51→	if err != nil {
    52→		t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, compileOutput, goCode)
    53→	}
    54→
    55→	// Run
    56→	runCmd := exec.Command(binFile)
    57→	output, err := runCmd.CombinedOutput()
    58→	if err != nil {
    59→		t.Fatalf("execution failed: %v\nOutput: %s", err, output)
    60→	}
    61→
    62→	return string(output)
    63→}
    64→
    65→// transpileCompileRunFromFile takes an occam file path, preprocesses it,
    66→// then transpiles, compiles, and runs.
    67→func transpileCompileRunFromFile(t *testing.T, mainFile string, includePaths []string) string {
    68→	t.Helper()
    69→
    70→	pp := preproc.New(preproc.WithIncludePaths(includePaths))
    71→	expanded, err := pp.ProcessFile(mainFile)
    72→	if err != nil {
    73→		t.Fatalf("preprocessor error: %v", err)
    74→	}
    75→	if len(pp.Errors()) > 0 {
    76→		for _, e := range pp.Errors() {
    77→			t.Errorf("preprocessor warning: %s", e)
    78→		}
    79→	}
    80→
    81→	return transpileCompileRun(t, expanded)
    82→}
    83→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go",
  "offset": 1,
  "limit": 50
}
     1→package codegen
     2→
     3→import "testing"
     4→
     5→func TestE2E_IfBasic(t *testing.T) {
     6→	// Test basic IF: first branch is true
     7→	occam := `SEQ
     8→  INT x, y:
     9→  x := 5
    10→  y := 0
    11→  IF
    12→    x > 0
    13→      y := 1
    14→    x = 0
    15→      y := 2
    16→  print.int(y)
    17→`
    18→	output := transpileCompileRun(t, occam)
    19→	expected := "1\n"
    20→	if output != expected {
    21→		t.Errorf("expected %q, got %q", expected, output)
    22→	}
    23→}
    24→
    25→func TestE2E_IfSecondBranch(t *testing.T) {
    26→	// Test IF where second branch matches
    27→	occam := `SEQ
    28→  INT x, y:
    29→  x := 0
    30→  y := 0
    31→  IF
    32→    x > 0
    33→      y := 1
    34→    x = 0
    35→      y := 2
    36→  print.int(y)
    37→`
    38→	output := transpileCompileRun(t, occam)
    39→	expected := "2\n"
    40→	if output != expected {
    41→		t.Errorf("expected %q, got %q", expected, output)
    42→	}
    43→}
    44→
    45→func TestE2E_IfThreeBranches(t *testing.T) {
    46→	// Test IF with three choices where the last matches
    47→	occam := `SEQ
    48→  INT x, y:
    49→  x := 0
    50→  y := 0

Good. Now let me add the e2e tests at the end of e2e_control_test.go:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go",
  "offset": 200
}
   200→	if output != expected {
   201→		t.Errorf("expected %q, got %q", expected, output)
   202→	}
   203→}
   204→
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go",
  "offset": 180
}
   180→	expected := "0\n"
   181→	if output != expected {
   182→		t.Errorf("expected %q, got %q", expected, output)
   183→	}
   184→}
   185→
   186→func TestE2E_CaseExpression(t *testing.T) {
   187→	occam := `SEQ
   188→  INT x:
   189→  x := 3
   190→  CASE x + 1
   191→    3
   192→      print.int(30)
   193→    4
   194→      print.int(40)
   195→    ELSE
   196→      print.int(0)
   197→`
   198→	output := transpileCompileRun(t, occam)
   199→	expected := "40\n"
   200→	if output != expected {
   201→		t.Errorf("expected %q, got %q", expected, output)
   202→	}
   203→}
   204→
✏️ Edit e2e_control_test.go
/home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go
func TestE2E_CaseExpression(t *testing.T) {
	occam := `SEQ
  INT x:
  x := 3
  CASE x + 1
    3
      print.int(30)
    4
      print.int(40)
    ELSE
      print.int(0)
`
	output := transpileCompileRun(t, occam)
	expected := "40\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}
+
func TestE2E_CaseExpression(t *testing.T) {
	occam := `SEQ
  INT x:
  x := 3
  CASE x + 1
    3
      print.int(30)
    4
      print.int(40)
    ELSE
      print.int(0)
`
	output := transpileCompileRun(t, occam)
	expected := "40\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_MultiStatementIfBody(t *testing.T) {
	occam := `SEQ
  INT x:
  x := 5
  IF
    x > 0
      INT y:
      y := x + 10
      print.int(y)
    TRUE
      SKIP
`
	output := transpileCompileRun(t, occam)
	expected := "15\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_MultiStatementCaseBody(t *testing.T) {
	occam := `SEQ
  INT x:
  x := 2
  CASE x
    1
      print.int(10)
    2
      INT y:
      y := x * 100
      print.int(y)
    ELSE
      print.int(0)
`
	output := transpileCompileRun(t, occam)
	expected := "200\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_MultiStatementWhileBody(t *testing.T) {
	occam := `SEQ
  INT x:
  x := 0
  WHILE x < 3
    INT step:
    step := 1
    x := x + step
  print.int(x)
`
	output := transpileCompileRun(t, occam)
	expected := "3\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}

func TestE2E_ChannelDirAtCallSite(t *testing.T) {
	occam := `PROC worker(CHAN OF INT in?, CHAN OF INT out!)
  INT x:
  in ? x
  out ! x + 1
:
SEQ
  CHAN OF INT a:
  CHAN OF INT b:
  PAR
    worker(a?, b!)
    SEQ
      a ! 10
      INT result:
      b ? result
      print.int(result)
`
	output := transpileCompileRun(t, occam)
	expected := "11\n"
	if output != expected {
		t.Errorf("expected %q, got %q", expected, output)
	}
}
The file /home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go has been updated successfully.
TaskUpdate
{
  "taskId": "4",
  "status": "completed"
}
Updated task #4 status
TaskUpdate
{
  "taskId": "5",
  "status": "in_progress"
}
Updated task #5 status

Step 5: Run 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	9.690s
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 also update the TODO.md as mentioned in the plan:

Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/TODO.md"
}
     1→# occam2go — Implementation Status
     2→
     3→## Fully Implemented
     4→
     5→### Core Constructs
     6→- **SEQ** — Sequential execution, with replicators (`SEQ i = 0 FOR n`) and optional STEP
     7→- **PAR** — Parallel execution via goroutines + sync.WaitGroup, with replicators
     8→- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators
     9→- **WHILE** — Loops, maps to Go `for` loops
    10→- **CASE** — Pattern matching with multiple cases and ELSE branch
    11→- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards and timer timeouts
    12→- **SKIP** — No-op process
    13→- **STOP** — Error + deadlock
    14→
    15→### Data Types & Declarations
    16→- **INT, BYTE, BOOL, REAL, REAL32, REAL64** — Scalar types (REAL/REAL64 map to float64, REAL32 maps to float32)
    17→- **Variable declarations** — `INT x, y, z:`
    18→- **Arrays** — `[n]TYPE arr:` with index expressions
    19→- **Channels** — `CHAN OF TYPE c:` with send (`!`) and receive (`?`); `CHAN BYTE` shorthand (without `OF`)
    20→- **Channel arrays** — `[n]CHAN OF TYPE cs:` with indexed send/receive and `[]CHAN OF TYPE` proc params
    21→- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only)
    22→- **Timers** — `TIMER tim:` with reads and `AFTER` expressions
    23→- **Abbreviations** — `VAL INT x IS 1:`, `INT y IS z:` — named constants and aliases
    24→- **INITIAL declarations** — `INITIAL INT x IS 42:` — mutable variables with initial values
    25→- **Byte literals** — `'A'`, `'0'` with occam escape sequences (`*n`, `*c`, `*t`)
    26→- **Hex integer literals** — `#FF`, `#80000000`
    27→
    28→### Procedures & Functions
    29→- **PROC** — Declaration with VAL, reference, CHAN OF, and open array (`[]TYPE`) parameters
    30→- **PROC calls** — With automatic `&`/`*` for reference params, pass-through for channels
    31→- **FUNCTION (IS form)** — `INT FUNCTION square(VAL INT x) IS x * x`
    32→- **FUNCTION (VALOF form)** — Local declarations + VALOF body + RESULT
    33→- **Multi-result FUNCTIONs** — `INT, INT FUNCTION f(...)` returning multiple values via `RESULT a, b`
    34→- **Nested PROCs/FUNCTIONs** — Local definitions inside a PROC body, compiled as Go closures
    35→- **KRoC-style colon terminators** — Optional `:` at end of PROC/FUNCTION body
    36→- **Built-in print** — `print.int`, `print.bool`, `print.string`, `print.newline`
    37→
    38→### Expressions & Operators
    39→- **Arithmetic** — `+`, `-`, `*`, `/`, `\` (modulo)
    40→- **Comparison** — `=`, `<>`, `<`, `>`, `<=`, `>=`
    41→- **Logical** — `AND`, `OR`, `NOT`
    42→- **Bitwise** — `/\`, `\/`, `><`, `~`, `<<`, `>>`
    43→- **AFTER** — As boolean expression (maps to `>`)
    44→- **Parenthesized expressions**
    45→- **Array indexing** — `arr[i]`, `arr[expr]`
    46→- **String literals** — Double-quoted strings
    47→- **Type conversions** — `INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`
    48→- **Checked arithmetic** — `PLUS`, `MINUS`, `TIMES` — modular (wrapping) operators
    49→- **MOSTNEG/MOSTPOS** — Type min/max constants for INT, BYTE, REAL32, REAL64
    50→- **SIZE operator** — `SIZE arr`, `SIZE "str"` maps to `len()`
    51→- **Array slices** — `[arr FROM n FOR m]` with slice assignment
    52→- **Multi-assignment** — `a, b := f(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`
    53→
    54→### Protocols
    55→- **Simple** — `PROTOCOL SIG IS INT` (type alias)
    56→- **Sequential** — `PROTOCOL PAIR IS INT ; BYTE` (struct)
    57→- **Variant** — `PROTOCOL MSG CASE tag; TYPE ...` (interface + concrete types)
    58→
    59→### Records
    60→- **RECORD** — Struct types with field access via bracket syntax (`p[x]`)
    61→
    62→### Preprocessor
    63→- **`#IF` / `#ELSE` / `#ENDIF`** — Conditional compilation with `TRUE`, `FALSE`, `DEFINED()`, `NOT`, equality
    64→- **`#DEFINE`** — Symbol definition
    65→- **`#INCLUDE`** — File inclusion with search paths and include guards
    66→- **`#COMMENT` / `#PRAGMA` / `#USE`** — Ignored (blank lines)
    67→- **Predefined symbols** — `TARGET.BITS.PER.WORD = 64`
    68→
    69→### Tooling
    70→- **gen-module** — Generate `.module` files from KRoC SConscript build files
    71→
    72→---
    73→
    74→## Not Yet Implemented
    75→
    76→### Required for shared_screen module (extends course module)
    77→
    78→| Feature | Notes | Used in |
    79→|---------|-------|---------|
    80→| **`DATA TYPE X IS TYPE:`** | Simple type alias (e.g. `DATA TYPE COLOUR IS BYTE:`). | shared_screen.inc |
    81→| **`DATA TYPE X RECORD`** | Alternative record syntax (vs current `RECORD X`). | shared_screen.inc |
    82→| **Counted array protocol** | `BYTE::[]BYTE` — length-prefixed array in protocols. | shared_screen.inc, shared_screen.occ |
    83→| **`RESULT` param qualifier** | `RESULT INT len` on PROC params (output-only, like a write-only reference). | float_io.occ |
    84→
    85→### Other language features
    86→
    87→| Feature | Notes |
    88→|---------|-------|
    89→| **PRI ALT / PRI PAR** | Priority variants of ALT and PAR. |
    90→| **PLACED PAR** | Assigning processes to specific hardware. |
    91→| **PORT OF** | Hardware port mapping. |
    92→| **`RETYPES`** | Type punning / reinterpret cast (`VAL INT X RETYPES X :`). Used in float_io.occ. |
    93→| **`CAUSEERROR ()`** | Built-in error-raising primitive. Used in float_io.occ. |
    94→| **Transputer intrinsics** | `LONGPROD`, `LONGDIV`, `LONGSUM`, `LONGDIFF`, `NORMALISE`, `SHIFTLEFT`, `SHIFTRIGHT`. Used in float_io.occ. |
    95→| **`VAL []BYTE` abbreviations** | `VAL []BYTE cmap IS "0123456789ABCDEF":` — named string constants. |
    96→| **`#PRAGMA DEFINED`** | Compiler hint to suppress definedness warnings. Can be ignored. |
    97→
✏️ Edit TODO.md
/home/david/projects/code-associates/occam2go/TODO.md
- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators
- **WHILE** — Loops, maps to Go `for` loops
- **CASE** — Pattern matching with multiple cases and ELSE branch
- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards and timer timeouts
+
- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators; supports multi-statement bodies (declarations scoped before process)
- **WHILE** — Loops, maps to Go `for` loops; supports multi-statement bodies
- **CASE** — Pattern matching with multiple cases and ELSE branch; supports multi-statement bodies
- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards, timer timeouts, and multi-statement bodies
The file /home/david/projects/code-associates/occam2go/TODO.md has been updated successfully.
✏️ Edit TODO.md
/home/david/projects/code-associates/occam2go/TODO.md
- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only)
+
- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only); direction annotations at call sites (`out!`, `in?`) accepted and ignored
The file /home/david/projects/code-associates/occam2go/TODO.md has been updated successfully.

Now update the CLAUDE.md to reflect these changes:

✏️ Edit CLAUDE.md
/home/david/projects/code-associates/occam2go/CLAUDE.md
| `PROC f(CHAN OF INT c?)` | `func f(c <-chan int)` (input/receive-only) |
| `PROC f(CHAN OF INT c!)` | `func f(c chan<- int)` (output/send-only) |
+
| `PROC f(CHAN OF INT c?)` | `func f(c <-chan int)` (input/receive-only) |
| `PROC f(CHAN OF INT c!)` | `func f(c chan<- int)` (output/send-only) |
| `f(out!, in?)` (call-site dir) | `f(out, in)` (direction annotations ignored) |
<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
  "offset": 35,
  "limit": 30
}
    35→
    36→1. **`preproc/`** — Textual preprocessor (pre-lexer pass). Handles `#IF`/`#ELSE`/`#ENDIF`/`#DEFINE` conditional compilation, `#INCLUDE` file inclusion with search paths, and ignores `#COMMENT`/`#PRAGMA`/`#USE`. Produces a single expanded string for the lexer.
    37→   - `preproc.go` — Preprocessor with condition stack and expression evaluator
    38→
    39→2. **`lexer/`** — Tokenizer with indentation tracking. Produces `INDENT`/`DEDENT` tokens from whitespace changes (2-space indent = 1 level). Key files:
    40→   - `token.go` — Token types and keyword lookup
    41→   - `lexer.go` — Lexer with `indentStack` and `pendingTokens` queue
    42→
    43→3. **`parser/`** — Recursive descent parser with Pratt expression parsing. Produces AST.
    44→   - `parser.go` — All parsing logic in one file
    45→
    46→4. **`ast/`** — AST node definitions. Every construct has a struct.
    47→   - `ast.go` — All node types: `Program`, `SeqBlock`, `ParBlock`, `VarDecl`, `Assignment`, `ProcDecl`, `FuncDecl`, etc.
    48→
    49→5. **`codegen/`** — AST → Go source code. Two-pass: first collects metadata (imports, proc signatures), then generates.
    50→   - `codegen.go` — Generator with `strings.Builder` output
    51→   - `codegen_test.go` — Unit tests (transpile, check output strings)
    52→   - `e2e_test.go` — End-to-end tests (transpile → `go build` → execute → check stdout)
    53→
    54→6. **`modgen/`** — Generates `.module` files from KRoC SConscript build files. Parses Python-based SConscript to extract source lists and `OccamLibrary` calls.
    55→   - `modgen.go` — SConscript parser and module file generator
    56→
    57→7. **`main.go`** — CLI entry point wiring the pipeline together
    58→
    59→## Occam → Go Mapping
    60→
    61→| Occam | Go |
    62→|---|---|
    63→| `SEQ` | Sequential statements (Go default) |
    64→| `SEQ i = 0 FOR n` | `for i := 0; i < n; i++` |
✏️ Edit CLAUDE.md
/home/david/projects/code-associates/occam2go/CLAUDE.md
| `PROC f(CHAN OF INT c?)` | `func f(c <-chan int)` (input/receive-only) |
| `PROC f(CHAN OF INT c!)` | `func f(c chan<- int)` (output/send-only) |
+
| `PROC f(CHAN OF INT c?)` | `func f(c <-chan int)` (input/receive-only) |
| `PROC f(CHAN OF INT c!)` | `func f(c chan<- int)` (output/send-only) |
| `f(out!, in?)` (call-site dir) | `f(out, in)` (direction annotations ignored) |
The file /home/david/projects/code-associates/occam2go/CLAUDE.md has been updated successfully.

Also update the "What's Implemented" section in CLAUDE.md to mention these features:

Grep
{
  "pattern": "What's Implemented",
  "path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
  "output_mode": "content"
}
160:## What's Implemented
Read
{
  "file_path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
  "offset": 160,
  "limit": 10
}
   160→## What's Implemented
   161→
   162→Preprocessor (`#IF`/`#ELSE`/`#ENDIF`/`#DEFINE`/`#INCLUDE` with search paths, include guards, `#COMMENT`/`#PRAGMA`/`#USE` ignored), module file generation from SConscript (`gen-module` subcommand), SEQ, PAR, IF, WHILE, CASE, ALT (with guards and timer timeouts), SKIP, STOP, variable/array/channel/timer declarations, abbreviations (`VAL INT x IS 42:`, `INT y IS z:`), assignments (simple and indexed), channel send/receive, channel arrays (`[n]CHAN OF TYPE` with indexed send/receive and `[]CHAN OF TYPE` proc params), PROC (with VAL, reference, CHAN, []CHAN, and open array `[]TYPE` params), channel direction restrictions (`CHAN OF INT c?` → `<-chan int`, `CHAN OF INT c!` → `chan<- int`), FUNCTION (IS and VALOF forms, including multi-result `INT, INT FUNCTION` with `RESULT a, b`), multi-assignment (`a, b := func(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`), KRoC-style colon terminators on PROC/FUNCTION (optional), replicators on SEQ/PAR/IF (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`, etc.), REAL32/REAL64 types, hex integer literals (`#FF`, `#80000000`), string literals, byte literals (`'A'`, `'*n'` with occam escape sequences), built-in print procedures, protocols (simple, sequential, and variant), record types (with field access via bracket syntax), SIZE operator, array slices (`[arr FROM n FOR m]` with slice assignment), nested PROCs/FUNCTIONs (local definitions as Go closures), MOSTNEG/MOSTPOS (type min/max constants for INT, BYTE, REAL32, REAL64), INITIAL declarations (`INITIAL INT x IS 42:` — mutable variable with initial value), checked (modular) arithmetic (`PLUS`, `MINUS`, `TIMES` — wrapping operators).
   163→
   164→## Not Yet Implemented
   165→
   166→PRI ALT/PRI PAR, PLACED PAR, PORT OF. See `TODO.md` for the full list with priorities.
   167→
✏️ Edit CLAUDE.md
/home/david/projects/code-associates/occam2go/CLAUDE.md
channel direction restrictions (`CHAN OF INT c?` → `<-chan int`, `CHAN OF INT c!` → `chan<- int`),
+
channel direction restrictions (`CHAN OF INT c?` → `<-chan int`, `CHAN OF INT c!` → `chan<- int`, call-site annotations `out!`/`in?` accepted),
The file /home/david/projects/code-associates/occam2go/CLAUDE.md has been updated successfully.
✏️ Edit CLAUDE.md
/home/david/projects/code-associates/occam2go/CLAUDE.md
SEQ, PAR, IF, WHILE, CASE, ALT (with guards and timer timeouts),
+
SEQ, PAR, IF, WHILE, CASE, ALT (with guards, timer timeouts, and multi-statement bodies with scoped declarations),
The file /home/david/projects/code-associates/occam2go/CLAUDE.md has been updated successfully.

Let me run the tests one final time to make sure everything is still passing:

$ Bash
Final test run
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	(cached)
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	(cached)
ok  	github.com/codeassociates/occam2go/preproc	(cached)

All tests pass. Let me also verify the new tests specifically ran:

$ Bash
Run new tests with verbose output
go test ./parser -run TestMultiStatementIfBody -v 2>&1 && go test ./parser -run TestChannelDirAtCallSite -v 2>&1 && go test ./codegen -run "TestE2E_MultiStatement|TestE2E_ChannelDir" -v 2>&1
=== RUN   TestMultiStatementIfBody
--- PASS: TestMultiStatementIfBody (0.00s)
PASS
ok  	github.com/codeassociates/occam2go/parser	0.001s
=== RUN   TestChannelDirAtCallSite
--- PASS: TestChannelDirAtCallSite (0.00s)
PASS
ok  	github.com/codeassociates/occam2go/parser	0.001s
=== RUN   TestE2E_MultiStatementIfBody
--- PASS: TestE2E_MultiStatementIfBody (0.08s)
=== RUN   TestE2E_MultiStatementCaseBody
--- PASS: TestE2E_MultiStatementCaseBody (0.08s)
=== RUN   TestE2E_MultiStatementWhileBody
--- PASS: TestE2E_MultiStatementWhileBody (0.08s)
=== RUN   TestE2E_ChannelDirAtCallSite
--- PASS: TestE2E_ChannelDirAtCallSite (0.08s)
PASS
ok  	github.com/codeassociates/occam2go/codegen	0.316s

All 6 new tests pass. Let me also quickly verify the binary builds and try the course library scenario from the plan:

$ Bash
Build the binary
go build -o occam2go . 2>&1
$ Bash
Check if course library exists
ls /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/SConscript 2>&1
/home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/SConscript
$ Bash
Generate course module file
./occam2go gen-module -o kroc/modules/course/libsrc/course.module kroc/modules/course/libsrc/SConscript 2>&1
$ Bash
Check if demo exists
ls /home/david/projects/code-associates/occam2go/kroc/demos/complexity/concurrent_hello_1.occ 2>&1
/home/david/projects/code-associates/occam2go/kroc/demos/complexity/concurrent_hello_1.occ
$ Bash
Try transpiling course library demo
./occam2go -I kroc/modules/course/libsrc kroc/demos/complexity/concurrent_hello_1.occ 2>&1
Exit code 137
[Request interrupted by user for tool use]

Continue from where you left off.

No response requested.

I think the previous command also leaked memory until the kernel crashed. Would it make sense to commit the current fixes to the repo before moving on to try to diagnose the remaining infinite loop problem?

Yes, that makes sense — let's lock in the working changes first, then investigate the remaining issue separately.

$ 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:   CLAUDE.md
	modified:   TODO.md
	modified:   ast/ast.go
	modified:   codegen/codegen.go
	modified:   codegen/e2e_control_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 staged and unstaged changes
git diff
diff --git a/CLAUDE.md b/CLAUDE.md
index e0c5f30..069043a 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -107,6 +107,7 @@ Six packages, one pipeline:
 | `PROC f([]CHAN OF INT cs)` | `func f(cs []chan int)` |
 | `PROC f(CHAN OF INT c?)` | `func f(c <-chan int)` (input/receive-only) |
 | `PROC f(CHAN OF INT c!)` | `func f(c chan<- int)` (output/send-only) |
+| `f(out!, in?)` (call-site dir) | `f(out, in)` (direction annotations ignored) |
 | Non-VAL params | `*type` pointer params, callers pass `&arg` |
 | `PROC f([]INT arr)` | `func f(arr []int)` (open array param, slice) |
 | `PROC f(VAL []INT arr)` | `func f(arr []int)` (VAL open array, also slice) |
@@ -158,7 +159,7 @@ Typical workflow for a new language construct:
 
 ## What's Implemented
 
-Preprocessor (`#IF`/`#ELSE`/`#ENDIF`/`#DEFINE`/`#INCLUDE` with search paths, include guards, `#COMMENT`/`#PRAGMA`/`#USE` ignored), module file generation from SConscript (`gen-module` subcommand), SEQ, PAR, IF, WHILE, CASE, ALT (with guards and timer timeouts), SKIP, STOP, variable/array/channel/timer declarations, abbreviations (`VAL INT x IS 42:`, `INT y IS z:`), assignments (simple and indexed), channel send/receive, channel arrays (`[n]CHAN OF TYPE` with indexed send/receive and `[]CHAN OF TYPE` proc params), PROC (with VAL, reference, CHAN, []CHAN, and open array `[]TYPE` params), channel direction restrictions (`CHAN OF INT c?` → `<-chan int`, `CHAN OF INT c!` → `chan<- int`), FUNCTION (IS and VALOF forms, including multi-result `INT, INT FUNCTION` with `RESULT a, b`), multi-assignment (`a, b := func(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`), KRoC-style colon terminators on PROC/FUNCTION (optional), replicators on SEQ/PAR/IF (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`, etc.), REAL32/REAL64 types, hex integer literals (`#FF`, `#80000000`), string literals, byte literals (`'A'`, `'*n'` with occam escape sequences), built-in print procedures, protocols (simple, sequential, and variant), record types (with field access via bracket syntax), SIZE operator, array slices (`[arr FROM n FOR m]` with slice assignment), nested PROCs/FUNCTIONs (local definitions as Go closures), MOSTNEG/MOSTPOS (type min/max constants for INT, BYTE, REAL32, REAL64), INITIAL declarations (`INITIAL INT x IS 42:` — mutable variable with initial value), checked (modular) arithmetic (`PLUS`, `MINUS`, `TIMES` — wrapping operators).
+Preprocessor (`#IF`/`#ELSE`/`#ENDIF`/`#DEFINE`/`#INCLUDE` with search paths, include guards, `#COMMENT`/`#PRAGMA`/`#USE` ignored), module file generation from SConscript (`gen-module` subcommand), SEQ, PAR, IF, WHILE, CASE, ALT (with guards, timer timeouts, and multi-statement bodies with scoped declarations), SKIP, STOP, variable/array/channel/timer declarations, abbreviations (`VAL INT x IS 42:`, `INT y IS z:`), assignments (simple and indexed), channel send/receive, channel arrays (`[n]CHAN OF TYPE` with indexed send/receive and `[]CHAN OF TYPE` proc params), PROC (with VAL, reference, CHAN, []CHAN, and open array `[]TYPE` params), channel direction restrictions (`CHAN OF INT c?` → `<-chan int`, `CHAN OF INT c!` → `chan<- int`, call-site annotations `out!`/`in?` accepted), FUNCTION (IS and VALOF forms, including multi-result `INT, INT FUNCTION` with `RESULT a, b`), multi-assignment (`a, b := func(...)` including indexed targets like `x[0], x[1] := x[1], x[0]`), KRoC-style colon terminators on PROC/FUNCTION (optional), replicators on SEQ/PAR/IF (with optional STEP), arithmetic/comparison/logical/AFTER/bitwise operators, type conversions (`INT expr`, `BYTE expr`, `REAL32 expr`, `REAL64 expr`, etc.), REAL32/REAL64 types, hex integer literals (`#FF`, `#80000000`), string literals, byte literals (`'A'`, `'*n'` with occam escape sequences), built-in print procedures, protocols (simple, sequential, and variant), record types (with field access via bracket syntax), SIZE operator, array slices (`[arr FROM n FOR m]` with slice assignment), nested PROCs/FUNCTIONs (local definitions as Go closures), MOSTNEG/MOSTPOS (type min/max constants for INT, BYTE, REAL32, REAL64), INITIAL declarations (`INITIAL INT x IS 42:` — mutable variable with initial value), checked (modular) arithmetic (`PLUS`, `MINUS`, `TIMES` — wrapping operators).
 
 ## Not Yet Implemented
 
diff --git a/TODO.md b/TODO.md
index 3a6cb24..881e129 100644
--- a/TODO.md
+++ b/TODO.md
@@ -5,10 +5,10 @@
 ### Core Constructs
 - **SEQ** — Sequential execution, with replicators (`SEQ i = 0 FOR n`) and optional STEP
 - **PAR** — Parallel execution via goroutines + sync.WaitGroup, with replicators
-- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators
-- **WHILE** — Loops, maps to Go `for` loops
-- **CASE** — Pattern matching with multiple cases and ELSE branch
-- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards and timer timeouts
+- **IF** — Multi-branch conditionals, maps to if/else if chains, with replicators; supports multi-statement bodies (declarations scoped before process)
+- **WHILE** — Loops, maps to Go `for` loops; supports multi-statement bodies
+- **CASE** — Pattern matching with multiple cases and ELSE branch; supports multi-statement bodies
+- **ALT** — Channel alternation, maps to Go `select`; supports boolean guards, timer timeouts, and multi-statement bodies
 - **SKIP** — No-op process
 - **STOP** — Error + deadlock
 
@@ -18,7 +18,7 @@
 - **Arrays** — `[n]TYPE arr:` with index expressions
 - **Channels** — `CHAN OF TYPE c:` with send (`!`) and receive (`?`); `CHAN BYTE` shorthand (without `OF`)
 - **Channel arrays** — `[n]CHAN OF TYPE cs:` with indexed send/receive and `[]CHAN OF TYPE` proc params
-- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only)
+- **Channel direction** — `CHAN OF INT c?` (receive-only) and `CHAN OF INT c!` (send-only); direction annotations at call sites (`out!`, `in?`) accepted and ignored
 - **Timers** — `TIMER tim:` with reads and `AFTER` expressions
 - **Abbreviations** — `VAL INT x IS 1:`, `INT y IS z:` — named constants and aliases
 - **INITIAL declarations** — `INITIAL INT x IS 42:` — mutable variables with initial values
diff --git a/ast/ast.go b/ast/ast.go
index 9a6ed53..370a898 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -189,7 +189,7 @@ func (f *FuncCall) TokenLiteral() string { return f.Token.Literal }
 type WhileLoop struct {
 	Token     lexer.Token // the WHILE token
 	Condition Expression
-	Body      Statement
+	Body      []Statement
 }
 
 func (w *WhileLoop) statementNode()       {}
@@ -204,7 +204,7 @@ type IfStatement struct {
 
 type IfChoice struct {
 	Condition Expression
-	Body      Statement
+	Body      []Statement
 }
 
 func (i *IfStatement) statementNode()       {}
@@ -220,7 +220,7 @@ type CaseStatement struct {
 type CaseChoice struct {
 	Values []Expression // nil/empty for ELSE
 	IsElse bool
-	Body   Statement
+	Body   []Statement
 }
 
 func (c *CaseStatement) statementNode()       {}
@@ -390,14 +390,14 @@ 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
-	ChannelIndex Expression // non-nil for cs[i] ? x in ALT
-	Variable     string     // variable to receive into
-	Body         Statement  // the body to execute
-	IsTimer      bool       // true if this is a timer AFTER case
-	Timer        string     // timer name (when IsTimer)
-	Deadline     Expression // AFTER deadline expression (when IsTimer)
+	Guard        Expression  // optional guard condition (nil if no guard)
+	Channel      string      // channel name
+	ChannelIndex Expression  // non-nil for cs[i] ? x in ALT
+	Variable     string      // variable to receive into
+	Body         []Statement // the body to execute
+	IsTimer      bool        // true if this is a timer AFTER case
+	Timer        string      // timer name (when IsTimer)
+	Deadline     Expression  // AFTER deadline expression (when IsTimer)
 }
 
 // TimerDecl represents a timer declaration: TIMER tim:
diff --git a/codegen/codegen.go b/codegen/codegen.go
index f877518..b56ba9b 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -195,8 +195,10 @@ func (g *Generator) containsPar(stmt ast.Statement) bool {
 		}
 	case *ast.AltBlock:
 		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
+				}
 			}
 		}
 	case *ast.ProcDecl:
@@ -212,19 +214,25 @@ func (g *Generator) containsPar(stmt ast.Statement) bool {
 			}
 		}
 	case *ast.WhileLoop:
-		if s.Body != nil && g.containsPar(s.Body) {
-			return true
+		for _, inner := range s.Body {
+			if g.containsPar(inner) {
+				return true
+			}
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsPar(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsPar(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.CaseStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsPar(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsPar(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.VariantReceive:
@@ -255,8 +263,10 @@ func (g *Generator) containsPrint(stmt ast.Statement) bool {
 		}
 	case *ast.AltBlock:
 		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
+				}
 			}
 		}
 	case *ast.ProcDecl:
@@ -272,19 +282,25 @@ func (g *Generator) containsPrint(stmt ast.Statement) bool {
 			}
 		}
 	case *ast.WhileLoop:
-		if s.Body != nil && g.containsPrint(s.Body) {
-			return true
+		for _, inner := range s.Body {
+			if g.containsPrint(inner) {
+				return true
+			}
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsPrint(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsPrint(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.CaseStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsPrint(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsPrint(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.VariantReceive:
@@ -306,8 +322,10 @@ func (g *Generator) containsTimer(stmt ast.Statement) bool {
 			if c.IsTimer {
 				return true
 			}
-			if c.Body != nil && g.containsTimer(c.Body) {
-				return true
+			for _, inner := range c.Body {
+				if g.containsTimer(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.SeqBlock:
@@ -335,19 +353,25 @@ func (g *Generator) containsTimer(stmt ast.Statement) bool {
 			}
 		}
 	case *ast.WhileLoop:
-		if s.Body != nil && g.containsTimer(s.Body) {
-			return true
+		for _, inner := range s.Body {
+			if g.containsTimer(inner) {
+				return true
+			}
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsTimer(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsTimer(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.CaseStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsTimer(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsTimer(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.VariantReceive:
@@ -378,8 +402,10 @@ func (g *Generator) containsStop(stmt ast.Statement) bool {
 		}
 	case *ast.AltBlock:
 		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
+				}
 			}
 		}
 	case *ast.ProcDecl:
@@ -395,19 +421,25 @@ func (g *Generator) containsStop(stmt ast.Statement) bool {
 			}
 		}
 	case *ast.WhileLoop:
-		if s.Body != nil && g.containsStop(s.Body) {
-			return true
+		for _, inner := range s.Body {
+			if g.containsStop(inner) {
+				return true
+			}
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsStop(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsStop(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.CaseStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil && g.containsStop(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsStop(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.VariantReceive:
@@ -465,16 +497,20 @@ func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
 		if g.exprNeedsMath(s.Condition) {
 			return true
 		}
-		if s.Body != nil && g.containsMostExpr(s.Body) {
-			return true
+		for _, inner := range s.Body {
+			if g.containsMostExpr(inner) {
+				return true
+			}
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
 			if g.exprNeedsMath(choice.Condition) {
 				return true
 			}
-			if choice.Body != nil && g.containsMostExpr(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsMostExpr(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.CaseStatement:
@@ -487,8 +523,10 @@ func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
 					return true
 				}
 			}
-			if choice.Body != nil && g.containsMostExpr(choice.Body) {
-				return true
+			for _, inner := range choice.Body {
+				if g.containsMostExpr(inner) {
+					return true
+				}
 			}
 		}
 	case *ast.Send:
@@ -508,8 +546,10 @@ func (g *Generator) containsMostExpr(stmt ast.Statement) bool {
 		}
 	case *ast.AltBlock:
 		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
+				}
 			}
 		}
 	case *ast.VariantReceive:
@@ -898,25 +938,25 @@ func (g *Generator) collectChanProtocols(stmt ast.Statement) {
 			g.collectChanProtocols(inner)
 		}
 	case *ast.WhileLoop:
-		if s.Body != nil {
-			g.collectChanProtocols(s.Body)
+		for _, inner := range s.Body {
+			g.collectChanProtocols(inner)
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil {
-				g.collectChanProtocols(choice.Body)
+			for _, inner := range choice.Body {
+				g.collectChanProtocols(inner)
 			}
 		}
 	case *ast.CaseStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil {
-				g.collectChanProtocols(choice.Body)
+			for _, inner := range choice.Body {
+				g.collectChanProtocols(inner)
 			}
 		}
 	case *ast.AltBlock:
 		for _, c := range s.Cases {
-			if c.Body != nil {
-				g.collectChanProtocols(c.Body)
+			for _, inner := range c.Body {
+				g.collectChanProtocols(inner)
 			}
 		}
 	}
@@ -954,25 +994,25 @@ func (g *Generator) collectRecordVars(stmt ast.Statement) {
 			g.collectRecordVars(inner)
 		}
 	case *ast.WhileLoop:
-		if s.Body != nil {
-			g.collectRecordVars(s.Body)
+		for _, inner := range s.Body {
+			g.collectRecordVars(inner)
 		}
 	case *ast.IfStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil {
-				g.collectRecordVars(choice.Body)
+			for _, inner := range choice.Body {
+				g.collectRecordVars(inner)
 			}
 		}
 	case *ast.CaseStatement:
 		for _, choice := range s.Choices {
-			if choice.Body != nil {
-				g.collectRecordVars(choice.Body)
+			for _, inner := range choice.Body {
+				g.collectRecordVars(inner)
 			}
 		}
 	case *ast.AltBlock:
 		for _, c := range s.Cases {
-			if c.Body != nil {
-				g.collectRecordVars(c.Body)
+			for _, inner := range c.Body {
+				g.collectRecordVars(inner)
 			}
 		}
 	}
@@ -1249,8 +1289,8 @@ func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
 			g.write(fmt.Sprintf("case %s = <-%s:\n", c.Variable, c.Channel))
 		}
 		g.indent++
-		if c.Body != nil {
-			g.generateStatement(c.Body)
+		for _, s := range c.Body {
+			g.generateStatement(s)
 		}
 		g.indent--
 	}
@@ -1500,8 +1540,8 @@ func (g *Generator) generateWhileLoop(loop *ast.WhileLoop) {
 	g.write(" {\n")
 	g.indent++
 
-	if loop.Body != nil {
-		g.generateStatement(loop.Body)
+	for _, s := range loop.Body {
+		g.generateStatement(s)
 	}
 
 	g.indent--
@@ -1548,8 +1588,8 @@ func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
 			g.write(" {\n")
 			g.indent++
 
-			if choice.Body != nil {
-				g.generateStatement(choice.Body)
+			for _, s := range choice.Body {
+				g.generateStatement(s)
 			}
 			g.writeLine("break")
 
@@ -1571,8 +1611,8 @@ func (g *Generator) generateIfStatement(stmt *ast.IfStatement) {
 			g.write(" {\n")
 			g.indent++
 
-			if choice.Body != nil {
-				g.generateStatement(choice.Body)
+			for _, s := range choice.Body {
+				g.generateStatement(s)
 			}
 
 			g.indent--
@@ -1602,8 +1642,8 @@ func (g *Generator) generateCaseStatement(stmt *ast.CaseStatement) {
 			g.write(":\n")
 		}
 		g.indent++
-		if choice.Body != nil {
-			g.generateStatement(choice.Body)
+		for _, s := range choice.Body {
+			g.generateStatement(s)
 		}
 		g.indent--
 	}
diff --git a/codegen/e2e_control_test.go b/codegen/e2e_control_test.go
index a380721..ccedf8a 100644
--- a/codegen/e2e_control_test.go
+++ b/codegen/e2e_control_test.go
@@ -201,3 +201,84 @@ func TestE2E_CaseExpression(t *testing.T) {
 		t.Errorf("expected %q, got %q", expected, output)
 	}
 }
+
+func TestE2E_MultiStatementIfBody(t *testing.T) {
+	occam := `SEQ
+  INT x:
+  x := 5
+  IF
+    x > 0
+      INT y:
+      y := x + 10
+      print.int(y)
+    TRUE
+      SKIP
+`
+	output := transpileCompileRun(t, occam)
+	expected := "15\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
+func TestE2E_MultiStatementCaseBody(t *testing.T) {
+	occam := `SEQ
+  INT x:
+  x := 2
+  CASE x
+    1
+      print.int(10)
+    2
+      INT y:
+      y := x * 100
+      print.int(y)
+    ELSE
+      print.int(0)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "200\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
+func TestE2E_MultiStatementWhileBody(t *testing.T) {
+	occam := `SEQ
+  INT x:
+  x := 0
+  WHILE x < 3
+    INT step:
+    step := 1
+    x := x + step
+  print.int(x)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "3\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
+
+func TestE2E_ChannelDirAtCallSite(t *testing.T) {
+	occam := `PROC worker(CHAN OF INT in?, CHAN OF INT out!)
+  INT x:
+  in ? x
+  out ! x + 1
+:
+SEQ
+  CHAN OF INT a:
+  CHAN OF INT b:
+  PAR
+    worker(a?, b!)
+    SEQ
+      a ! 10
+      INT result:
+      b ? result
+      print.int(result)
+`
+	output := transpileCompileRun(t, occam)
+	expected := "11\n"
+	if output != expected {
+		t.Errorf("expected %q, got %q", expected, output)
+	}
+}
diff --git a/parser/parser.go b/parser/parser.go
index 47e833b..3340c70 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -1635,13 +1635,7 @@ func (p *Parser) parseAltCase() *ast.AltCase {
 	p.nextToken() // consume INDENT
 	p.nextToken() // move into body
 
-	// Parse the body (first statement)
-	altCase.Body = p.parseStatement()
-
-	// Skip to end of body block
-	for !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
-		p.nextToken()
-	}
+	altCase.Body = p.parseBodyStatements()
 
 	return altCase
 }
@@ -1699,6 +1693,64 @@ func (p *Parser) parseBlockStatements() []ast.Statement {
 	return statements
 }
 
+// parseBodyStatements parses multiple statements inside a branch body
+// (IF choice, CASE choice, ALT case, WHILE). Called after the caller has
+// consumed the INDENT token and advanced into the body.
+// Returns all statements found at this indentation level.
+func (p *Parser) parseBodyStatements() []ast.Statement {
+	var statements []ast.Statement
+	startLevel := p.indentLevel
+
+	for !p.curTokenIs(lexer.EOF) {
+		// Skip newlines
+		for p.curTokenIs(lexer.NEWLINE) {
+			p.nextToken()
+		}
+
+		// Handle DEDENT tokens
+		for p.curTokenIs(lexer.DEDENT) {
+			if p.indentLevel < startLevel {
+				return statements
+			}
+			p.nextToken()
+		}
+
+		// Skip any more newlines after DEDENT
+		for p.curTokenIs(lexer.NEWLINE) {
+			p.nextToken()
+		}
+
+		if p.curTokenIs(lexer.EOF) {
+			break
+		}
+
+		if p.indentLevel < startLevel {
+			break
+		}
+
+		// Safety guard: record position before parsing to detect no-progress
+		prevToken := p.curToken
+		prevPeek := p.peekToken
+
+		stmt := p.parseStatement()
+		if stmt != nil {
+			statements = append(statements, stmt)
+		}
+
+		// 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()
+		}
+
+		// No-progress guard: if we haven't moved, break to prevent infinite loop
+		if p.curToken == prevToken && p.peekToken == prevPeek {
+			break
+		}
+	}
+
+	return statements
+}
+
 func (p *Parser) parseProcDecl() *ast.ProcDecl {
 	proc := &ast.ProcDecl{Token: p.curToken}
 
@@ -1910,11 +1962,19 @@ func (p *Parser) parseProcCall() *ast.ProcCall {
 
 	p.nextToken() // move to first arg
 	call.Args = append(call.Args, p.parseExpression(LOWEST))
+	// Consume optional channel direction annotation at call site (e.g., out!)
+	if p.peekTokenIs(lexer.SEND) || p.peekTokenIs(lexer.RECEIVE) {
+		p.nextToken()
+	}
 
 	for p.peekTokenIs(lexer.COMMA) {
 		p.nextToken() // consume comma
 		p.nextToken() // move to next arg
 		call.Args = append(call.Args, p.parseExpression(LOWEST))
+		// Consume optional channel direction annotation at call site
+		if p.peekTokenIs(lexer.SEND) || p.peekTokenIs(lexer.RECEIVE) {
+			p.nextToken()
+		}
 	}
 
 	if !p.expectPeek(lexer.RPAREN) {
@@ -2148,12 +2208,7 @@ func (p *Parser) parseWhileLoop() *ast.WhileLoop {
 	p.nextToken() // consume INDENT
 	p.nextToken() // move to first statement
 
-	loop.Body = p.parseStatement()
-
-	// Consume until DEDENT
-	for !p.curTokenIs(lexer.DEDENT) && !p.curTokenIs(lexer.EOF) {
-		p.nextToken()
-	}
+	loop.Body = p.parseBodyStatements()
 
 	return loop
 }
@@ -2225,12 +2280,7 @@ func (p *Parser) parseIfStatement() *ast.IfStatement {
 		if p.peekTokenIs(lexer.INDENT) {
 			p.nextToken() // consume INDENT
 			p.nextToken() // move to body
-			choice.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()
-			}
+			choice.Body = p.parseBodyStatements()
 		}
 
 		stmt.Choices = append(stmt.Choices, choice)
@@ -2305,12 +2355,7 @@ func (p *Parser) parseCaseStatement() *ast.CaseStatement {
 		if p.peekTokenIs(lexer.INDENT) {
 			p.nextToken() // consume INDENT
 			p.nextToken() // move to body
-			choice.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()
-			}
+			choice.Body = p.parseBodyStatements()
 		}
 
 		stmt.Choices = append(stmt.Choices, choice)
diff --git a/parser/parser_test.go b/parser/parser_test.go
index a5cd1b7..87eece9 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -371,7 +371,7 @@ func TestWhileLoop(t *testing.T) {
 		t.Error("expected condition")
 	}
 
-	if loop.Body == nil {
+	if len(loop.Body) == 0 {
 		t.Error("expected body")
 	}
 }
@@ -405,7 +405,7 @@ func TestIfStatement(t *testing.T) {
 		t.Error("expected condition on first choice")
 	}
 
-	if ifStmt.Choices[0].Body == nil {
+	if len(ifStmt.Choices[0].Body) == 0 {
 		t.Error("expected body on first choice")
 	}
 
@@ -413,7 +413,7 @@ func TestIfStatement(t *testing.T) {
 		t.Error("expected condition on second choice")
 	}
 
-	if ifStmt.Choices[1].Body == nil {
+	if len(ifStmt.Choices[1].Body) == 0 {
 		t.Error("expected body on second choice")
 	}
 }
@@ -577,7 +577,7 @@ func TestReplicatedIf(t *testing.T) {
 		t.Error("expected condition on choice")
 	}
 
-	if ifStmt.Choices[0].Body == nil {
+	if len(ifStmt.Choices[0].Body) == 0 {
 		t.Error("expected body on choice")
 	}
 }
@@ -1017,7 +1017,7 @@ func TestCaseStatement(t *testing.T) {
 	if len(caseStmt.Choices[0].Values) != 1 {
 		t.Fatalf("expected 1 value in first choice, got %d", len(caseStmt.Choices[0].Values))
 	}
-	if caseStmt.Choices[0].Body == nil {
+	if len(caseStmt.Choices[0].Body) == 0 {
 		t.Error("expected body on first choice")
 	}
 
@@ -1025,7 +1025,7 @@ func TestCaseStatement(t *testing.T) {
 	if caseStmt.Choices[1].IsElse {
 		t.Error("second choice should not be ELSE")
 	}
-	if caseStmt.Choices[1].Body == nil {
+	if len(caseStmt.Choices[1].Body) == 0 {
 		t.Error("expected body on second choice")
 	}
 
@@ -1033,7 +1033,7 @@ func TestCaseStatement(t *testing.T) {
 	if !caseStmt.Choices[2].IsElse {
 		t.Error("third choice should be ELSE")
 	}
-	if caseStmt.Choices[2].Body == nil {
+	if len(caseStmt.Choices[2].Body) == 0 {
 		t.Error("expected body on ELSE choice")
 	}
 }
@@ -2781,3 +2781,95 @@ func TestCheckedAndSymbolMixed(t *testing.T) {
 		t.Errorf("expected top-level operator +, got %s", binExpr.Operator)
 	}
 }
+
+func TestMultiStatementIfBody(t *testing.T) {
+	input := `IF
+  x > 0
+    INT y:
+    y := 42
+    print.int(y)
+  TRUE
+    SKIP
+`
+	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))
+	}
+
+	ifStmt, ok := program.Statements[0].(*ast.IfStatement)
+	if !ok {
+		t.Fatalf("expected IfStatement, got %T", program.Statements[0])
+	}
+
+	if len(ifStmt.Choices) != 2 {
+		t.Fatalf("expected 2 choices, got %d", len(ifStmt.Choices))
+	}
+
+	// First choice should have 3 body statements: VarDecl, Assignment, ProcCall
+	if len(ifStmt.Choices[0].Body) != 3 {
+		t.Fatalf("expected 3 body statements in first choice, got %d", len(ifStmt.Choices[0].Body))
+	}
+	if _, ok := ifStmt.Choices[0].Body[0].(*ast.VarDecl); !ok {
+		t.Errorf("expected VarDecl as first body stmt, got %T", ifStmt.Choices[0].Body[0])
+	}
+	if _, ok := ifStmt.Choices[0].Body[1].(*ast.Assignment); !ok {
+		t.Errorf("expected Assignment as second body stmt, got %T", ifStmt.Choices[0].Body[1])
+	}
+	if _, ok := ifStmt.Choices[0].Body[2].(*ast.ProcCall); !ok {
+		t.Errorf("expected ProcCall as third body stmt, got %T", ifStmt.Choices[0].Body[2])
+	}
+
+	// Second choice should have 1 body statement: Skip
+	if len(ifStmt.Choices[1].Body) != 1 {
+		t.Fatalf("expected 1 body statement in second choice, got %d", len(ifStmt.Choices[1].Body))
+	}
+	if _, ok := ifStmt.Choices[1].Body[0].(*ast.Skip); !ok {
+		t.Errorf("expected Skip, got %T", ifStmt.Choices[1].Body[0])
+	}
+}
+
+func TestChannelDirAtCallSite(t *testing.T) {
+	input := `foo(out!, in?)
+`
+	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))
+	}
+
+	call, ok := program.Statements[0].(*ast.ProcCall)
+	if !ok {
+		t.Fatalf("expected ProcCall, got %T", program.Statements[0])
+	}
+
+	if call.Name != "foo" {
+		t.Errorf("expected proc name 'foo', got %q", call.Name)
+	}
+
+	if len(call.Args) != 2 {
+		t.Fatalf("expected 2 args, got %d", len(call.Args))
+	}
+
+	arg0, ok := call.Args[0].(*ast.Identifier)
+	if !ok {
+		t.Fatalf("expected Identifier for arg 0, got %T", call.Args[0])
+	}
+	if arg0.Value != "out" {
+		t.Errorf("expected arg 0 = 'out', got %q", arg0.Value)
+	}
+
+	arg1, ok := call.Args[1].(*ast.Identifier)
+	if !ok {
+		t.Fatalf("expected Identifier for arg 1, got %T", call.Args[1])
+	}
+	if arg1.Value != "in" {
+		t.Errorf("expected arg 1 = 'in', got %q", arg1.Value)
+	}
+}
$ Bash
Show recent commits
git log --oneline -5
5cbfc5a Merge pull request #22 from codeassociates/docs/consolidate-todo-implemented
461e4a2 Remove completed course.module section from TODO.md
c058eec Merge pull request #21 from codeassociates/feature/checked-arithmetic
99d0442 Add checked (modular) arithmetic support (PLUS, MINUS, TIMES)
af7e09d Merge pull request #20 from codeassociates/feature/initial-declarations

Continue from where you left off.

No response requested.

The host machine's kernel becams unstable. I've resumed your session. Please continue.

Right, let's commit the current working changes. All tests were passing before the crash.

$ 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:   CLAUDE.md
	modified:   TODO.md
	modified:   ast/ast.go
	modified:   codegen/codegen.go
	modified:   codegen/e2e_control_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 recent commits
git log --oneline -3
5cbfc5a Merge pull request #22 from codeassociates/docs/consolidate-todo-implemented
461e4a2 Remove completed course.module section from TODO.md
c058eec Merge pull request #21 from codeassociates/feature/checked-arithmetic

Let me quickly verify tests still pass after the crash, then commit.

$ 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	(cached)
ok  	github.com/codeassociates/occam2go/lexer	(cached)
ok  	github.com/codeassociates/occam2go/modgen	(cached)
ok  	github.com/codeassociates/occam2go/parser	(cached)
ok  	github.com/codeassociates/occam2go/preproc	(cached)

All passing. Let me commit.

$ Bash
Stage all changed files
git add ast/ast.go parser/parser.go parser/parser_test.go codegen/codegen.go codegen/e2e_control_test.go CLAUDE.md TODO.md
$ Bash
Create commit
git commit -m "$(cat <<'EOF'
Support multi-statement bodies in IF/CASE/WHILE/ALT and call-site channel direction

The parser previously only called parseStatement() once per branch body in
IF/CASE/WHILE/ALT, causing an infinite loop (OOM) when occam programs had
scoped declarations before processes in branch bodies. This changes Body
fields from ast.Statement to []ast.Statement and adds parseBodyStatements()
to parse all statements at the body's indentation level with a no-progress
safety guard.

Also handles channel direction annotations at call sites (out!, in?) in
parseProcCall(), which are informational and ignored in codegen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
EOF
)"
$ Bash
Verify clean working tree
git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean
TaskUpdate
{
  "taskId": "5",
  "status": "completed"
}
Updated task #5 status

Committed as b088adc. Working tree is clean. The remaining infinite loop when transpiling the course library demo is a separate issue — likely an unsupported construct in the course library source that causes the parser to spin. That can be investigated safely now without risking loss of these fixes.