Implement the following plan:
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.
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)
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
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.
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.
.Body (change nil checks to len() checks, access Body[0] instead of Body)Update TODO.md to note these features are supported.
go test ./... to verifygo 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
Let me start by understanding the current codebase state and then implement the changes in order. Let me read the key files first.
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→ 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→ 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→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:
Body from Statement to []Statement in WhileLoop, IfChoice, CaseChoice, AltCaseparseBodyStatements() and update call sites, handle channel direction at call sitesLet me also check the test files to understand what existing tests look like.
{
"subject": "Change Body fields to []Statement in AST",
"activeForm": "Updating AST types"
}{
"subject": "Add parseBodyStatements() and update parser call sites",
"activeForm": "Updating parser"
}{
"subject": "Update codegen for body slices",
"activeForm": "Updating codegen"
}{
"subject": "Update tests and add new tests",
"activeForm": "Updating tests"
}{
"subject": "Run tests and verify",
"activeForm": "Running tests"
}// 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.
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.
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.
// 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.
First, add the parseBodyStatements() helper after parseBlockStatements():
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.
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.
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.
// 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 parsingThe file /home/david/projects/code-associates/occam2go/parser/parser.go has been updated successfully.
// 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.
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.
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:
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.
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.
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.
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.
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.
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) containsTimerThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
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) containsStopThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
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) containsMostExprThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
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.
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) exprNeedsMathThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
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) collectRecordVarsThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
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) generateRecordDeclThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
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]){
"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→{
"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)){
"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 { 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.
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.
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.
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.
{
"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→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.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_test.go",
"offset": 1,
"limit": 30
}/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
{
"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→{
"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{
"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→{
"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→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.
? 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)
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→- **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.
- **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.
| `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>
{
"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++` || `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.
{
"pattern": "What's Implemented",
"path": "/home/david/projects/code-associates/occam2go/CLAUDE.md",
"output_mode": "content"
}{
"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→
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.
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.
? 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)
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:
ls /home/david/projects/code-associates/occam2go/kroc/modules/course/libsrc/SConscript 2>&1
./occam2go gen-module -o kroc/modules/course/libsrc/course.module kroc/modules/course/libsrc/SConscript 2>&1
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
./occam2go -I kroc/modules/course/libsrc kroc/demos/complexity/concurrent_hello_1.occ 2>&1
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.
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")
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)
+ }
+}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
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")
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
? 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)
git add ast/ast.go parser/parser.go parser/parser_test.go codegen/codegen.go codegen/e2e_control_test.go CLAUDE.md TODO.md
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 )"
7 files changed, 374 insertions(+), 115 deletions(-)
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
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.