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
65 changes: 64 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,70 @@ This outputs:
#ENDIF
```

> **Note:** The preprocessor and module infrastructure is in place, but the KRoC course module source files themselves use several occam features not yet supported by the transpiler (abbreviations like `VAL INT x IS 1:`, `CHAN BYTE` without `OF`, `VAL []BYTE` array slice params, `SIZE`, `:` PROC terminators). Full course module transpilation is a future goal. See [TODO.md](TODO.md) for the implementation roadmap.
### Running Programs with the Course Module

The KRoC [course module](https://www.cs.kent.ac.uk/projects/ofa/kroc/) is a standard occam library providing I/O utilities (`out.string`, `out.int`, `out.repeat`, etc.) for character-level communication over byte channels. The transpiler fully supports it.

Occam programs that follow the standard entry point pattern — a PROC with three `CHAN BYTE` parameters `(keyboard?, screen!, error!)` — automatically get a generated `main()` that wires stdin, stdout, and stderr to channels.

```bash
# 1. Clone the KRoC repository (one-time setup)
./scripts/clone-kroc.sh

# 2. Build the transpiler
go build -o occam2go

# 3. Transpile an example that uses the course module
./occam2go -I kroc/modules/course/libsrc \
-D TARGET.BITS.PER.WORD=32 \
-o hello.go examples/course_hello.occ

# 4. Run it
go run hello.go
```

Output:
```
Hello from occam2go!
The answer is: 42
------------------------------
Counting: 1, 2, 3, 4, 5
```

The `-I` flag tells the preprocessor where to find the course module source files, and `-D TARGET.BITS.PER.WORD=32` sets the word size expected by the course module (the transpiler defaults to 64).

The example program (`examples/course_hello.occ`):
```occam
#INCLUDE "course.module"

PROC hello (CHAN BYTE keyboard?, screen!, error!)
SEQ
out.string ("Hello from occam2go!*c*n", 0, screen!)
out.string ("The answer is: ", 0, screen!)
out.int (42, 0, screen!)
out.string ("*c*n", 0, screen!)
out.repeat ('-', 30, screen!)
out.string ("*c*n", 0, screen!)
out.string ("Counting: ", 0, screen!)
SEQ i = 1 FOR 5
SEQ
IF
i > 1
out.string (", ", 0, screen!)
TRUE
SKIP
out.int (i, 0, screen!)
out.string ("*c*n", 0, screen!)
:
```

You can also transpile the KRoC examples directly:
```bash
./occam2go -I kroc/modules/course/libsrc \
-D TARGET.BITS.PER.WORD=32 \
-o hello_world.go kroc/modules/course/examples/hello_world.occ
go run hello_world.go
```

## How Channels are Mapped

Expand Down
209 changes: 173 additions & 36 deletions codegen/codegen.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Generator struct {
needOs bool // track if we need os package import
needMath bool // track if we need math package import
needMathBits bool // track if we need math/bits package import
needBufio bool // track if we need bufio package import

// Track procedure signatures for proper pointer handling
procSigs map[string][]ast.ProcParam
Expand Down Expand Up @@ -93,6 +94,7 @@ func (g *Generator) Generate(program *ast.Program) string {
g.needOs = false
g.needMath = false
g.needMathBits = false
g.needBufio = 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 @@ -142,42 +144,6 @@ func (g *Generator) Generate(program *ast.Program) string {
g.collectRecordVars(stmt)
}

// Write package declaration
g.writeLine("package main")
g.writeLine("")

// Write imports
if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits {
g.writeLine("import (")
g.indent++
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.needSync {
g.writeLine(`"sync"`)
}
if g.needTime {
g.writeLine(`"time"`)
}
g.indent--
g.writeLine(")")
g.writeLine("")
}

// Emit transputer intrinsic helper functions
if g.needMathBits {
g.emitIntrinsicHelpers()
}

// Separate protocol, record, procedure declarations from other statements
var typeDecls []ast.Statement
var procDecls []ast.Statement
Expand Down Expand Up @@ -220,6 +186,56 @@ func (g *Generator) Generate(program *ast.Program) string {
}
}

// Detect entry point PROC so we can set import flags before writing imports
var entryProc *ast.ProcDecl
if len(mainStatements) == 0 {
entryProc = g.findEntryProc(procDecls)
if entryProc != nil {
g.needOs = true
g.needSync = true
g.needBufio = true
}
}

// Write package declaration
g.writeLine("package main")
g.writeLine("")

// Write imports
if g.needSync || g.needFmt || g.needTime || g.needOs || g.needMath || g.needMathBits || g.needBufio {
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.needSync {
g.writeLine(`"sync"`)
}
if g.needTime {
g.writeLine(`"time"`)
}
g.indent--
g.writeLine(")")
g.writeLine("")
}

// Emit transputer intrinsic helper functions
if g.needMathBits {
g.emitIntrinsicHelpers()
}

// Generate type definitions first (at package level)
for _, stmt := range typeDecls {
g.generateStatement(stmt)
Expand Down Expand Up @@ -265,6 +281,8 @@ func (g *Generator) Generate(program *ast.Program) string {
g.nestingLevel--
g.indent--
g.writeLine("}")
} else if entryProc != nil {
g.generateEntryHarness(entryProc)
}

return g.builder.String()
Expand Down Expand Up @@ -336,6 +354,125 @@ func (g *Generator) collectNestedProcSigsScoped(stmts []ast.Statement, oldSigs m
}
}

// findEntryProc looks for the last top-level PROC with the standard occam
// entry point signature: exactly 3 CHAN OF BYTE params (keyboard?, screen!, error!).
func (g *Generator) findEntryProc(procDecls []ast.Statement) *ast.ProcDecl {
var entry *ast.ProcDecl
for _, stmt := range procDecls {
proc, ok := stmt.(*ast.ProcDecl)
if !ok {
continue
}
if len(proc.Params) != 3 {
continue
}
p0, p1, p2 := proc.Params[0], proc.Params[1], proc.Params[2]
if p0.IsChan && p0.ChanElemType == "BYTE" && p0.ChanDir == "?" &&
p1.IsChan && p1.ChanElemType == "BYTE" && p1.ChanDir == "!" &&
p2.IsChan && p2.ChanElemType == "BYTE" && p2.ChanDir == "!" {
entry = proc
}
}
return entry
}

// 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("}")
}

func (g *Generator) containsPar(stmt ast.Statement) bool {
switch s := stmt.(type) {
case *ast.ParBlock:
Expand Down
11 changes: 11 additions & 0 deletions codegen/codegen_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ func TestStringLiteral(t *testing.T) {
}
}

func TestStringEscapeCodegen(t *testing.T) {
input := `x := "hello*c*n"
`
output := transpile(t, input)

// The *c*n should become \r\n in the Go output (via %q formatting)
if !strings.Contains(output, `x = "hello\r\n"`) {
t.Errorf("expected string with \\r\\n escape, got:\n%s", output)
}
}

func TestByteLiteral(t *testing.T) {
input := "x := 'A'\n"
output := transpile(t, input)
Expand Down
Loading
Loading