From eb7faab1c6818221b6e713203bde63bd3b25c300 Mon Sep 17 00:00:00 2001 From: Associate 1 Date: Mon, 23 Feb 2026 18:06:46 -0700 Subject: [PATCH] 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 --- codegen/codegen.go | 145 ++++++++++++++++++++++++++++----------- codegen/e2e_misc_test.go | 32 +++++++++ 2 files changed, 136 insertions(+), 41 deletions(-) diff --git a/codegen/codegen.go b/codegen/codegen.go index e91bc12..86f71db 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -493,6 +493,11 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) { g.writeLine(`w.WriteByte('\r')`) g.writeLine("}") 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-- @@ -519,6 +524,11 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) { 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-- @@ -1962,6 +1972,15 @@ func (g *Generator) generateAltBlock(alt *ast.AltBlock) { } } + // 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 { @@ -1981,55 +2000,99 @@ func (g *Generator) generateAltBlock(alt *ast.AltBlock) { } } - g.writeLine("select {") - for i, c := range alt.Cases { + 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)) - 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("_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.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel))) } + g.writeLine("}") + g.indent-- + + // else { select without default — blocks on channels } + g.writeLine("} else {") 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++ + g.writeLine("select {") + for i, c := range alt.Cases { + if !c.IsSkip { + g.generateAltChannelCase(i, c) + } } - for _, s := range c.Body { - g.generateStatement(s) + 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) + } } - if guardedSkip { - g.indent-- - g.writeLine("}") + 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.indent-- + g.write(fmt.Sprintf("case %s = <-%s:\n", varRef, goIdent(c.Channel))) } - g.writeLine("}") + g.indent++ + for _, s := range c.Body { + g.generateStatement(s) + } + g.indent-- } func (g *Generator) generateReplicatedAlt(alt *ast.AltBlock) { diff --git a/codegen/e2e_misc_test.go b/codegen/e2e_misc_test.go index 0d142fa..590bfb4 100644 --- a/codegen/e2e_misc_test.go +++ b/codegen/e2e_misc_test.go @@ -286,6 +286,38 @@ func TestE2E_AltGuardedSkipFalse(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 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) + } +} + func TestE2E_MultiLineAbbreviation(t *testing.T) { // Issue #79: IS at end of line as continuation occam := `SEQ