Implement the following plan:
The Game of Life example (historical-examples/life.occ) compiles and runs, but the editor mode doesn't visibly work (issue #72, latest comment). Two bugs:
Screen output never flushes in editor mode: The screen goroutine uses bufio.NewWriter (4096-byte buffer) and only flushes on byte 255 (never sent) or buffer full. In edit mode, cursor movements produce ~8-24 bytes per keypress — far too few to trigger a flush. "Idle" is visible only because the busy-wait loop writes it thousands of times.
PRI ALT guarded SKIP causes busy-wait: The controller's PRI ALT has (activity <> idle) & SKIP. When idle, this guard is false, so the ALT should block on the keyboard channel. But the Go translation uses unconditional default: in select, which always fires — creating a 100% CPU spin loop.
File: codegen/codegen.go (lines 483-502, screen writer goroutine)
After w.WriteByte(b) (line 495), add:
if len(screen) == 0 {
w.Flush()
}
Apply the same fix to the error writer goroutine (line 521, after the error WriteByte).
This flushes after each burst of output while still batching rapid sequential writes.
File: codegen/codegen.go, generateAltBlock() (lines 1947-2033)
When an ALT has a guarded SKIP case (c.IsSkip && c.Guard != nil), generate two select blocks instead of one:
_altSkipReady := <guard expression>
if _altSkipReady {
select {
case x = <-_alt0:
...channel body...
default:
...skip body...
}
} else {
select {
case x = <-_alt0:
...channel body...
}
}
When guard is true: default fires if no channel ready (correct PRI ALT semantics).
When guard is false: no default, select blocks on channels (correct blocking).
Extract helper method generateAltChannelCase(i int, c ast.AltCase) — generates a single channel case label + body (the code currently at lines 1989-2013 + 2014-2030, minus the SKIP handling). This avoids duplicating the channel case generation code across both select blocks.
In generateAltBlock(), detect guarded SKIP:
go
guardedSkipIdx := -1
for i, c := range alt.Cases {
if c.IsSkip && c.Guard != nil {
guardedSkipIdx = i
break
}
}
If guardedSkipIdx >= 0: generate _altSkipReady variable, then the if/else with two selects (using helper for channel cases). The SKIP body no longer needs the inner if guard { } wrapper since the guard is checked in the outer if.
If guardedSkipIdx < 0: existing codegen unchanged (unguarded SKIP → default:, no SKIP → no default).
Add TestE2E_AltGuardedSkipFalseBlocking in codegen/e2e_misc_test.go — verify that when SKIP guard is false, the ALT blocks on the channel case and correctly receives.
TestE2E_AltGuardedSkipFalse (line 263) is currently racy — it passes by luck because Go's scheduler runs the sender goroutine before the receiver reaches the select. With the fix, the blocking select correctly waits for channel data.
go test ./... # all tests pass
go test ./codegen -run TestE2E_Alt # ALT-specific tests
./occam2go -o /tmp/life.go historical-examples/life.occ
go vet /tmp/life.go # compiles cleanly
go run /tmp/life.go # manual test: e→arrows→*→q→r works
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/218201bc-e51d-4c80-a4dc-65311974873c.jsonl
Let me start by understanding the current codebase state. I need to read the relevant files to implement the plan.
I'll start by reading the relevant files to understand the current code before making changes.
{
"subject": "Fix screen output auto-flush on channel drain",
"activeForm": "Fixing screen output flush"
}{
"subject": "Implement guarded SKIP dual-select pattern in ALT",
"activeForm": "Implementing guarded SKIP dual-select"
}{
"subject": "Add E2E test for guarded SKIP blocking",
"activeForm": "Adding ALT guarded SKIP tests"
}{
"subject": "Run tests and verify life.occ",
"activeForm": "Running tests and verification"
}{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 475,
"limit": 60
} 475→ g.writeLine("var wg sync.WaitGroup")
476→ g.writeLine("wg.Add(2)")
477→ g.writeLine("")
478→
479→ // Screen writer goroutine — in raw mode, insert CR before LF
480→ g.writeLine("go func() {")
481→ g.indent++
482→ g.writeLine("defer wg.Done()")
483→ g.writeLine("w := bufio.NewWriter(os.Stdout)")
484→ g.writeLine("for b := range screen {")
485→ g.indent++
486→ g.writeLine("if b == 255 {")
487→ g.indent++
488→ g.writeLine("w.Flush()")
489→ g.indent--
490→ g.writeLine("} else {")
491→ g.indent++
492→ g.writeLine(`if rawMode && b == '\n' {`)
493→ g.writeLine(`w.WriteByte('\r')`)
494→ g.writeLine("}")
495→ g.writeLine("w.WriteByte(b)")
496→ g.indent--
497→ g.writeLine("}")
498→ g.indent--
499→ g.writeLine("}")
500→ g.writeLine("w.Flush()")
501→ g.indent--
502→ g.writeLine("}()")
503→ g.writeLine("")
504→
505→ // Error writer goroutine — same CR/LF handling
506→ g.writeLine("go func() {")
507→ g.indent++
508→ g.writeLine("defer wg.Done()")
509→ g.writeLine("w := bufio.NewWriter(os.Stderr)")
510→ g.writeLine("for b := range _error {")
511→ g.indent++
512→ g.writeLine("if b == 255 {")
513→ g.indent++
514→ g.writeLine("w.Flush()")
515→ g.indent--
516→ g.writeLine("} else {")
517→ g.indent++
518→ g.writeLine(`if rawMode && b == '\n' {`)
519→ g.writeLine(`w.WriteByte('\r')`)
520→ g.writeLine("}")
521→ g.writeLine("w.WriteByte(b)")
522→ g.indent--
523→ g.writeLine("}")
524→ g.indent--
525→ g.writeLine("}")
526→ g.writeLine("w.Flush()")
527→ g.indent--
528→ g.writeLine("}()")
529→ g.writeLine("")
530→
531→ // Keyboard reader goroutine
532→ g.writeLine("go func() {")
533→ g.indent++
534→ g.writeLine("if rawMode {"){
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1940,
"limit": 120
} 1940→ g.writeLine("}()")
1941→ }
1942→
1943→ g.writeLine("wg.Wait()")
1944→ }
1945→}
1946→
1947→func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
1948→ if alt.Replicator != nil {
1949→ g.generateReplicatedAlt(alt)
1950→ return
1951→ }
1952→
1953→ // ALT becomes Go select statement
1954→ // For guards, we use a pattern with nil channels
1955→
1956→ // Check if any cases have guards
1957→ hasGuards := false
1958→ for _, c := range alt.Cases {
1959→ if c.Guard != nil {
1960→ hasGuards = true
1961→ break
1962→ }
1963→ }
1964→
1965→ if hasGuards {
1966→ // Generate channel variables for guarded cases
1967→ for i, c := range alt.Cases {
1968→ if c.Guard != nil && !c.IsSkip {
1969→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1970→ // Look up the channel's element type
1971→ elemType := "int" // default fallback
1972→ if t, ok := g.chanElemTypes[c.Channel]; ok {
1973→ elemType = t
1974→ }
1975→ g.write(fmt.Sprintf("var _alt%d <-chan %s = nil\n", i, elemType))
1976→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1977→ g.write(fmt.Sprintf("if "))
1978→ g.generateExpression(c.Guard)
1979→ g.write(fmt.Sprintf(" { _alt%d = %s }\n", i, goIdent(c.Channel)))
1980→ }
1981→ }
1982→ }
1983→
1984→ g.writeLine("select {")
1985→ for i, c := range alt.Cases {
1986→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1987→ if c.IsSkip {
1988→ g.write("default:\n")
1989→ } else if c.IsTimer {
1990→ g.write("case <-time.After(time.Duration(")
1991→ g.generateExpression(c.Deadline)
1992→ g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
1993→ } else if c.Guard != nil {
1994→ varRef := goIdent(c.Variable)
1995→ if len(c.VariableIndices) > 0 {
1996→ varRef += g.generateIndicesStr(c.VariableIndices)
1997→ }
1998→ g.write(fmt.Sprintf("case %s = <-_alt%d:\n", varRef, i))
1999→ } else if len(c.ChannelIndices) > 0 {
2000→ varRef := goIdent(c.Variable)
2001→ if len(c.VariableIndices) > 0 {
2002→ varRef += g.generateIndicesStr(c.VariableIndices)
2003→ }
2004→ g.write(fmt.Sprintf("case %s = <-%s", varRef, goIdent(c.Channel)))
2005→ g.generateIndices(c.ChannelIndices)
2006→ g.write(":\n")
2007→ } else {
2008→ varRef := goIdent(c.Variable)
2009→ if len(c.VariableIndices) > 0 {
2010→ varRef += g.generateIndicesStr(c.VariableIndices)
2011→ }
2012→ g.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel)))
2013→ }
2014→ g.indent++
2015→ guardedSkip := c.IsSkip && c.Guard != nil
2016→ if guardedSkip {
2017→ g.builder.WriteString(strings.Repeat("\t", g.indent))
2018→ g.write("if ")
2019→ g.generateExpression(c.Guard)
2020→ g.write(" {\n")
2021→ g.indent++
2022→ }
2023→ for _, s := range c.Body {
2024→ g.generateStatement(s)
2025→ }
2026→ if guardedSkip {
2027→ g.indent--
2028→ g.writeLine("}")
2029→ }
2030→ g.indent--
2031→ }
2032→ g.writeLine("}")
2033→}
2034→
2035→func (g *Generator) generateReplicatedAlt(alt *ast.AltBlock) {
2036→ // Replicated ALT: ALT i = start FOR count
2037→ // Uses reflect.Select for runtime-variable case count
2038→ if len(alt.Cases) == 0 {
2039→ return
2040→ }
2041→ c := alt.Cases[0]
2042→ rep := alt.Replicator
2043→ v := goIdent(rep.Variable)
2044→
2045→ // Determine receive type from scoped declarations
2046→ recvType := "int" // default
2047→ for _, decl := range c.Declarations {
2048→ if vd, ok := decl.(*ast.VarDecl); ok {
2049→ for _, name := range vd.Names {
2050→ if name == c.Variable {
2051→ recvType = g.occamTypeToGo(vd.Type)
2052→ break
2053→ }
2054→ }
2055→ }
2056→ }
2057→
2058→ // Open a block for scoping
2059→ g.writeLine("{"){
"pattern": "AltGuardedSkip",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"output_mode": "files_with_matches"
}{
"pattern": "AltGuardedSkip",
"path": "/home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go",
"output_mode": "content",
"-n": true
}212:func TestE2E_AltGuardedSkip(t *testing.T) {
236:func TestE2E_AltGuardedSkipTrue(t *testing.T) {
263:func TestE2E_AltGuardedSkipFalse(t *testing.T) {{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go",
"offset": 212,
"limit": 100
} 212→func TestE2E_AltGuardedSkip(t *testing.T) {
213→ // Issue #78: guard & SKIP in ALT (always-ready alternative)
214→ occam := `SEQ
215→ CHAN OF INT c:
216→ INT result:
217→ BOOL ready:
218→ ready := TRUE
219→ result := 0
220→ PAR
221→ SEQ
222→ ALT
223→ ready & SKIP
224→ SKIP
225→ c ! 42
226→ c ? result
227→ print.int(result)
228→`
229→ output := transpileCompileRun(t, occam)
230→ expected := "42\n"
231→ if output != expected {
232→ t.Errorf("expected %q, got %q", expected, output)
233→ }
234→}
235→
236→func TestE2E_AltGuardedSkipTrue(t *testing.T) {
237→ // Issue #77: ALT with channel case and guarded SKIP where guard is TRUE
238→ // The SKIP fires immediately, then the channel send proceeds
239→ occam := `SEQ
240→ CHAN OF INT c:
241→ INT result:
242→ BOOL ready:
243→ ready := TRUE
244→ result := 0
245→ PAR
246→ SEQ
247→ ALT
248→ ready & SKIP
249→ result := 99
250→ c ? result
251→ SKIP
252→ c ! 42
253→ c ? result
254→ print.int(result)
255→`
256→ output := transpileCompileRun(t, occam)
257→ expected := "42\n"
258→ if output != expected {
259→ t.Errorf("expected %q, got %q", expected, output)
260→ }
261→}
262→
263→func TestE2E_AltGuardedSkipFalse(t *testing.T) {
264→ // Issue #77: ALT with channel case and guarded SKIP where guard is FALSE
265→ // The SKIP guard is false, so the channel case fires
266→ occam := `SEQ
267→ CHAN OF INT c:
268→ INT result:
269→ BOOL ready:
270→ ready := FALSE
271→ result := 0
272→ PAR
273→ SEQ
274→ ALT
275→ ready & SKIP
276→ result := 99
277→ c ? result
278→ SKIP
279→ c ! 77
280→ print.int(result)
281→`
282→ output := transpileCompileRun(t, occam)
283→ expected := "77\n"
284→ if output != expected {
285→ t.Errorf("expected %q, got %q", expected, output)
286→ }
287→}
288→
289→func TestE2E_MultiLineAbbreviation(t *testing.T) {
290→ // Issue #79: IS at end of line as continuation
291→ occam := `SEQ
292→ VAL INT x IS
293→ 42 :
294→ print.int(x)
295→`
296→ output := transpileCompileRun(t, occam)
297→ expected := "42\n"
298→ if output != expected {
299→ t.Errorf("expected %q, got %q", expected, output)
300→ }
301→}
302→
303→func TestE2E_MultiLineAbbreviationExpr(t *testing.T) {
304→ // Issue #79: IS continuation with complex expression
305→ occam := `SEQ
306→ VAL INT a IS 10 :
307→ VAL INT b IS
308→ (a + 5) :
309→ print.int(b)
310→`
311→ output := transpileCompileRun(t, occam){
"pattern": "type AltCase struct|type AltBlock struct",
"path": "/home/david/projects/code-associates/occam2go/ast/ast.go",
"output_mode": "content",
"-n": true,
"-A": 20
}388:type AltBlock struct {
389- Token lexer.Token // the ALT token
390- Cases []AltCase
391- Replicator *Replicator // optional replicator
392- Priority bool // true for PRI ALT
393-}
394-
395-func (a *AltBlock) statementNode() {}
396-func (a *AltBlock) TokenLiteral() string { return a.Token.Literal }
397-
398-// AltCase represents a single case in an ALT block
399:type AltCase struct {
400- Guard Expression // optional guard condition (nil if no guard)
401- Channel string // channel name
402- ChannelIndices []Expression // non-empty for cs[i] ? x or cs[i][j] ? x in ALT
403- Variable string // variable to receive into
404- VariableIndices []Expression // non-empty for c ? flags[0] or c ? grid[i][j]
405- Body []Statement // the body to execute
406- IsTimer bool // true if this is a timer AFTER case
407- IsSkip bool // true if this is a guarded SKIP case (guard & SKIP)
408- Timer string // timer name (when IsTimer)
409- Deadline Expression // AFTER deadline expression (when IsTimer)
410- Declarations []Statement / scoped declarations before channel input (e.g., BYTE ch:)
411-}
412-
413-/ TimerDecl represents a timer declaration: TIMER tim:
414-type TimerDecl struct {
415- Token lexer.Token // the TIMER token
416- Names []string // timer variable names
417-}
418-
419-func (td *TimerDecl) statementNode() {} g.writeLine("w.WriteByte(b)")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.writeLine("w.Flush()")
g.indent--
g.writeLine("}()")
g.writeLine("")
// Error writer goroutine — same CR/LF handling g.writeLine("w.WriteByte(b)")
g.writeLine("if len(screen) == 0 {")
g.indent++
g.writeLine("w.Flush()")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.writeLine("w.Flush()")
g.indent--
g.writeLine("}()")
g.writeLine("")
// Error writer goroutine — same CR/LF handlingThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
g.writeLine(`w.WriteByte('\r')`)
g.writeLine("}")
g.writeLine("w.WriteByte(b)")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.writeLine("w.Flush()")
g.indent--
g.writeLine("}()")
g.writeLine("")
// Keyboard reader goroutine g.writeLine(`w.WriteByte('\r')`)
g.writeLine("}")
g.writeLine("w.WriteByte(b)")
g.writeLine("if len(_error) == 0 {")
g.indent++
g.writeLine("w.Flush()")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.writeLine("w.Flush()")
g.indent--
g.writeLine("}()")
g.writeLine("")
// Keyboard reader goroutineThe file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
Now I need to refactor generateAltBlock(). Let me re-read the full function to plan the exact edit:
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 1947,
"limit": 95
} 1947→ g.writeLine("defer wg.Done()")
1948→ g.generateStatement(stmt)
1949→ g.indent--
1950→ g.writeLine("}()")
1951→ }
1952→
1953→ g.writeLine("wg.Wait()")
1954→ }
1955→}
1956→
1957→func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
1958→ if alt.Replicator != nil {
1959→ g.generateReplicatedAlt(alt)
1960→ return
1961→ }
1962→
1963→ // ALT becomes Go select statement
1964→ // For guards, we use a pattern with nil channels
1965→
1966→ // Check if any cases have guards
1967→ hasGuards := false
1968→ for _, c := range alt.Cases {
1969→ if c.Guard != nil {
1970→ hasGuards = true
1971→ break
1972→ }
1973→ }
1974→
1975→ if hasGuards {
1976→ // Generate channel variables for guarded cases
1977→ for i, c := range alt.Cases {
1978→ if c.Guard != nil && !c.IsSkip {
1979→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1980→ // Look up the channel's element type
1981→ elemType := "int" // default fallback
1982→ if t, ok := g.chanElemTypes[c.Channel]; ok {
1983→ elemType = t
1984→ }
1985→ g.write(fmt.Sprintf("var _alt%d <-chan %s = nil\n", i, elemType))
1986→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1987→ g.write(fmt.Sprintf("if "))
1988→ g.generateExpression(c.Guard)
1989→ g.write(fmt.Sprintf(" { _alt%d = %s }\n", i, goIdent(c.Channel)))
1990→ }
1991→ }
1992→ }
1993→
1994→ g.writeLine("select {")
1995→ for i, c := range alt.Cases {
1996→ g.builder.WriteString(strings.Repeat("\t", g.indent))
1997→ if c.IsSkip {
1998→ g.write("default:\n")
1999→ } else if c.IsTimer {
2000→ g.write("case <-time.After(time.Duration(")
2001→ g.generateExpression(c.Deadline)
2002→ g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
2003→ } else if c.Guard != nil {
2004→ varRef := goIdent(c.Variable)
2005→ if len(c.VariableIndices) > 0 {
2006→ varRef += g.generateIndicesStr(c.VariableIndices)
2007→ }
2008→ g.write(fmt.Sprintf("case %s = <-_alt%d:\n", varRef, i))
2009→ } else if len(c.ChannelIndices) > 0 {
2010→ varRef := goIdent(c.Variable)
2011→ if len(c.VariableIndices) > 0 {
2012→ varRef += g.generateIndicesStr(c.VariableIndices)
2013→ }
2014→ g.write(fmt.Sprintf("case %s = <-%s", varRef, goIdent(c.Channel)))
2015→ g.generateIndices(c.ChannelIndices)
2016→ g.write(":\n")
2017→ } else {
2018→ varRef := goIdent(c.Variable)
2019→ if len(c.VariableIndices) > 0 {
2020→ varRef += g.generateIndicesStr(c.VariableIndices)
2021→ }
2022→ g.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel)))
2023→ }
2024→ g.indent++
2025→ guardedSkip := c.IsSkip && c.Guard != nil
2026→ if guardedSkip {
2027→ g.builder.WriteString(strings.Repeat("\t", g.indent))
2028→ g.write("if ")
2029→ g.generateExpression(c.Guard)
2030→ g.write(" {\n")
2031→ g.indent++
2032→ }
2033→ for _, s := range c.Body {
2034→ g.generateStatement(s)
2035→ }
2036→ if guardedSkip {
2037→ g.indent--
2038→ g.writeLine("}")
2039→ }
2040→ g.indent--
2041→ }Now I'll replace the entire generateAltBlock function with the new dual-select implementation:
func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
if alt.Replicator != nil {
g.generateReplicatedAlt(alt)
return
}
// ALT becomes Go select statement
// For guards, we use a pattern with nil channels
// Check if any cases have guards
hasGuards := false
for _, c := range alt.Cases {
if c.Guard != nil {
hasGuards = true
break
}
}
if hasGuards {
// Generate channel variables for guarded cases
for i, c := range alt.Cases {
if c.Guard != nil && !c.IsSkip {
g.builder.WriteString(strings.Repeat("\t", g.indent))
// Look up the channel's element type
elemType := "int" // default fallback
if t, ok := g.chanElemTypes[c.Channel]; ok {
elemType = t
}
g.write(fmt.Sprintf("var _alt%d <-chan %s = nil\n", i, elemType))
g.builder.WriteString(strings.Repeat("\t", g.indent))
g.write(fmt.Sprintf("if "))
g.generateExpression(c.Guard)
g.write(fmt.Sprintf(" { _alt%d = %s }\n", i, goIdent(c.Channel)))
}
}
}
g.writeLine("select {")
for i, c := range alt.Cases {
g.builder.WriteString(strings.Repeat("\t", g.indent))
if c.IsSkip {
g.write("default:\n")
} else if c.IsTimer {
g.write("case <-time.After(time.Duration(")
g.generateExpression(c.Deadline)
g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
} else if c.Guard != nil {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-_alt%d:\n", varRef, i))
} else if len(c.ChannelIndices) > 0 {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-%s", varRef, goIdent(c.Channel)))
g.generateIndices(c.ChannelIndices)
g.write(":\n")
} else {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel)))
}
g.indent++
guardedSkip := c.IsSkip && c.Guard != nil
if guardedSkip {
g.builder.WriteString(strings.Repeat("\t", g.indent))
g.write("if ")
g.generateExpression(c.Guard)
g.write(" {\n")
g.indent++
}
for _, s := range c.Body {
g.generateStatement(s)
}
if guardedSkip {
g.indent--
g.writeLine("}")
}
g.indent--
}
g.writeLine("}")
}func (g *Generator) generateAltBlock(alt *ast.AltBlock) {
if alt.Replicator != nil {
g.generateReplicatedAlt(alt)
return
}
// ALT becomes Go select statement
// For guards, we use a pattern with nil channels
// Check if any cases have guards
hasGuards := false
for _, c := range alt.Cases {
if c.Guard != nil {
hasGuards = true
break
}
}
// Detect guarded SKIP case — needs dual-select pattern to avoid busy-wait
guardedSkipIdx := -1
for i, c := range alt.Cases {
if c.IsSkip && c.Guard != nil {
guardedSkipIdx = i
break
}
}
if hasGuards {
// Generate channel variables for guarded cases
for i, c := range alt.Cases {
if c.Guard != nil && !c.IsSkip {
g.builder.WriteString(strings.Repeat("\t", g.indent))
// Look up the channel's element type
elemType := "int" // default fallback
if t, ok := g.chanElemTypes[c.Channel]; ok {
elemType = t
}
g.write(fmt.Sprintf("var _alt%d <-chan %s = nil\n", i, elemType))
g.builder.WriteString(strings.Repeat("\t", g.indent))
g.write(fmt.Sprintf("if "))
g.generateExpression(c.Guard)
g.write(fmt.Sprintf(" { _alt%d = %s }\n", i, goIdent(c.Channel)))
}
}
}
if guardedSkipIdx >= 0 {
// Dual-select pattern: when guard is true, use default (non-blocking);
// when guard is false, omit default (blocking on channels).
g.builder.WriteString(strings.Repeat("\t", g.indent))
g.write("_altSkipReady := ")
g.generateExpression(alt.Cases[guardedSkipIdx].Guard)
g.write("\n")
// if _altSkipReady { select with default }
g.writeLine("if _altSkipReady {")
g.indent++
g.writeLine("select {")
for i, c := range alt.Cases {
if c.IsSkip {
g.builder.WriteString(strings.Repeat("\t", g.indent))
g.write("default:\n")
g.indent++
for _, s := range c.Body {
g.generateStatement(s)
}
g.indent--
} else {
g.generateAltChannelCase(i, c)
}
}
g.writeLine("}")
g.indent--
// else { select without default — blocks on channels }
g.writeLine("} else {")
g.indent++
g.writeLine("select {")
for i, c := range alt.Cases {
if !c.IsSkip {
g.generateAltChannelCase(i, c)
}
}
g.writeLine("}")
g.indent--
g.writeLine("}")
} else {
// Standard single-select pattern
g.writeLine("select {")
for i, c := range alt.Cases {
if c.IsSkip {
g.builder.WriteString(strings.Repeat("\t", g.indent))
g.write("default:\n")
g.indent++
for _, s := range c.Body {
g.generateStatement(s)
}
g.indent--
} else {
g.generateAltChannelCase(i, c)
}
}
g.writeLine("}")
}
}
// generateAltChannelCase generates a single channel or timer case for a select block.
func (g *Generator) generateAltChannelCase(i int, c ast.AltCase) {
g.builder.WriteString(strings.Repeat("\t", g.indent))
if c.IsTimer {
g.write("case <-time.After(time.Duration(")
g.generateExpression(c.Deadline)
g.write(" - int(time.Now().UnixMicro())) * time.Microsecond):\n")
} else if c.Guard != nil {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-_alt%d:\n", varRef, i))
} else if len(c.ChannelIndices) > 0 {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-%s", varRef, goIdent(c.Channel)))
g.generateIndices(c.ChannelIndices)
g.write(":\n")
} else {
varRef := goIdent(c.Variable)
if len(c.VariableIndices) > 0 {
varRef += g.generateIndicesStr(c.VariableIndices)
}
g.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel)))
}
g.indent++
for _, s := range c.Body {
g.generateStatement(s)
}
g.indent--
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go",
"offset": 263,
"limit": 30
} 263→func TestE2E_AltGuardedSkipFalse(t *testing.T) {
264→ // Issue #77: ALT with channel case and guarded SKIP where guard is FALSE
265→ // The SKIP guard is false, so the channel case fires
266→ occam := `SEQ
267→ CHAN OF INT c:
268→ INT result:
269→ BOOL ready:
270→ ready := FALSE
271→ result := 0
272→ PAR
273→ SEQ
274→ ALT
275→ ready & SKIP
276→ result := 99
277→ c ? result
278→ SKIP
279→ c ! 77
280→ print.int(result)
281→`
282→ output := transpileCompileRun(t, occam)
283→ expected := "77\n"
284→ if output != expected {
285→ t.Errorf("expected %q, got %q", expected, output)
286→ }
287→}
288→
289→func TestE2E_MultiLineAbbreviation(t *testing.T) {
290→ // Issue #79: IS at end of line as continuation
291→ occam := `SEQ
292→ VAL INT x ISfunc TestE2E_MultiLineAbbreviation(t *testing.T) {func TestE2E_AltGuardedSkipFalseBlocking(t *testing.T) {
// Verify that when the SKIP guard is false, the ALT blocks on channels
// (not busy-waiting via default). The sender delays slightly to prove blocking.
occam := `SEQ
CHAN OF INT c:
INT result:
BOOL ready:
ready := FALSE
result := 0
PAR
SEQ
ALT
ready & SKIP
result := 99
c ? result
SKIP
SEQ
TIMER clock:
INT t:
clock ? t
clock ? AFTER t PLUS 100000
c ! 55
print.int(result)
`
output := transpileCompileRun(t, occam)
expected := "55\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}
func TestE2E_MultiLineAbbreviation(t *testing.T) {The file /home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go has been updated successfully.
Exit code 1
? github.com/codeassociates/occam2go [no test files]
? github.com/codeassociates/occam2go/ast [no test files]
--- FAIL: TestE2E_AltGuardedSkipFalseBlocking (0.00s)
e2e_misc_test.go:313: parser error: line 18: expected IDENT, got AFTER
e2e_misc_test.go:313: parser error: line 18: unexpected token: AFTER
e2e_misc_test.go:313: parser error: line 18: unexpected token: PLUS
e2e_misc_test.go:313: parser error: line 18: unexpected token: INT
FAIL
FAIL github.com/codeassociates/occam2go/codegen 19.482s
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)
FAIL
? github.com/codeassociates/occam2go [no test files]
? github.com/codeassociates/occam2go/ast [no test files]
--- FAIL: TestE2E_AltGuardedSkipFalseBlocking (0.00s)
e2e_misc_test.go:313: parser error: line 18: expected IDENT, got AFTER
e2e_misc_test.go:313: parser error: line 18: unexpected token: AFTER
e2e_misc_test.go:313: parser error: line 18: unexpected token: PLUS
e2e_misc_test.go:313: parser error: line 18: unexpected token: INT
FAIL
FAIL github.com/codeassociates/occam2go/codegen 19.482s
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)
FAILThe test has a syntax issue - clock ? AFTER t PLUS 100000 is timer AFTER syntax but it seems the parser doesn't handle it when used as a standalone statement. Let me check how timer waits work in the existing tests.
The timer syntax in my test is wrong. Let me check how timer delays are used in existing tests:
{
"pattern": "TIMER.*AFTER|clock.*AFTER",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"output_mode": "content",
"-n": true
}{
"pattern": "TIMER|timer.*AFTER",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"output_mode": "content",
"-n": true,
"-i": true,
"glob": "*_test.go"
}codegen/e2e_misc_test.go:306: TIMER clock:
codegen/e2e_concurrency_test.go:142:func TestE2E_TimerRead(t *testing.T) {
codegen/e2e_concurrency_test.go:143: // Test reading a timer: value should be positive (microseconds since epoch)
codegen/e2e_concurrency_test.go:145: TIMER tim:
codegen/e2e_concurrency_test.go:161:func TestE2E_TimerAltTimeout(t *testing.T) {
codegen/e2e_concurrency_test.go:162: // Test ALT with timer timeout: no channel is ready, so timer fires
codegen/e2e_concurrency_test.go:164: TIMER tim:{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go",
"offset": 142,
"limit": 50
} 142→func TestE2E_TimerRead(t *testing.T) {
143→ // Test reading a timer: value should be positive (microseconds since epoch)
144→ occam := `SEQ
145→ TIMER tim:
146→ INT t:
147→ tim ? t
148→ IF
149→ t > 0
150→ print.int(1)
151→ TRUE
152→ print.int(0)
153→`
154→ output := transpileCompileRun(t, occam)
155→ expected := "1\n"
156→ if output != expected {
157→ t.Errorf("expected %q, got %q", expected, output)
158→ }
159→}
160→
161→func TestE2E_TimerAltTimeout(t *testing.T) {
162→ // Test ALT with timer timeout: no channel is ready, so timer fires
163→ occam := `SEQ
164→ TIMER tim:
165→ INT t:
166→ tim ? t
167→ CHAN OF INT c:
168→ INT result:
169→ result := 0
170→ ALT
171→ c ? result
172→ result := 1
173→ tim ? AFTER (t + 1000)
174→ result := 2
175→ print.int(result)
176→`
177→ output := transpileCompileRun(t, occam)
178→ expected := "2\n"
179→ if output != expected {
180→ t.Errorf("expected %q, got %q", expected, output)
181→ }
182→}
183→
184→func TestE2E_ChanParam(t *testing.T) {
185→ occam := `PROC sender(CHAN OF INT output)
186→ output ! 42
187→
188→SEQ
189→ CHAN OF INT c:
190→ PAR
191→ sender(c)The AFTER syntax is used inside ALT cases, not as standalone timer wait statements. The standalone timer read is tim ? t to get current time. For a delay, I can't use clock ? AFTER as a standalone statement - it's only available in ALT blocks.
I need a simpler approach. Instead of a timer delay, I can use a different test strategy. The key thing I want to test is that when the SKIP guard is false, the ALT blocks on the channel case.
A simpler approach: use a chain of channels where the sender sends after some setup that guarantees ordering. Actually, the existing test TestE2E_AltGuardedSkipFalse already tests this - but per the plan, it's "racy" because Go's scheduler might run the sender first. With the fix, the blocking select correctly waits, so it should be reliable now.
I'll set up a relay pattern with two channels to enforce the sender delays before sending, ensuring the receiver blocks first and the test becomes deterministic.
The AFTER syntax is only valid inside ALT cases, not as standalone timer waits. Let me fix the test to use a simpler delay mechanism:
func TestE2E_AltGuardedSkipFalseBlocking(t *testing.T) {
// Verify that when the SKIP guard is false, the ALT blocks on channels
// (not busy-waiting via default). The sender delays slightly to prove blocking.
occam := `SEQ
CHAN OF INT c:
INT result:
BOOL ready:
ready := FALSE
result := 0
PAR
SEQ
ALT
ready & SKIP
result := 99
c ? result
SKIP
SEQ
TIMER clock:
INT t:
clock ? t
clock ? AFTER t PLUS 100000
c ! 55
print.int(result)
`
output := transpileCompileRun(t, occam)
expected := "55\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}func TestE2E_AltGuardedSkipFalseBlocking(t *testing.T) {
// Verify that when the SKIP guard is false, the ALT blocks on channels
// (not busy-waiting via default). The sender goes through a relay channel
// to introduce a delay, proving the ALT blocks until data arrives.
occam := `SEQ
CHAN OF INT relay:
CHAN OF INT c:
INT result:
BOOL ready:
ready := FALSE
result := 0
PAR
SEQ
ALT
ready & SKIP
result := 99
c ? result
SKIP
SEQ
INT tmp:
relay ? tmp
c ! tmp
relay ! 55
print.int(result)
`
output := transpileCompileRun(t, occam)
expected := "55\n"
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}The file /home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go has been updated successfully.
=== RUN TestE2E_AltBasic --- PASS: TestE2E_AltBasic (0.08s) === RUN TestE2E_AltSecondChannel --- PASS: TestE2E_AltSecondChannel (0.09s) === RUN TestE2E_AltWithBody --- PASS: TestE2E_AltWithBody (0.09s) === RUN TestE2E_AltWithBooleanGuard --- PASS: TestE2E_AltWithBooleanGuard (0.08s) === RUN TestE2E_AltWithTrueGuard --- PASS: TestE2E_AltWithTrueGuard (0.08s) === RUN TestE2E_AltWithParenthesizedGuard --- PASS: TestE2E_AltWithParenthesizedGuard (0.08s) === RUN TestE2E_AltGuardedSkip --- PASS: TestE2E_AltGuardedSkip (0.08s) === RUN TestE2E_AltGuardedSkipTrue --- PASS: TestE2E_AltGuardedSkipTrue (0.08s) === RUN TestE2E_AltGuardedSkipFalse --- PASS: TestE2E_AltGuardedSkipFalse (0.08s) === RUN TestE2E_AltGuardedSkipFalseBlocking --- PASS: TestE2E_AltGuardedSkipFalseBlocking (0.08s) PASS ok github.com/codeassociates/occam2go/codegen 0.819s
? github.com/codeassociates/occam2go [no test files] ? github.com/codeassociates/occam2go/ast [no test files] ok github.com/codeassociates/occam2go/codegen 18.839s 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)
./occam2go -o /tmp/life.go historical-examples/life.occ 2>&1 && go vet /tmp/life.go 2>&1
./occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module 2>&1 && go vet /tmp/course_out.go 2>&1
All done. Here's a summary of the changes:
Fix 1: Screen output auto-flush (codegen/codegen.go)
- Added if len(screen) == 0 { w.Flush() } after w.WriteByte(b) in the screen writer goroutine — flushes when the channel drains, so small bursts of output (like cursor movements in editor mode) are flushed immediately
- Applied the same fix to the error writer goroutine with len(_error) == 0
Fix 2: Guarded SKIP dual-select (codegen/codegen.go)
- Extracted generateAltChannelCase() helper method to avoid duplicating channel case generation
- When an ALT has a guarded SKIP (c.IsSkip && c.Guard != nil), generates a dual-select pattern:
- if _altSkipReady { select { ...cases... default: ...skip body... } } — non-blocking when guard is true
- else { select { ...cases... } } — blocks on channels when guard is false
- Unguarded SKIPs and ALTs without SKIP still use the original single-select pattern
Fix 3: Test (codegen/e2e_misc_test.go)
- Added TestE2E_AltGuardedSkipFalseBlocking — uses a relay channel to force the sender to delay, proving the ALT blocks when the SKIP guard is false (would deadlock with the old busy-wait default: pattern)
title: Investigate transpiler compatibility with example program from the Occam2 book state: OPEN author: dboreham labels: comments: 2 assignees: projects: milestone: number: 72 -- A code example from the book "Programming in Occam2" has been added to this repo here: ./historical-examples/life.occ Please analyze this program to determine why it doesn't parse and run with the current version of the transpiler.
author: associate-1 association: member edited: false status: none -- Analysis complete — see the detailed findings in [this comment on PR #71](https://github.com/codeassociates/occam2go/pull/71#issuecomment-3945869818). Seven unsupported features were identified and filed as individual issues: #73, #74, #75, #76, #77, #78, #79. -- author: dboreham association: member edited: false status: none -- I've been doing some manual testing of this. The life example now compiles and runs. In addition a recent fix to the transpiler golang preamble has made individual keystrokes on the keyboard channel work correctly. However, when I run the life program I'm not sure it is working. I see it display "Idle". If I hit the "e" key it seems to pause updating the screen, perhaps indicating that has indeed entered edit mode. But when I hit the A, B, C, D keys for direction and asterisk to activate a cell, I don't see anything on the screen. If I hit the q key it looks like it exits edit mode. So at least something is working but I haven't been able to get it to fully work with live cells and running the game. --
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: codegen/codegen.go modified: codegen/e2e_misc_test.go Untracked files: (use "git add <file>..." to include in what will be committed) od no changes added to commit (use "git add" and/or "git commit -a") codegen/codegen.go | 145 +++++++++++++++++++++++++++++++++-------------- codegen/e2e_misc_test.go | 32 +++++++++++ 2 files changed, 136 insertions(+), 41 deletions(-)
6272c72 Merge pull request #91 from codeassociates/feat/raw-terminal-keyboard-90 6da7760 Raw terminal mode for keyboard channel (#90) 814db69 Merge pull request #89 from codeassociates/fix/life-occ-compilation-72 5ab69cb Use entry point PROC pattern for life.occ terminal I/O 126b182 Fix Game of Life example compilation (#72)
gh issue comment 72 --body "$(cat <<'EOF' ## Root cause analysis of editor mode issues Two bugs were identified: **1. Screen output never flushes in editor mode** The screen goroutine uses `bufio.NewWriter` (4096-byte buffer) and only flushes on byte 255 (never sent in editor mode) or when the buffer is full. In edit mode, cursor movements produce ~8-24 bytes per keypress — far too few to trigger a flush. "Idle" was only visible because the busy-wait loop wrote it thousands of times, eventually filling the buffer. **2. PRI ALT guarded SKIP causes busy-wait (100% CPU spin)** The controller's `PRI ALT` has `(activity <> idle) & SKIP`. When idle, this guard is false, so the ALT should block on the keyboard channel. But the Go translation used an unconditional `default:` in `select`, which always fires — creating a 100% CPU spin loop that also prevented the ALT from ever waiting on the keyboard channel for input. Fix in PR incoming. EOF )" 2>&1
git add codegen/codegen.go codegen/e2e_misc_test.go && git commit -m "$(cat <<'EOF' Fix display buffering and PRI ALT busy-wait in life.occ (#72) Two bugs prevented the Game of Life editor mode from working: 1. Screen output goroutine never flushed for small writes. Add auto-flush when the channel drains (len(ch) == 0), so bursts of output flush immediately while still batching rapid writes. 2. Guarded SKIP in ALT generated unconditional `default:` in select, causing 100% CPU spin when the guard was false. Use a dual-select pattern: when guard is true, include default (non-blocking); when false, omit default (blocks on channels). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> EOF )" 2>&1
2 files changed, 136 insertions(+), 41 deletions(-)
remote: remote: Create a pull request for 'fix/life-display-and-alt-busywait-72' on GitHub by visiting: remote: https://github.com/codeassociates/occam2go/pull/new/fix/life-display-and-alt-busywait-72 remote: To associate-1.github.com:codeassociates/occam2go.git * [new branch] fix/life-display-and-alt-busywait-72 -> fix/life-display-and-alt-busywait-72 branch 'fix/life-display-and-alt-busywait-72' set up to track 'origin/fix/life-display-and-alt-busywait-72'.
gh pr create --title "Fix life.occ display buffering and ALT busy-wait" --body "$(cat <<'EOF' ## Summary Fixes the two bugs preventing the Game of Life editor mode from working (#72): - **Auto-flush screen output on channel drain**: The buffered writer now flushes when `len(ch) == 0`, so small writes (cursor movements, single characters) appear immediately instead of waiting for the 4096-byte buffer to fill - **Dual-select pattern for guarded SKIP in ALT**: When a SKIP guard is false, the generated `select` omits `default:` so it blocks on channel cases instead of busy-waiting at 100% CPU ## Test plan - [x] All existing tests pass (`go test ./...`) - [x] New `TestE2E_AltGuardedSkipFalseBlocking` verifies ALT blocks when SKIP guard is false (uses relay channel to force sender delay) - [x] `life.occ` transpiles and passes `go vet` - [ ] Manual test: `go run /tmp/life.go` — e→arrows→*→q→r works 🤖 Generated with [Claude Code](https://claude.com/claude-code) EOF )" 2>&1
Done. Comment added to issue #72 and PR created: https://github.com/codeassociates/occam2go/pull/92