Skip to content

codeassociates/occam2go

Repository files navigation

Preamble

Greetings humans. This paragraph is the only part of this repo generated by a human. Everything else (code, PRs, CI) was created by LLMs (so far Claude Code Opus 4.5 and 4.6). My goal as the human in this endevour was to see if I could refute posts I saw after Anthropic publicised their project to have Claude write a C compiler. I noticed many comments to the effect "well that's easy because there's many C compilers in the training data". I had the idea: Occam is so old that there's basically nothing about it on the modern internet. I had also had a long standing curiosity about the connection between Occam and go(lang) that made be curious as to whether you could build an Occam to golang transpiler. This project is the result: scratching two itches -- can an LLM create a working compiler for a language that doesn't show up in its training data, and can one transpile Occam to Go. Without Claude I'd never have the time to scratch that second itch. And...over to Claude:

occam2go

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.

Building

go build -o occam2go

Usage

./occam2go [options] <input.occ>
./occam2go gen-module [-o output] [-name GUARD] <SConscript>

Options:

  • -o <file> - Write output to file (default: stdout)
  • -I <path> - Include search path for #INCLUDE resolution (repeatable)
  • -D <SYMBOL> - Predefined preprocessor symbol (repeatable, supports SYMBOL=value)
  • -version - Print version and exit

Running an Example

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 -o output.go examples/print.occ

# 3. Compile the generated Go code
go build -o output output.go

# 4. Run the compiled program
./output

Or as a one-liner to see the output immediately:

./occam2go -o output.go examples/print.occ && go run output.go

Example

Input (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)
}

Codebase Size

~16,200 lines of code (excluding the vendored kroc/ subtree).

Category Lines
Production code 7,300
Tests 8,800
Occam examples 135
Total ~16,200

Largest files:

File Lines
parser/parser_test.go 3,124
parser/parser.go 2,811
codegen/codegen.go 2,617
codegen/codegen_test.go 669
ast/ast.go 520
lexer/lexer.go 448
preproc/preproc_test.go 437
E2E test files (18 files) 3,826

Implemented Features

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)

Channels

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

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

Records

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])

Arrays

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)

ALT (Alternation)

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

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

Built-in I/O Procedures

Occam Go
print.int(x) fmt.Println(x)
print.bool(x) fmt.Println(x)
print.string(x) fmt.Println(x)
print.newline() fmt.Println()

Preprocessor and Modules

Occam programs use #INCLUDE to import library modules. The transpiler includes a textual preprocessor that runs before lexing, handling conditional compilation and file inclusion.

Preprocessor Directives

Directive Description
#INCLUDE "file" Textually include a file (resolved relative to current file, then -I paths)
#DEFINE SYMBOL Define a preprocessor symbol
#IF expr Conditional compilation (TRUE, FALSE, DEFINED (SYM), NOT, (SYM = val))
#ELSE Alternative branch
#ENDIF End conditional block
#COMMENT, #PRAGMA, #USE Ignored (replaced with blank lines to preserve line numbers)

The predefined symbol TARGET.BITS.PER.WORD is set to 64 (Go always uses 64-bit integers).

Using Modules with #INCLUDE

Create a module file with include guards to prevent double-inclusion:

-- mathlib.module
#IF NOT (DEFINED (MATHLIB.MODULE))
#DEFINE MATHLIB.MODULE

INT FUNCTION abs(VAL INT x)
  INT result:
  VALOF
    IF
      x < 0
        result := 0 - x
      TRUE
        result := x
    RESULT result

#ENDIF

Then include it in your program:

-- main.occ
#INCLUDE "mathlib.module"

SEQ
  print.int(abs(0 - 42))

Transpile with -I to specify where to find the module:

./occam2go -I examples -o main.go examples/include_demo.occ
go run main.go

Output:

42
20
10

A working example is provided in examples/include_demo.occ with examples/mathlib.module.

Generating Module Files from KRoC SConscript

The KRoC project defines module composition in SConscript (Python) build files. The gen-module subcommand parses these to generate .module files:

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

# Generate a module file from SConscript
./occam2go gen-module kroc/modules/course/libsrc/SConscript

This outputs:

#IF NOT (DEFINED (COURSE.MODULE))
#DEFINE COURSE.MODULE
#INCLUDE "consts.inc"
#INCLUDE "utils.occ"
#INCLUDE "string.occ"
#INCLUDE "demo_cycles.occ"
#INCLUDE "demo_nets.occ"
#INCLUDE "file_in.occ"
#INCLUDE "float_io.occ"
#INCLUDE "random.occ"
#ENDIF

Running Programs with the Course Module

The KRoC course module 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.

# 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):

#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:

./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

Both Occam and Go draw from Tony Hoare's Communicating Sequential Processes (CSP) model, making channel communication a natural fit for transpilation.

Conceptual Mapping

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 <-

Synchronous Communication

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.

Differences and Limitations

  1. Channel direction: Occam channels are inherently unidirectional. Go channels can be bidirectional but can be restricted using types (chan<- for send-only, <-chan for receive-only). The transpiler currently generates bidirectional Go channels.

  2. Protocol types: Simple, sequential, and variant protocols are supported. Nested protocols (protocols referencing other protocols) are not yet supported.

  3. Channel arrays: Channel arrays ([n]CHAN OF TYPE) are supported, including indexed send/receive, []CHAN OF TYPE proc params, and ALT with indexed channels.

  4. ALT construct: Occam's ALT maps to Go's select statement. Basic ALT, guards, and timer timeouts are supported. Priority ALT (PRI ALT) and replicated ALT are not yet implemented.

How PAR is Mapped

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.

Basic PAR

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.

Replicated PAR

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()

Differences and Limitations

  1. 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 PAR branches may behave differently.

  2. 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.

  3. PLACED PAR: Occam's PLACED PAR for assigning processes to specific Transputer links or processors is not supported.

How Timers are Mapped

Occam's TIMER provides access to a hardware clock. The transpiler maps timer operations to Go's time package.

Timer Declaration

Timer declarations are no-ops in the generated code since Go accesses time through the time package directly:

TIMER tim:

Generates:

// TIMER tim

Reading the Current Time

A 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 Timeouts in ALT

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.

AFTER as a Boolean Expression

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
}

Differences and Limitations

  1. 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.

  2. Guarded timer ALT: guard & tim ? AFTER deadline (timer cases with boolean guards) is not yet supported.

  3. Clock wraparound: Occam's AFTER operator handles 32-bit clock wraparound correctly. The transpiler uses a simple > comparison, which does not handle wraparound.

About

An Occam to Golang transpiler

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •