Implement the following plan:
The generated entry harness reads keyboard input via bufio.NewReader(os.Stdin), which operates in the terminal's default line-buffered mode. Characters aren't available until Enter is pressed, breaking programs like echoing.occ that expect character-by-character input. We need to put the terminal in raw mode so bytes are available immediately as typed.
Use golang.org/x/term to switch stdin to raw mode in the generated entry harness. Fall back to the current buffered behavior when stdin is not a terminal (piped input).
go get golang.org/x/term — adds the package to go.mod/go.sumcodegen/codegen.go — Generator struct (line ~21)needTerm bool flagcodegen/codegen.go — flag initialization (line ~106)g.needTerm = falsecodegen/codegen.go — entry harness detection (lines 216-220)g.needTerm = true when entry harness is detectedcodegen/codegen.go — import generation (lines 228-256)needTerm in the import block guardneedTerm, emit imports for "os/signal", "syscall", and "golang.org/x/term" (alphabetical order within the block)codegen/codegen.go — generateEntryHarness() (lines 419-512)Rewrite to generate code structured like this:
func main() {
keyboard := make(chan byte, 256)
screen := make(chan byte, 256)
_error := make(chan byte, 256)
// Raw terminal mode
var rawMode bool
var oldState *term.State
fd := int(os.Stdin.Fd())
if term.IsTerminal(fd) {
var err error
oldState, err = term.MakeRaw(fd)
if err == nil {
rawMode = true
defer term.Restore(fd, oldState)
// Restore terminal on signals (SIGINT won't fire from Ctrl+C in
// raw mode, but can arrive via external kill; SIGTERM from kill)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
term.Restore(fd, oldState)
os.Exit(1)
}()
}
}
var wg sync.WaitGroup
wg.Add(2)
// Screen writer — in raw mode, insert CR before LF
go func() {
defer wg.Done()
w := bufio.NewWriter(os.Stdout)
for b := range screen {
if b == 255 {
w.Flush()
} else {
if rawMode && b == '\n' {
w.WriteByte('\r')
}
w.WriteByte(b)
}
}
w.Flush()
}()
// Error writer — same CR/LF handling
go func() { /* same pattern as screen, writing to os.Stderr */ }()
// Keyboard reader
go func() {
if rawMode {
buf := make([]byte, 1)
for {
n, err := os.Stdin.Read(buf)
if err != nil || n == 0 {
close(keyboard)
return
}
if buf[0] == 3 { // Ctrl+C
term.Restore(fd, oldState)
os.Exit(1)
}
keyboard <- buf[0]
}
} else {
r := bufio.NewReader(os.Stdin)
for {
b, err := r.ReadByte()
if err != nil {
close(keyboard)
return
}
keyboard <- b
}
}
}()
entryProc(keyboard, screen, _error)
close(screen)
close(_error)
wg.Wait()
}
Key design decisions:
- Ctrl+C handling: In raw mode, ISIG is disabled so Ctrl+C sends byte 3 instead of SIGINT. The keyboard reader intercepts byte 3 to restore terminal and exit cleanly.
- LF→CRLF in output: Raw mode disables OPOST, so \n no longer auto-translates to \r\n. The screen/error writers insert CR before LF. This is harmless if the occam program already sends both *c and *n (the extra CR is a no-op).
- Non-terminal fallback: When stdin is piped, skip raw mode and use the existing bufio.NewReader approach for efficient buffered reading.
- Signal handler: Catches external SIGINT/SIGTERM to restore terminal state before exit.
codegen/codegen_test.go — unit testterm.MakeRaw, term.IsTerminal, and term.Restore.codegen/e2e_helpers_test.go — new helper + e2e testtranspileCompileRunWithInput() helper that:go mod init test && go mod tidy)echoing.occ-style program, pipe input "hello Z", verify output matches.go test ./... — all existing tests passechoing.occ:
bash
go build -o occam2go . && ./occam2go -I kroc/modules/course/libsrc -o /tmp/echo.go kroc/modules/course/examples/echoing.occ
cd /tmp && go mod init echo_test && go mod tidy && go run echo.go
# Type characters — they should echo immediately, one at a time
# Type 'Z' to exitIf 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/0114918c-281e-45dd-98ac-dfc9baf171b5.jsonl
Let me start by understanding the current codebase state, then implement the plan step by step. Let me read the key files first.
File content (32660 tokens) exceeds maximum allowed tokens (25000). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen_test.go"
}{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_test.go"
}{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"limit": 100
} 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→ needMathBits bool // track if we need math/bits package import
20→ needBufio bool // track if we need bufio package import
21→ needReflect bool // track if we need reflect package import
22→ needBoolHelper bool // track if we need _boolToInt helper
23→
24→ // Track procedure signatures for proper pointer handling
25→ procSigs map[string][]ast.ProcParam
26→ // Track current procedure's reference parameters
27→ refParams map[string]bool
28→
29→ // Protocol support
30→ protocolDefs map[string]*ast.ProtocolDecl
31→ chanProtocols map[string]string // channel name → protocol name
32→ tmpCounter int // for unique temp variable names
33→
34→ // Record support
35→ recordDefs map[string]*ast.RecordDecl
36→ recordVars map[string]string // variable name → record type name
37→
38→ // Channel element type tracking (for ALT guard codegen)
39→ chanElemTypes map[string]string // channel name → Go element type
40→
41→ // Bool variable tracking (for type conversion codegen)
42→ boolVars map[string]bool
43→
44→ // Nesting level: 0 = package level, >0 = inside a function
45→ nestingLevel int
46→
47→ // RETYPES parameter renames: when a RETYPES declaration shadows a
48→ // parameter (e.g. VAL INT X RETYPES X :), the parameter is renamed
49→ // in the signature so := can create a new variable with the original name.
50→ retypesRenames map[string]string
51→}
52→
53→// Transputer intrinsic function names
54→var transpIntrinsics = map[string]bool{
55→ "LONGPROD": true,
56→ "LONGDIV": true,
57→ "LONGSUM": true,
58→ "LONGDIFF": true,
59→ "NORMALISE": true,
60→ "SHIFTRIGHT": true,
61→ "SHIFTLEFT": true,
62→}
63→
64→// Built-in print procedures
65→var printBuiltins = map[string]bool{
66→ "print.int": true,
67→ "print.string": true,
68→ "print.bool": true,
69→ "print.newline": true,
70→}
71→
72→// New creates a new code generator
73→func New() *Generator {
74→ return &Generator{}
75→}
76→
77→// goIdent converts an occam identifier to a valid Go identifier.
78→// Occam allows dots in identifiers (e.g., out.repeat); Go does not.
79→// goReserved is a set of Go keywords and predeclared identifiers that cannot be
80→// used as variable names when they also appear as type conversions in the generated code.
81→var goReserved = map[string]bool{
82→ "byte": true, "int": true, "string": true, "len": true, "cap": true,
83→ "make": true, "new": true, "copy": true, "close": true, "delete": true,
84→ "panic": true, "recover": true, "print": true, "println": true,
85→ "error": true, "rune": true, "bool": true, "true": true, "false": true,
86→}
87→
88→func goIdent(name string) string {
89→ name = strings.ReplaceAll(name, ".", "_")
90→ if goReserved[name] {
91→ return "_" + name
92→ }
93→ return name
94→}
95→
96→// Generate produces Go code from the AST
97→func (g *Generator) Generate(program *ast.Program) string {
98→ g.builder.Reset()
99→ g.needSync = false
100→ g.needFmt = false{
"pattern": "codegen/e2e_helpers_test.go",
"path": "/home/david/projects/code-associates/occam2go"
}{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 100,
"limit": 200
} 100→ g.needFmt = false
101→ g.needTime = false
102→ g.needOs = false
103→ g.needMath = false
104→ g.needMathBits = false
105→ g.needBufio = false
106→ g.needReflect = false
107→ g.needBoolHelper = false
108→ g.procSigs = make(map[string][]ast.ProcParam)
109→ g.refParams = make(map[string]bool)
110→ g.protocolDefs = make(map[string]*ast.ProtocolDecl)
111→ g.chanProtocols = make(map[string]string)
112→ g.chanElemTypes = make(map[string]string)
113→ g.tmpCounter = 0
114→ g.recordDefs = make(map[string]*ast.RecordDecl)
115→ g.recordVars = make(map[string]string)
116→ g.boolVars = make(map[string]bool)
117→
118→ // Pre-pass: collect BOOL variable names (needed before containsBoolConversion)
119→ for _, stmt := range program.Statements {
120→ g.collectBoolVars(stmt)
121→ }
122→
123→ // First pass: collect procedure signatures, protocols, and check for PAR/print
124→ for _, stmt := range program.Statements {
125→ if g.containsPar(stmt) {
126→ g.needSync = true
127→ }
128→ if g.containsPrint(stmt) {
129→ g.needFmt = true
130→ }
131→ if g.containsTimer(stmt) {
132→ g.needTime = true
133→ }
134→ if g.containsStop(stmt) {
135→ g.needOs = true
136→ g.needFmt = true
137→ }
138→ if g.containsMostExpr(stmt) {
139→ g.needMath = true
140→ }
141→ if g.containsIntrinsics(stmt) {
142→ g.needMathBits = true
143→ }
144→ if g.containsRetypes(stmt) {
145→ g.needMath = true
146→ }
147→ if g.containsAltReplicator(stmt) {
148→ g.needReflect = true
149→ }
150→ if g.containsBoolConversion(stmt) {
151→ g.needBoolHelper = true
152→ }
153→ if proc, ok := stmt.(*ast.ProcDecl); ok {
154→ g.procSigs[proc.Name] = proc.Params
155→ g.collectNestedProcSigs(proc.Body)
156→ }
157→ if fn, ok := stmt.(*ast.FuncDecl); ok {
158→ g.procSigs[fn.Name] = fn.Params
159→ }
160→ if proto, ok := stmt.(*ast.ProtocolDecl); ok {
161→ g.protocolDefs[proto.Name] = proto
162→ }
163→ if rec, ok := stmt.(*ast.RecordDecl); ok {
164→ g.recordDefs[rec.Name] = rec
165→ }
166→ g.collectChanProtocols(stmt)
167→ g.collectRecordVars(stmt)
168→ }
169→
170→ // Separate protocol, record, procedure declarations from other statements
171→ var typeDecls []ast.Statement
172→ var procDecls []ast.Statement
173→ var mainStatements []ast.Statement
174→
175→ // First pass: check if there are any proc/func declarations
176→ hasProcDecls := false
177→ for _, stmt := range program.Statements {
178→ if _, ok := stmt.(*ast.ProcDecl); ok {
179→ hasProcDecls = true
180→ break
181→ }
182→ if _, ok := stmt.(*ast.FuncDecl); ok {
183→ hasProcDecls = true
184→ break
185→ }
186→ }
187→
188→ var abbrDecls []ast.Statement
189→ for _, stmt := range program.Statements {
190→ switch s := stmt.(type) {
191→ case *ast.ProtocolDecl, *ast.RecordDecl:
192→ typeDecls = append(typeDecls, stmt)
193→ case *ast.ProcDecl, *ast.FuncDecl:
194→ procDecls = append(procDecls, stmt)
195→ case *ast.Abbreviation:
196→ if hasProcDecls {
197→ // Top-level abbreviations need to be at package level
198→ // so PROCs can reference them
199→ abbrDecls = append(abbrDecls, stmt)
200→ } else {
201→ mainStatements = append(mainStatements, stmt)
202→ }
203→ case *ast.RetypesDecl:
204→ _ = s
205→ // RETYPES declarations are local to functions, not package-level
206→ mainStatements = append(mainStatements, stmt)
207→ default:
208→ mainStatements = append(mainStatements, stmt)
209→ }
210→ }
211→
212→ // Detect entry point PROC so we can set import flags before writing imports
213→ var entryProc *ast.ProcDecl
214→ if len(mainStatements) == 0 {
215→ entryProc = g.findEntryProc(procDecls)
216→ if entryProc != nil {
217→ g.needOs = true
218→ g.needSync = true
219→ g.needBufio = true
220→ }
221→ }
222→
223→ // Write package declaration
224→ g.writeLine("package main")
225→ g.writeLine("")
226→
227→ // Write imports
228→ if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect {
229→ g.writeLine("import (")
230→ g.indent++
231→ if g.needBufio {
232→ g.writeLine(`"bufio"`)
233→ }
234→ if g.needFmt {
235→ g.writeLine(`"fmt"`)
236→ }
237→ if g.needMath {
238→ g.writeLine(`"math"`)
239→ }
240→ if g.needMathBits {
241→ g.writeLine(`"math/bits"`)
242→ }
243→ if g.needOs {
244→ g.writeLine(`"os"`)
245→ }
246→ if g.needReflect {
247→ g.writeLine(`"reflect"`)
248→ }
249→ if g.needSync {
250→ g.writeLine(`"sync"`)
251→ }
252→ if g.needTime {
253→ g.writeLine(`"time"`)
254→ }
255→ g.indent--
256→ g.writeLine(")")
257→ g.writeLine("")
258→ }
259→
260→ // Emit transputer intrinsic helper functions
261→ if g.needMathBits {
262→ g.emitIntrinsicHelpers()
263→ }
264→
265→ // Emit _boolToInt helper function
266→ if g.needBoolHelper {
267→ g.emitBoolHelper()
268→ }
269→
270→ // Generate type definitions first (at package level)
271→ for _, stmt := range typeDecls {
272→ g.generateStatement(stmt)
273→ }
274→
275→ // Generate package-level abbreviations (constants)
276→ for _, stmt := range abbrDecls {
277→ abbr := stmt.(*ast.Abbreviation)
278→ if abbr.Type == "" {
279→ // Untyped VAL: let Go infer the type
280→ g.builder.WriteString("var ")
281→ g.write(fmt.Sprintf("%s = ", goIdent(abbr.Name)))
282→ g.generateExpression(abbr.Value)
283→ g.write("\n")
284→ } else {
285→ goType := g.occamTypeToGo(abbr.Type)
286→ if abbr.IsOpenArray || abbr.IsFixedArray {
287→ goType = "[]" + goType
288→ }
289→ g.builder.WriteString("var ")
290→ g.write(fmt.Sprintf("%s %s = ", goIdent(abbr.Name), goType))
291→ // Wrap string literals with []byte() when assigned to []byte variables
292→ if _, isStr := abbr.Value.(*ast.StringLiteral); isStr && abbr.IsOpenArray && abbr.Type == "BYTE" {
293→ g.write("[]byte(")
294→ g.generateExpression(abbr.Value)
295→ g.write(")")
296→ } else {
297→ g.generateExpression(abbr.Value)
298→ }
299→ g.write("\n"){
"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→Now let me read the entry harness generation code and the rest of the import/flag sections.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen.go",
"offset": 395,
"limit": 150
} 395→// findEntryProc looks for the last top-level PROC with the standard occam
396→// entry point signature: exactly 3 CHAN OF BYTE params (keyboard?, screen!, error!).
397→func (g *Generator) findEntryProc(procDecls []ast.Statement) *ast.ProcDecl {
398→ var entry *ast.ProcDecl
399→ for _, stmt := range procDecls {
400→ proc, ok := stmt.(*ast.ProcDecl)
401→ if !ok {
402→ continue
403→ }
404→ if len(proc.Params) != 3 {
405→ continue
406→ }
407→ p0, p1, p2 := proc.Params[0], proc.Params[1], proc.Params[2]
408→ if p0.IsChan && p0.ChanElemType == "BYTE" && p0.ChanDir == "?" &&
409→ p1.IsChan && p1.ChanElemType == "BYTE" && p1.ChanDir == "!" &&
410→ p2.IsChan && p2.ChanElemType == "BYTE" && p2.ChanDir == "!" {
411→ entry = proc
412→ }
413→ }
414→ return entry
415→}
416→
417→// generateEntryHarness emits a func main() that wires stdin/stdout/stderr
418→// to channels and calls the entry PROC.
419→func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
420→ name := goIdent(proc.Name)
421→ g.writeLine("func main() {")
422→ g.indent++
423→
424→ // Create channels
425→ g.writeLine("keyboard := make(chan byte, 256)")
426→ g.writeLine("screen := make(chan byte, 256)")
427→ g.writeLine("_error := make(chan byte, 256)")
428→ g.writeLine("")
429→
430→ // WaitGroup for writer goroutines to finish draining
431→ g.writeLine("var wg sync.WaitGroup")
432→ g.writeLine("wg.Add(2)")
433→ g.writeLine("")
434→
435→ // Screen writer goroutine
436→ g.writeLine("go func() {")
437→ g.indent++
438→ g.writeLine("defer wg.Done()")
439→ g.writeLine("w := bufio.NewWriter(os.Stdout)")
440→ g.writeLine("for b := range screen {")
441→ g.indent++
442→ g.writeLine("if b == 255 {")
443→ g.indent++
444→ g.writeLine("w.Flush()")
445→ g.indent--
446→ g.writeLine("} else {")
447→ g.indent++
448→ g.writeLine("w.WriteByte(b)")
449→ g.indent--
450→ g.writeLine("}")
451→ g.indent--
452→ g.writeLine("}")
453→ g.writeLine("w.Flush()")
454→ g.indent--
455→ g.writeLine("}()")
456→ g.writeLine("")
457→
458→ // Error writer goroutine
459→ g.writeLine("go func() {")
460→ g.indent++
461→ g.writeLine("defer wg.Done()")
462→ g.writeLine("w := bufio.NewWriter(os.Stderr)")
463→ g.writeLine("for b := range _error {")
464→ g.indent++
465→ g.writeLine("if b == 255 {")
466→ g.indent++
467→ g.writeLine("w.Flush()")
468→ g.indent--
469→ g.writeLine("} else {")
470→ g.indent++
471→ g.writeLine("w.WriteByte(b)")
472→ g.indent--
473→ g.writeLine("}")
474→ g.indent--
475→ g.writeLine("}")
476→ g.writeLine("w.Flush()")
477→ g.indent--
478→ g.writeLine("}()")
479→ g.writeLine("")
480→
481→ // Keyboard reader goroutine
482→ g.writeLine("go func() {")
483→ g.indent++
484→ g.writeLine("r := bufio.NewReader(os.Stdin)")
485→ g.writeLine("for {")
486→ g.indent++
487→ g.writeLine("b, err := r.ReadByte()")
488→ g.writeLine("if err != nil {")
489→ g.indent++
490→ g.writeLine("close(keyboard)")
491→ g.writeLine("return")
492→ g.indent--
493→ g.writeLine("}")
494→ g.writeLine("keyboard <- b")
495→ g.indent--
496→ g.writeLine("}")
497→ g.indent--
498→ g.writeLine("}()")
499→ g.writeLine("")
500→
501→ // Call the entry proc
502→ g.writeLine(fmt.Sprintf("%s(keyboard, screen, _error)", name))
503→ g.writeLine("")
504→
505→ // Close output channels and wait for writers to drain
506→ g.writeLine("close(screen)")
507→ g.writeLine("close(_error)")
508→ g.writeLine("wg.Wait()")
509→
510→ g.indent--
511→ g.writeLine("}")
512→}
513→
514→func (g *Generator) containsPar(stmt ast.Statement) bool {
515→ switch s := stmt.(type) {
516→ case *ast.ParBlock:
517→ return true
518→ case *ast.SeqBlock:
519→ for _, inner := range s.Statements {
520→ if g.containsPar(inner) {
521→ return true
522→ }
523→ }
524→ case *ast.AltBlock:
525→ for _, c := range s.Cases {
526→ for _, inner := range c.Body {
527→ if g.containsPar(inner) {
528→ return true
529→ }
530→ }
531→ }
532→ case *ast.ProcDecl:
533→ for _, inner := range s.Body {
534→ if g.containsPar(inner) {
535→ return true
536→ }
537→ }
538→ case *ast.FuncDecl:
539→ for _, inner := range s.Body {
540→ if g.containsPar(inner) {
541→ return true
542→ }
543→ }
544→ case *ast.WhileLoop:{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen_test.go",
"limit": 50
} 1→package codegen
2→
3→import (
4→ "strings"
5→ "testing"
6→
7→ "github.com/codeassociates/occam2go/lexer"
8→ "github.com/codeassociates/occam2go/parser"
9→)
10→
11→func TestSimpleVarDecl(t *testing.T) {
12→ input := `INT x:
13→`
14→ output := transpile(t, input)
15→
16→ if !strings.Contains(output, "var x int") {
17→ t.Errorf("expected 'var x int' in output, got:\n%s", output)
18→ }
19→}
20→
21→func TestMultipleVarDecl(t *testing.T) {
22→ input := `INT x, y, z:
23→`
24→ output := transpile(t, input)
25→
26→ if !strings.Contains(output, "var x, y, z int") {
27→ t.Errorf("expected 'var x, y, z int' in output, got:\n%s", output)
28→ }
29→}
30→
31→func TestReal32VarDecl(t *testing.T) {
32→ input := `REAL32 x:
33→`
34→ output := transpile(t, input)
35→
36→ if !strings.Contains(output, "var x float32") {
37→ t.Errorf("expected 'var x float32' in output, got:\n%s", output)
38→ }
39→}
40→
41→func TestReal64VarDecl(t *testing.T) {
42→ input := `REAL64 x:
43→`
44→ output := transpile(t, input)
45→
46→ if !strings.Contains(output, "var x float64") {
47→ t.Errorf("expected 'var x float64' in output, got:\n%s", output)
48→ }
49→}
50→{
"subject": "Add golang.org/x/term dependency",
"activeForm": "Adding golang.org/x/term dependency"
}{
"subject": "Update codegen.go: needTerm flag, imports, entry harness",
"activeForm": "Updating codegen.go for raw terminal mode"
}{
"subject": "Add unit test for entry harness term.MakeRaw",
"activeForm": "Adding unit test for raw terminal mode"
}{
"subject": "Add e2e test with piped stdin",
"activeForm": "Adding e2e test with piped stdin"
}{
"subject": "Run tests and verify",
"activeForm": "Running tests"
}go: downloading golang.org/x/term v0.40.0 go: downloading golang.org/x/sys v0.41.0 go: added golang.org/x/sys v0.41.0 go: added golang.org/x/term v0.40.0
needReflect bool // track if we need reflect package import needBoolHelper bool // track if we need _boolToInt helper
needReflect bool // track if we need reflect package import needBoolHelper bool // track if we need _boolToInt helper needTerm bool // track if we need golang.org/x/term package import
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
g.needReflect = false g.needBoolHelper = false
g.needReflect = false g.needBoolHelper = false g.needTerm = false
The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
if entryProc != nil {
g.needOs = true
g.needSync = true
g.needBufio = true
} if entryProc != nil {
g.needOs = true
g.needSync = true
g.needBufio = true
g.needTerm = true
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect {
g.writeLine("import (")
g.indent++
if g.needBufio {
g.writeLine(`"bufio"`)
}
if g.needFmt {
g.writeLine(`"fmt"`)
}
if g.needMath {
g.writeLine(`"math"`)
}
if g.needMathBits {
g.writeLine(`"math/bits"`)
}
if g.needOs {
g.writeLine(`"os"`)
}
if g.needReflect {
g.writeLine(`"reflect"`)
}
if g.needSync {
g.writeLine(`"sync"`)
}
if g.needTime {
g.writeLine(`"time"`)
}
g.indent--
g.writeLine(")")
g.writeLine("")
} if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect || g.needTerm {
g.writeLine("import (")
g.indent++
if g.needBufio {
g.writeLine(`"bufio"`)
}
if g.needFmt {
g.writeLine(`"fmt"`)
}
if g.needMath {
g.writeLine(`"math"`)
}
if g.needMathBits {
g.writeLine(`"math/bits"`)
}
if g.needOs {
g.writeLine(`"os"`)
}
if g.needTerm {
g.writeLine(`"os/signal"`)
}
if g.needReflect {
g.writeLine(`"reflect"`)
}
if g.needSync {
g.writeLine(`"sync"`)
}
if g.needTerm {
g.writeLine(`"syscall"`)
}
if g.needTime {
g.writeLine(`"time"`)
}
if g.needTerm {
g.writeLine("")
g.writeLine(`"golang.org/x/term"`)
}
g.indent--
g.writeLine(")")
g.writeLine("")
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
// generateEntryHarness emits a func main() that wires stdin/stdout/stderr
// to channels and calls the entry PROC.
func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
name := goIdent(proc.Name)
g.writeLine("func main() {")
g.indent++
// Create channels
g.writeLine("keyboard := make(chan byte, 256)")
g.writeLine("screen := make(chan byte, 256)")
g.writeLine("_error := make(chan byte, 256)")
g.writeLine("")
// WaitGroup for writer goroutines to finish draining
g.writeLine("var wg sync.WaitGroup")
g.writeLine("wg.Add(2)")
g.writeLine("")
// Screen writer goroutine
g.writeLine("go func() {")
g.indent++
g.writeLine("defer wg.Done()")
g.writeLine("w := bufio.NewWriter(os.Stdout)")
g.writeLine("for b := range screen {")
g.indent++
g.writeLine("if b == 255 {")
g.indent++
g.writeLine("w.Flush()")
g.indent--
g.writeLine("} else {")
g.indent++
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
g.writeLine("go func() {")
g.indent++
g.writeLine("defer wg.Done()")
g.writeLine("w := bufio.NewWriter(os.Stderr)")
g.writeLine("for b := range _error {")
g.indent++
g.writeLine("if b == 255 {")
g.indent++
g.writeLine("w.Flush()")
g.indent--
g.writeLine("} else {")
g.indent++
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("go func() {")
g.indent++
g.writeLine("r := bufio.NewReader(os.Stdin)")
g.writeLine("for {")
g.indent++
g.writeLine("b, err := r.ReadByte()")
g.writeLine("if err != nil {")
g.indent++
g.writeLine("close(keyboard)")
g.writeLine("return")
g.indent--
g.writeLine("}")
g.writeLine("keyboard <- b")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}()")
g.writeLine("")
// Call the entry proc
g.writeLine(fmt.Sprintf("%s(keyboard, screen, _error)", name))
g.writeLine("")
// Close output channels and wait for writers to drain
g.writeLine("close(screen)")
g.writeLine("close(_error)")
g.writeLine("wg.Wait()")
g.indent--
g.writeLine("}")
}// generateEntryHarness emits a func main() that wires stdin/stdout/stderr
// to channels and calls the entry PROC. When stdin is a terminal, the
// harness switches to raw mode (via golang.org/x/term) so that keyboard
// input is available character-by-character without waiting for Enter.
func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
name := goIdent(proc.Name)
g.writeLine("func main() {")
g.indent++
// Create channels
g.writeLine("keyboard := make(chan byte, 256)")
g.writeLine("screen := make(chan byte, 256)")
g.writeLine("_error := make(chan byte, 256)")
g.writeLine("")
// Raw terminal mode setup
g.writeLine("// Raw terminal mode — gives character-at-a-time keyboard input")
g.writeLine("var rawMode bool")
g.writeLine("var oldState *term.State")
g.writeLine("fd := int(os.Stdin.Fd())")
g.writeLine("if term.IsTerminal(fd) {")
g.indent++
g.writeLine("var err error")
g.writeLine("oldState, err = term.MakeRaw(fd)")
g.writeLine("if err == nil {")
g.indent++
g.writeLine("rawMode = true")
g.writeLine("defer term.Restore(fd, oldState)")
g.writeLine("// Restore terminal on external signals")
g.writeLine("sigCh := make(chan os.Signal, 1)")
g.writeLine("signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)")
g.writeLine("go func() {")
g.indent++
g.writeLine("<-sigCh")
g.writeLine("term.Restore(fd, oldState)")
g.writeLine("os.Exit(1)")
g.indent--
g.writeLine("}()")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.writeLine("")
// WaitGroup for writer goroutines to finish draining
g.writeLine("var wg sync.WaitGroup")
g.writeLine("wg.Add(2)")
g.writeLine("")
// Screen writer goroutine — in raw mode, insert CR before LF
g.writeLine("go func() {")
g.indent++
g.writeLine("defer wg.Done()")
g.writeLine("w := bufio.NewWriter(os.Stdout)")
g.writeLine("for b := range screen {")
g.indent++
g.writeLine("if b == 255 {")
g.indent++
g.writeLine("w.Flush()")
g.indent--
g.writeLine("} else {")
g.indent++
g.writeLine(`if rawMode && b == '\n' {`)
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("")
// Error writer goroutine — same CR/LF handling
g.writeLine("go func() {")
g.indent++
g.writeLine("defer wg.Done()")
g.writeLine("w := bufio.NewWriter(os.Stderr)")
g.writeLine("for b := range _error {")
g.indent++
g.writeLine("if b == 255 {")
g.indent++
g.writeLine("w.Flush()")
g.indent--
g.writeLine("} else {")
g.indent++
g.writeLine(`if rawMode && b == '\n' {`)
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("go func() {")
g.indent++
g.writeLine("if rawMode {")
g.indent++
g.writeLine("buf := make([]byte, 1)")
g.writeLine("for {")
g.indent++
g.writeLine("n, err := os.Stdin.Read(buf)")
g.writeLine("if err != nil || n == 0 {")
g.indent++
g.writeLine("close(keyboard)")
g.writeLine("return")
g.indent--
g.writeLine("}")
g.writeLine("if buf[0] == 3 { // Ctrl+C")
g.indent++
g.writeLine("term.Restore(fd, oldState)")
g.writeLine("os.Exit(1)")
g.indent--
g.writeLine("}")
g.writeLine("keyboard <- buf[0]")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("} else {")
g.indent++
g.writeLine("r := bufio.NewReader(os.Stdin)")
g.writeLine("for {")
g.indent++
g.writeLine("b, err := r.ReadByte()")
g.writeLine("if err != nil {")
g.indent++
g.writeLine("close(keyboard)")
g.writeLine("return")
g.indent--
g.writeLine("}")
g.writeLine("keyboard <- b")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}")
g.indent--
g.writeLine("}()")
g.writeLine("")
// Call the entry proc
g.writeLine(fmt.Sprintf("%s(keyboard, screen, _error)", name))
g.writeLine("")
// Close output channels and wait for writers to drain
g.writeLine("close(screen)")
g.writeLine("close(_error)")
g.writeLine("wg.Wait()")
g.indent--
g.writeLine("}")
}The file /home/david/projects/code-associates/occam2go/codegen/codegen.go has been updated successfully.
Now add the unit test. Let me find the transpile helper first and the end of the test file.
{
"pattern": "^func transpile\\(",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"output_mode": "content"
}wc -l /home/david/projects/code-associates/occam2go/codegen/codegen_test.go
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/codegen_test.go",
"offset": 840
} 840→ output := transpile(t, input)
841→ if !strings.Contains(output, "grid[i][j] = 42") {
842→ t.Errorf("expected 'grid[i][j] = 42' in output, got:\n%s", output)
843→ }
844→}
845→
846→func TestMultiDimProcParamCodegen(t *testing.T) {
847→ input := `PROC fill([][]CHAN OF INT grid)
848→ SKIP
849→`
850→ output := transpile(t, input)
851→ if !strings.Contains(output, "func fill(grid [][]chan int)") {
852→ t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
853→ }
854→}
855→func TestMultiDimProcParamCodegen(t *testing.T) {
input := `PROC fill([][]CHAN OF INT grid)
SKIP
`
output := transpile(t, input)
if !strings.Contains(output, "func fill(grid [][]chan int)") {
t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
}
}func TestMultiDimProcParamCodegen(t *testing.T) {
input := `PROC fill([][]CHAN OF INT grid)
SKIP
`
output := transpile(t, input)
if !strings.Contains(output, "func fill(grid [][]chan int)") {
t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
}
}
func TestEntryHarnessRawTerminal(t *testing.T) {
input := `PROC echo(CHAN OF BYTE keyboard?, screen!, error!)
BYTE ch:
SEQ
keyboard ? ch
screen ! ch
:
`
output := transpile(t, input)
// Should contain raw terminal mode setup
for _, want := range []string{
"term.IsTerminal",
"term.MakeRaw",
"term.Restore",
`"golang.org/x/term"`,
`"os/signal"`,
`"syscall"`,
"rawMode",
} {
if !strings.Contains(output, want) {
t.Errorf("expected %q in entry harness output, got:\n%s", want, output)
}
}
}The file /home/david/projects/code-associates/occam2go/codegen/codegen_test.go has been updated successfully.
{
"file_path": "/home/david/projects/code-associates/occam2go/codegen/e2e_test.go",
"limit": 60
}File does not exist. Note: your current working directory is /home/david/projects/code-associates/occam2go.
{
"pattern": "codegen/*e2e*",
"path": "/home/david/projects/code-associates/occam2go"
}/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_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_basic_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_phase2_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_course_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_intrinsics_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_strings_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_params_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_retypes_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_types_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_control_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_array_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_misc_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_protocol_test.go /home/david/projects/code-associates/occam2go/codegen/e2e_concurrency_test.go
{
"pattern": "entry|harness|Entry|Harness|CHAN OF BYTE keyboard\"",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"glob": "*e2e*test.go",
"output_mode": "content"
}{
"pattern": "CHAN OF BYTE\"",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"glob": "*e2e*test.go",
"output_mode": "content"
}{
"pattern": "CHAN OF BYTE",
"path": "/home/david/projects/code-associates/occam2go/codegen",
"glob": "*e2e*test.go",
"output_mode": "content"
}No existing entry harness e2e tests yet. The entry harness program needs go mod init + go mod tidy since it imports golang.org/x/term. Let me add the helper and test.
{
"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→import ( "os" "os/exec" "path/filepath" "testing" "github.com/codeassociates/occam2go/lexer" "github.com/codeassociates/occam2go/parser" "github.com/codeassociates/occam2go/preproc" )
import ( "os" "os/exec" "path/filepath" "strings" "testing" "github.com/codeassociates/occam2go/lexer" "github.com/codeassociates/occam2go/parser" "github.com/codeassociates/occam2go/preproc" )
The file /home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go has been updated successfully.
return transpileCompileRun(t, expanded) }
return transpileCompileRun(t, expanded)
}
// transpileCompileRunWithInput takes Occam source that uses the entry-point
// PROC pattern (CHAN OF BYTE keyboard?, screen!, error!), transpiles to Go,
// initialises a Go module (needed for golang.org/x/term), compiles, pipes
// the given input to stdin, and returns the stdout output.
func transpileCompileRunWithInput(t *testing.T, occamSource, stdin string) string {
t.Helper()
// Transpile
l := lexer.New(occamSource)
p := parser.New(l)
program := p.ParseProgram()
if len(p.Errors()) > 0 {
for _, err := range p.Errors() {
t.Errorf("parser error: %s", err)
}
t.FailNow()
}
gen := New()
goCode := gen.Generate(program)
// Create temp directory
tmpDir, err := os.MkdirTemp("", "occam2go-test-*")
if err != nil {
t.Fatalf("failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Write Go source
goFile := filepath.Join(tmpDir, "main.go")
if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil {
t.Fatalf("failed to write Go file: %v", err)
}
// Initialise Go module (needed for golang.org/x/term dependency)
modInit := exec.Command("go", "mod", "init", "test")
modInit.Dir = tmpDir
if out, err := modInit.CombinedOutput(); err != nil {
t.Fatalf("go mod init failed: %v\n%s", err, out)
}
modTidy := exec.Command("go", "mod", "tidy")
modTidy.Dir = tmpDir
if out, err := modTidy.CombinedOutput(); err != nil {
t.Fatalf("go mod tidy failed: %v\n%s\nGo code:\n%s", err, out, goCode)
}
// Compile
binFile := filepath.Join(tmpDir, "main")
compileCmd := exec.Command("go", "build", "-o", binFile, ".")
compileCmd.Dir = tmpDir
if out, err := compileCmd.CombinedOutput(); err != nil {
t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, out, goCode)
}
// Run with piped stdin
runCmd := exec.Command(binFile)
runCmd.Stdin = strings.NewReader(stdin)
output, err := runCmd.CombinedOutput()
if err != nil {
t.Fatalf("execution failed: %v\nOutput: %s", err, output)
}
return string(output)
}The file /home/david/projects/code-associates/occam2go/codegen/e2e_helpers_test.go has been updated successfully.
package codegen
import "testing"
func TestE2EEntryHarnessEcho(t *testing.T) {
// An echoing program that reads characters until 'Z' and echoes each one.
// Uses the standard occam entry-point PROC signature.
input := `PROC echo(CHAN OF BYTE keyboard?, screen!, error!)
BYTE ch:
SEQ
keyboard ? ch
WHILE ch <> 'Z'
SEQ
screen ! ch
keyboard ? ch
:
`
// Pipe "hello Z" — the program should echo "hello " (everything before Z)
output := transpileCompileRunWithInput(t, input, "hello Z")
expected := "hello "
if output != expected {
t.Errorf("expected %q, got %q", expected, output)
}
}
File created successfully at: /home/david/projects/code-associates/occam2go/codegen/e2e_harness_test.go
go test ./codegen -run TestEntryHarnessRawTerminal -v 2>&1 | head -40
=== RUN TestEntryHarnessRawTerminal --- PASS: TestEntryHarnessRawTerminal (0.00s) PASS ok github.com/codeassociates/occam2go/codegen 0.001s
Unit test passes. Now the e2e test (this will be slower due to go mod tidy downloading deps).
go test ./codegen -run TestE2EEntryHarnessEcho -v 2>&1 | tail -30
=== RUN TestE2EEntryHarnessEcho --- PASS: TestE2EEntryHarnessEcho (0.59s) PASS ok github.com/codeassociates/occam2go/codegen 0.594s
# github.com/codeassociates/occam2go ./test.go:2689:6: main redeclared in this block ./main.go:29:6: other declaration of main FAIL github.com/codeassociates/occam2go [build failed] ? github.com/codeassociates/occam2go/ast [no test files] ok github.com/codeassociates/occam2go/codegen 19.107s 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
The only failure is the pre-existing test.go file in the repo root (noted in git status as untracked) which conflicts with main.go. That's unrelated to our changes. All the codegen, lexer, parser, modgen, and preproc tests pass.
Let me verify the course module still works too.
go build -o /tmp/occam2go . && /tmp/occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module && go vet /tmp/course_out.go 2>&1
Exit code 1 # github.com/codeassociates/occam2go ./test.go:2689:6: main redeclared in this block ./main.go:29:6: other declaration of main # github.com/codeassociates/occam2go ./test.go:2689:6: main redeclared in this block ./main.go:29:6: other declaration of main
go build -o /tmp/occam2go ./main.go && /tmp/occam2go -I kroc/modules/course/libsrc -D TARGET.BITS.PER.WORD=32 -o /tmp/course_out.go kroc/modules/course/libsrc/course.module && go vet /tmp/course_out.go 2>&1
All done. Here's a summary of the changes:
go.mod / go.sum — Added golang.org/x/term (and transitive golang.org/x/sys) dependency
codegen/codegen.go — Three areas changed:
needTerm bool field to Generator struct (line ~22) and initialized it in Generate() (line ~108)g.needTerm = true when entry harness is detected (line ~221)"os/signal", "syscall", and "golang.org/x/term" when needTerm is setRewrote generateEntryHarness() to:
term.MakeRaw() when stdin is a terminalbufio.NewReader when stdin is piped (non-terminal)\r before \n in screen/error writers when in raw mode (raw mode disables OPOST)codegen/codegen_test.go — Added TestEntryHarnessRawTerminal unit test verifying generated code contains term.MakeRaw, term.IsTerminal, term.Restore, and the new imports
codegen/e2e_helpers_test.go — Added transpileCompileRunWithInput() helper that initializes a Go module (for x/term dep), compiles, and pipes stdin to the binary
codegen/e2e_harness_test.go — New e2e test TestE2EEntryHarnessEcho that transpiles an echoing program, pipes "hello Z", and verifies output is "hello "
go vettest.go file, unrelated to these changesThat's great. I did a simple manual test and it looks to be working. Please create a PR for this new feature.
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/codegen_test.go modified: codegen/e2e_helpers_test.go modified: go.mod Untracked files: (use "git add <file>..." to include in what will be committed) codegen/e2e_harness_test.go go.sum od test.go no changes added to commit (use "git add" and/or "git commit -a")
diff --git a/codegen/codegen.go b/codegen/codegen.go
index 4211f74..e91bc12 100644
--- a/codegen/codegen.go
+++ b/codegen/codegen.go
@@ -20,6 +20,7 @@ type Generator struct {
needBufio bool // track if we need bufio package import
needReflect bool // track if we need reflect package import
needBoolHelper bool // track if we need _boolToInt helper
+ needTerm bool // track if we need golang.org/x/term package import
// Track procedure signatures for proper pointer handling
procSigs map[string][]ast.ProcParam
@@ -105,6 +106,7 @@ func (g *Generator) Generate(program *ast.Program) string {
g.needBufio = false
g.needReflect = false
g.needBoolHelper = false
+ g.needTerm = false
g.procSigs = make(map[string][]ast.ProcParam)
g.refParams = make(map[string]bool)
g.protocolDefs = make(map[string]*ast.ProtocolDecl)
@@ -217,6 +219,7 @@ func (g *Generator) Generate(program *ast.Program) string {
g.needOs = true
g.needSync = true
g.needBufio = true
+ g.needTerm = true
}
}
@@ -225,7 +228,7 @@ func (g *Generator) Generate(program *ast.Program) string {
g.writeLine("")
// Write imports
- if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect {
+ if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio || g.needReflect || g.needTerm {
g.writeLine("import (")
g.indent++
if g.needBufio {
@@ -243,15 +246,25 @@ func (g *Generator) Generate(program *ast.Program) string {
if g.needOs {
g.writeLine(`"os"`)
}
+ if g.needTerm {
+ g.writeLine(`"os/signal"`)
+ }
if g.needReflect {
g.writeLine(`"reflect"`)
}
if g.needSync {
g.writeLine(`"sync"`)
}
+ if g.needTerm {
+ g.writeLine(`"syscall"`)
+ }
if g.needTime {
g.writeLine(`"time"`)
}
+ if g.needTerm {
+ g.writeLine("")
+ g.writeLine(`"golang.org/x/term"`)
+ }
g.indent--
g.writeLine(")")
g.writeLine("")
@@ -415,7 +428,9 @@ func (g *Generator) findEntryProc(procDecls []ast.Statement) *ast.ProcDecl {
}
// generateEntryHarness emits a func main() that wires stdin/stdout/stderr
-// to channels and calls the entry PROC.
+// to channels and calls the entry PROC. When stdin is a terminal, the
+// harness switches to raw mode (via golang.org/x/term) so that keyboard
+// input is available character-by-character without waiting for Enter.
func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
name := goIdent(proc.Name)
g.writeLine("func main() {")
@@ -427,12 +442,41 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
g.writeLine("_error := make(chan byte, 256)")
g.writeLine("")
+ // Raw terminal mode setup
+ g.writeLine("// Raw terminal mode — gives character-at-a-time keyboard input")
+ g.writeLine("var rawMode bool")
+ g.writeLine("var oldState *term.State")
+ g.writeLine("fd := int(os.Stdin.Fd())")
+ g.writeLine("if term.IsTerminal(fd) {")
+ g.indent++
+ g.writeLine("var err error")
+ g.writeLine("oldState, err = term.MakeRaw(fd)")
+ g.writeLine("if err == nil {")
+ g.indent++
+ g.writeLine("rawMode = true")
+ g.writeLine("defer term.Restore(fd, oldState)")
+ g.writeLine("// Restore terminal on external signals")
+ g.writeLine("sigCh := make(chan os.Signal, 1)")
+ g.writeLine("signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)")
+ g.writeLine("go func() {")
+ g.indent++
+ g.writeLine("<-sigCh")
+ g.writeLine("term.Restore(fd, oldState)")
+ g.writeLine("os.Exit(1)")
+ g.indent--
+ g.writeLine("}()")
+ g.indent--
+ g.writeLine("}")
+ g.indent--
+ g.writeLine("}")
+ g.writeLine("")
+
// WaitGroup for writer goroutines to finish draining
g.writeLine("var wg sync.WaitGroup")
g.writeLine("wg.Add(2)")
g.writeLine("")
- // Screen writer goroutine
+ // Screen writer goroutine — in raw mode, insert CR before LF
g.writeLine("go func() {")
g.indent++
g.writeLine("defer wg.Done()")
@@ -445,6 +489,9 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
g.indent--
g.writeLine("} else {")
g.indent++
+ g.writeLine(`if rawMode && b == '\n' {`)
+ g.writeLine(`w.WriteByte('\r')`)
+ g.writeLine("}")
g.writeLine("w.WriteByte(b)")
g.indent--
g.writeLine("}")
@@ -455,7 +502,7 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
g.writeLine("}()")
g.writeLine("")
- // Error writer goroutine
+ // Error writer goroutine — same CR/LF handling
g.writeLine("go func() {")
g.indent++
g.writeLine("defer wg.Done()")
@@ -468,6 +515,9 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
g.indent--
g.writeLine("} else {")
g.indent++
+ g.writeLine(`if rawMode && b == '\n' {`)
+ g.writeLine(`w.WriteByte('\r')`)
+ g.writeLine("}")
g.writeLine("w.WriteByte(b)")
g.indent--
g.writeLine("}")
@@ -481,6 +531,30 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
// Keyboard reader goroutine
g.writeLine("go func() {")
g.indent++
+ g.writeLine("if rawMode {")
+ g.indent++
+ g.writeLine("buf := make([]byte, 1)")
+ g.writeLine("for {")
+ g.indent++
+ g.writeLine("n, err := os.Stdin.Read(buf)")
+ g.writeLine("if err != nil || n == 0 {")
+ g.indent++
+ g.writeLine("close(keyboard)")
+ g.writeLine("return")
+ g.indent--
+ g.writeLine("}")
+ g.writeLine("if buf[0] == 3 { // Ctrl+C")
+ g.indent++
+ g.writeLine("term.Restore(fd, oldState)")
+ g.writeLine("os.Exit(1)")
+ g.indent--
+ g.writeLine("}")
+ g.writeLine("keyboard <- buf[0]")
+ g.indent--
+ g.writeLine("}")
+ g.indent--
+ g.writeLine("} else {")
+ g.indent++
g.writeLine("r := bufio.NewReader(os.Stdin)")
g.writeLine("for {")
g.indent++
@@ -495,6 +569,8 @@ func (g *Generator) generateEntryHarness(proc *ast.ProcDecl) {
g.indent--
g.writeLine("}")
g.indent--
+ g.writeLine("}")
+ g.indent--
g.writeLine("}()")
g.writeLine("")
diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go
index 4882a43..486e9a5 100644
--- a/codegen/codegen_test.go
+++ b/codegen/codegen_test.go
@@ -852,3 +852,29 @@ func TestMultiDimProcParamCodegen(t *testing.T) {
t.Errorf("expected 'func fill(grid [][]chan int)' in output, got:\n%s", output)
}
}
+
+func TestEntryHarnessRawTerminal(t *testing.T) {
+ input := `PROC echo(CHAN OF BYTE keyboard?, screen!, error!)
+ BYTE ch:
+ SEQ
+ keyboard ? ch
+ screen ! ch
+:
+`
+ output := transpile(t, input)
+
+ // Should contain raw terminal mode setup
+ for _, want := range []string{
+ "term.IsTerminal",
+ "term.MakeRaw",
+ "term.Restore",
+ `"golang.org/x/term"`,
+ `"os/signal"`,
+ `"syscall"`,
+ "rawMode",
+ } {
+ if !strings.Contains(output, want) {
+ t.Errorf("expected %q in entry harness output, got:\n%s", want, output)
+ }
+ }
+}
diff --git a/codegen/e2e_helpers_test.go b/codegen/e2e_helpers_test.go
index 47f0f55..dae94ca 100644
--- a/codegen/e2e_helpers_test.go
+++ b/codegen/e2e_helpers_test.go
@@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"path/filepath"
+ "strings"
"testing"
"github.com/codeassociates/occam2go/lexer"
@@ -80,3 +81,69 @@ func transpileCompileRunFromFile(t *testing.T, mainFile string, includePaths []s
return transpileCompileRun(t, expanded)
}
+
+// transpileCompileRunWithInput takes Occam source that uses the entry-point
+// PROC pattern (CHAN OF BYTE keyboard?, screen!, error!), transpiles to Go,
+// initialises a Go module (needed for golang.org/x/term), compiles, pipes
+// the given input to stdin, and returns the stdout output.
+func transpileCompileRunWithInput(t *testing.T, occamSource, stdin string) string {
+ t.Helper()
+
+ // Transpile
+ l := lexer.New(occamSource)
+ p := parser.New(l)
+ program := p.ParseProgram()
+
+ if len(p.Errors()) > 0 {
+ for _, err := range p.Errors() {
+ t.Errorf("parser error: %s", err)
+ }
+ t.FailNow()
+ }
+
+ gen := New()
+ goCode := gen.Generate(program)
+
+ // Create temp directory
+ tmpDir, err := os.MkdirTemp("", "occam2go-test-*")
+ if err != nil {
+ t.Fatalf("failed to create temp dir: %v", err)
+ }
+ defer os.RemoveAll(tmpDir)
+
+ // Write Go source
+ goFile := filepath.Join(tmpDir, "main.go")
+ if err := os.WriteFile(goFile, []byte(goCode), 0644); err != nil {
+ t.Fatalf("failed to write Go file: %v", err)
+ }
+
+ // Initialise Go module (needed for golang.org/x/term dependency)
+ modInit := exec.Command("go", "mod", "init", "test")
+ modInit.Dir = tmpDir
+ if out, err := modInit.CombinedOutput(); err != nil {
+ t.Fatalf("go mod init failed: %v\n%s", err, out)
+ }
+ modTidy := exec.Command("go", "mod", "tidy")
+ modTidy.Dir = tmpDir
+ if out, err := modTidy.CombinedOutput(); err != nil {
+ t.Fatalf("go mod tidy failed: %v\n%s\nGo code:\n%s", err, out, goCode)
+ }
+
+ // Compile
+ binFile := filepath.Join(tmpDir, "main")
+ compileCmd := exec.Command("go", "build", "-o", binFile, ".")
+ compileCmd.Dir = tmpDir
+ if out, err := compileCmd.CombinedOutput(); err != nil {
+ t.Fatalf("compilation failed: %v\nOutput: %s\nGo code:\n%s", err, out, goCode)
+ }
+
+ // Run with piped stdin
+ runCmd := exec.Command(binFile)
+ runCmd.Stdin = strings.NewReader(stdin)
+ output, err := runCmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("execution failed: %v\nOutput: %s", err, output)
+ }
+
+ return string(output)
+}
diff --git a/go.mod b/go.mod
index 70c29f1..38cf26c 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,8 @@
module github.com/codeassociates/occam2go
go 1.25.6
+
+require (
+ golang.org/x/sys v0.41.0 // indirect
+ golang.org/x/term v0.40.0 // indirect
+)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) 3540129 Merge pull request #88 from codeassociates/fix/receive-indexed-target-85 58a1542 Support receive into indexed target variables (#85)
git add codegen/codegen.go codegen/codegen_test.go codegen/e2e_helpers_test.go codegen/e2e_harness_test.go go.mod go.sum
git commit -m "$(cat <<'EOF' Raw terminal mode for keyboard channel (#90) Use golang.org/x/term to switch stdin to raw mode in the generated entry harness so characters are available immediately as typed, without waiting for Enter. Falls back to buffered bufio.NewReader when stdin is not a terminal (piped input). Handles Ctrl+C in raw mode, LF→CRLF in output writers, and restores terminal state on signals. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> EOF )"
6 files changed, 207 insertions(+), 4 deletions(-) create mode 100644 codegen/e2e_harness_test.go create mode 100644 go.sum
remote: remote: Create a pull request for 'feat/raw-terminal-keyboard-90' on GitHub by visiting: remote: https://github.com/codeassociates/occam2go/pull/new/feat/raw-terminal-keyboard-90 remote: To associate-1.github.com:codeassociates/occam2go.git * [new branch] feat/raw-terminal-keyboard-90 -> feat/raw-terminal-keyboard-90 branch 'feat/raw-terminal-keyboard-90' set up to track 'origin/feat/raw-terminal-keyboard-90'.
gh pr create --title "Raw terminal mode for keyboard channel" --body "$(cat <<'EOF' ## Summary - Use `golang.org/x/term` to switch stdin to raw mode in the generated entry harness, so keyboard input is available character-by-character without waiting for Enter (#90) - Fall back to buffered `bufio.NewReader` when stdin is piped (non-terminal) - Handle Ctrl+C (byte 3) in raw mode to restore terminal and exit cleanly - Insert CR before LF in screen/error output writers when in raw mode (raw mode disables OPOST) - Restore terminal state on SIGINT/SIGTERM via signal handler ## Test plan - [x] Unit test verifies generated code contains `term.MakeRaw`, `term.IsTerminal`, `term.Restore`, and new imports - [x] E2E test transpiles an echoing program, pipes `"hello Z"` to stdin, verifies output matches `"hello "` - [x] All existing tests pass (`go test ./codegen ./lexer ./parser ./preproc ./modgen`) - [x] Course module still transpiles and passes `go vet` - [x] Manual test with `echoing.occ` — characters echo immediately on keypress Closes #90 🤖 Generated with [Claude Code](https://claude.com/claude-code) EOF )"