A transpiler from Occam to Go, written in Go.
Occam was developed in the 1980s to support concurrent programming on the Transputer. Go, created decades later, shares similar CSP-influenced concurrency primitives. This transpiler bridges the two.
go build -o occam2go./occam2go [options] <input.occ>Options:
-o <file>- Write output to file (default: stdout)-version- Print version and exit
Here's how to transpile, compile, and run an Occam program:
# 1. Build the transpiler (only needed once)
go build -o occam2go
# 2. Transpile an Occam file to Go
./occam2go examples/print.occ -o output.go
# 3. Compile the generated Go code
go build -o output output.go
# 4. Run the compiled program
./outputOr as a one-liner to see the output immediately:
./occam2go examples/print.occ -o output.go && go run output.goInput (example.occ):
SEQ
INT x, y:
PAR
x := 1
y := 2
x := x + y
Output:
package main
import (
"sync"
)
func main() {
var x, y int
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
x = 1
}()
go func() {
defer wg.Done()
y = 2
}()
wg.Wait()
x = (x + y)
}See TODO.md for the full implementation status and roadmap.
| Occam | Go |
|---|---|
INT, BYTE, BOOL, REAL |
int, byte, bool, float64 |
SEQ |
Sequential code |
PAR |
Goroutines with sync.WaitGroup |
IF |
if / else if |
WHILE |
for loop |
STOP |
Print to stderr + select {} (deadlock) |
PROC with VAL params |
Functions with value/pointer params |
:= assignment |
= assignment |
Arithmetic: +, -, *, /, \ |
+, -, *, /, % |
Comparison: =, <>, <, >, <=, >= |
==, !=, <, >, <=, >= |
Logic: AND, OR, NOT |
&&, ||, ! |
Bitwise: /\, \/, ><, ~ |
&, |, ^, ^ (AND, OR, XOR, NOT) |
Shifts: <<, >> |
<<, >> |
Type conversions: INT x, BYTE n |
int(x), byte(n) |
| Occam | Go |
|---|---|
CHAN OF INT c: |
c := make(chan int) |
c ! x (send) |
c <- x |
c ? y (receive) |
y = <-c |
[5]CHAN OF INT cs: |
cs := make([]chan int, 5) + init loop |
cs[i] ! x (indexed send) |
cs[i] <- x |
cs[i] ? y (indexed receive) |
y = <-cs[i] |
PROC f([]CHAN OF INT cs) |
func f(cs []chan int) |
Example:
SEQ
CHAN OF INT c:
INT result:
PAR
c ! 42
c ? result
print.int(result)
Channel array example:
SEQ
[3]CHAN OF INT cs:
INT sum:
sum := 0
PAR
PAR i = 0 FOR 3
cs[i] ! (i + 1) * 10
SEQ i = 0 FOR 3
INT x:
cs[i] ? x
sum := sum + x
print.int(sum)
Protocols define the type of data carried on a channel. Three forms are supported:
| Occam | Go |
|---|---|
PROTOCOL SIG IS INT |
type _proto_SIG = int |
PROTOCOL PAIR IS INT ; BYTE |
type _proto_PAIR struct { _0 int; _1 byte } |
PROTOCOL MSG CASE tag; INT ... |
Interface + concrete structs per tag |
c ! 42 ; 65 (sequential send) |
c <- _proto_PAIR{42, 65} |
c ? x ; y (sequential recv) |
_tmp := <-c; x = _tmp._0; y = _tmp._1 |
c ! tag ; val (variant send) |
c <- _proto_MSG_tag{val} |
c ? CASE ... (variant recv) |
switch _v := (<-c).(type) { ... } |
Sequential protocol example:
PROTOCOL PAIR IS INT ; INT
SEQ
CHAN OF PAIR c:
INT x, y:
PAR
c ! 10 ; 20
c ? x ; y
print.int(x + y)
Variant protocol example:
PROTOCOL MSG
CASE
data; INT
quit
SEQ
CHAN OF MSG c:
INT result:
PAR
c ! data ; 42
c ? CASE
data ; result
print.int(result)
quit
SKIP
| Occam | Go |
|---|---|
RECORD POINT with INT x: INT y: |
type POINT struct { x int; y int } |
POINT p: |
var p POINT |
p[x] := 10 |
p.x = 10 |
p[x] (in expression) |
p.x |
PROC foo(POINT p) (ref) |
func foo(p *POINT) |
PROC foo(VAL POINT p) (val) |
func foo(p POINT) |
Example:
RECORD POINT
INT x:
INT y:
SEQ
POINT p:
p[x] := 10
p[y] := 20
print.int(p[x] + p[y])
| Occam | Go |
|---|---|
[5]INT arr: |
arr := make([]int, 5) |
arr[i] := x |
arr[i] = x |
x := arr[i] |
x = arr[i] |
Example:
SEQ
[5]INT arr:
SEQ i = 0 FOR 5
arr[i] := (i + 1) * 10
INT sum:
sum := 0
SEQ i = 0 FOR 5
sum := sum + arr[i]
print.int(sum)
| Occam | Go |
|---|---|
ALT |
select |
guard & c ? x |
Conditional channel with nil pattern |
SEQ i = 0 FOR n |
for i := 0; i < n; i++ |
PAR i = 0 FOR n |
Parallel for loop with goroutines |
Example:
ALT
c1 ? x
print.int(x)
c2 ? y
print.int(y)
Generates:
select {
case x = <-c1:
fmt.Println(x)
case y = <-c2:
fmt.Println(y)
}ALT with guards (optional boolean conditions):
ALT
enabled & c1 ? x
process(x)
TRUE & c2 ? y
process(y)
Replicators allow you to repeat a block of code a specified number of times.
| Occam | Go |
|---|---|
SEQ i = 0 FOR n |
for i := 0; i < n; i++ |
PAR i = 0 FOR n |
Parallel for loop with goroutines |
Example with replicated SEQ:
SEQ i = 1 FOR 5
print.int(i)
This prints 1, 2, 3, 4, 5.
Example with replicated PAR (spawns n concurrent processes):
PAR i = 0 FOR 4
c ! i
| Occam | Go |
|---|---|
print.int(x) |
fmt.Println(x) |
print.bool(x) |
fmt.Println(x) |
print.string(x) |
fmt.Println(x) |
print.newline() |
fmt.Println() |
Both Occam and Go draw from Tony Hoare's Communicating Sequential Processes (CSP) model, making channel communication a natural fit for transpilation.
In Occam, channels are the primary mechanism for communication between parallel processes. A channel is a synchronous, unbuffered, point-to-point connection. Go channels share these characteristics by default.
| Concept | Occam | Go |
|---|---|---|
| Declaration | CHAN OF INT c: |
c := make(chan int) |
| Send (blocks until receiver ready) | c ! value |
c <- value |
| Receive (blocks until sender ready) | c ? variable |
variable = <-c |
| Synchronisation | Implicit in ! and ? |
Implicit in <- |
Both languages use synchronous (rendezvous) communication by default:
PAR
c ! 42 -- blocks until receiver is ready
c ? x -- blocks until sender is ready
The sender and receiver must both be ready before the communication occurs. This is preserved in the generated Go code, where unbuffered channels have the same semantics.
-
Channel direction: Occam channels are inherently unidirectional. Go channels can be bidirectional but can be restricted using types (
chan<-for send-only,<-chanfor receive-only). The transpiler currently generates bidirectional Go channels. -
Protocol types: Simple, sequential, and variant protocols are supported. Nested protocols (protocols referencing other protocols) are not yet supported.
-
Channel arrays: Channel arrays (
[n]CHAN OF TYPE) are supported, including indexed send/receive,[]CHAN OF TYPEproc params, and ALT with indexed channels. -
ALT construct: Occam's
ALTmaps to Go'sselectstatement. Basic ALT, guards, and timer timeouts are supported. Priority ALT (PRI ALT) and replicated ALT are not yet implemented.
Occam's PAR construct runs processes truly in parallel. On the Transputer this was hardware-scheduled; in Go it maps to goroutines coordinated with a sync.WaitGroup.
Each branch of a PAR block becomes a goroutine. The transpiler inserts a WaitGroup to ensure all branches complete before execution continues:
PAR
c ! 42
c ? x
Generates:
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
c <- 42
}()
go func() {
defer wg.Done()
x = <-c
}()
wg.Wait()The wg.Wait() call blocks until all goroutines have finished, preserving Occam's semantics that execution only continues after all parallel branches complete.
A replicated PAR spawns N concurrent processes using a loop. Each iteration captures the loop variable to avoid closure issues:
PAR i = 0 FOR 4
c ! i
Generates:
var wg sync.WaitGroup
wg.Add(int(4))
for i := 0; i < 0 + 4; i++ {
i := i // capture loop variable
go func() {
defer wg.Done()
c <- i
}()
}
wg.Wait()-
Scheduling: Occam on the Transputer had deterministic, priority-based scheduling. Go's goroutine scheduler is preemptive and non-deterministic. Programs that depend on execution order between
PARbranches may behave differently. -
Shared memory: Occam enforces at compile time that parallel processes do not share variables (the "disjointness" rule). The transpiler does not enforce this, so generated Go code may contain data races if the original Occam would have been rejected by a full Occam compiler.
-
PLACED PAR: Occam's
PLACED PARfor assigning processes to specific Transputer links or processors is not supported.
Occam's TIMER provides access to a hardware clock. The transpiler maps timer operations to Go's time package.
Timer declarations are no-ops in the generated code since Go accesses time through the time package directly:
TIMER tim:
Generates:
// TIMER timA timer read stores the current time as an integer (microseconds since epoch):
TIMER tim:
INT t:
tim ? t
Generates:
// TIMER tim
var t int
t = int(time.Now().UnixMicro())Timer cases in ALT allow a process to wait until a deadline. This maps to Go's time.After inside a select:
TIMER tim:
INT t:
tim ? t
ALT
c ? x
process(x)
tim ? AFTER (t + 100000)
handle.timeout()
Generates:
// TIMER tim
var t int
t = int(time.Now().UnixMicro())
select {
case x = <-c:
process(x)
case <-time.After(time.Duration((t + 100000) - int(time.Now().UnixMicro())) * time.Microsecond):
handle_timeout()
}The deadline expression (t + 100000) represents an absolute time. The generated code computes the remaining duration by subtracting the current time.
The AFTER operator compares two time values and evaluates to true if the left operand is later than the right. It maps to >:
IF
t2 AFTER t1
-- t2 is later
Generates:
if (t2 > t1) {
// t2 is later
}-
Clock resolution: Occam timers are hardware-dependent (often microsecond resolution on the Transputer). The transpiler uses
time.Now().UnixMicro()for microsecond values, but actual resolution depends on the OS. -
Guarded timer ALT:
guard & tim ? AFTER deadline(timer cases with boolean guards) is not yet supported. -
Clock wraparound: Occam's
AFTERoperator handles 32-bit clock wraparound correctly. The transpiler uses a simple>comparison, which does not handle wraparound.