Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 80 additions & 4 deletions codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -217,6 +219,7 @@ func (g *Generator) Generate(program *ast.Program) string {
g.needOs = true
g.needSync = true
g.needBufio = true
g.needTerm = true
}
}

Expand All @@ -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 {
Expand All @@ -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("")
Expand Down Expand Up @@ -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() {")
Expand All @@ -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()")
Expand All @@ -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("}")
Expand All @@ -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()")
Expand All @@ -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("}")
Expand All @@ -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++
Expand All @@ -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("")

Expand Down
26 changes: 26 additions & 0 deletions codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
25 changes: 25 additions & 0 deletions codegen/e2e_harness_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
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)
}
}
67 changes: 67 additions & 0 deletions codegen/e2e_helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

"github.com/codeassociates/occam2go/lexer"
Expand Down Expand Up @@ -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)
}
5 changes: 5 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=