Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SSPUR for Go and Rust programmers

If you write Go or Rust, you already expect static types, explicit errors and native speed. SSPUR keeps those and changes the defaults: values are immutable and their memory is managed for you (freed at last use when the compiler can prove a value has one owner, collected otherwise), errors are an effect instead of a return value, and every side effect a function can perform is in its type. Ownership exists, but only in the opt-in sys profile for code that manages real resources.

What maps to what

GoRustSSPUR
int64i64Int; overflow traps in every build
structstructrecord type {x: Int, y: Int}
interface + type switchenumsum type A | B{f: Int} with match
(T, error)Result<T, E> and ?T ! fail[E]; errors propagate without ?, and catch handles them
nilOption<T>Opt[T]
interfacetraittrait and impl
generics with constraints<T: Ord>[T: Ord], specialized per type like Rust
go f() + sync.WaitGroupstd::thread::scopepar(a, b) and for x in par(xs)
chan Tmpsc::channelChan[T] under the conc effect
sync/atomicAtomicI64Atomic[Int]
cgoextern "C"extern fn ... ! ffi
defer f.Close()Dropres type T = {..} drop close in profile sys
go test#[test]test name = expr next to the code
go.sumCargo.locksspur.lock, pinned by the hash of each package’s exports

Errors

func parsePort(s string) (int, error) {
	t := strings.TrimSpace(s)
	if t == "" {
		return 0, ErrEmpty
	}
	n, err := strconv.Atoi(t)
	if err != nil || n < 1 || n > 65535 {
		return 0, fmt.Errorf("bad port %q", t)
	}
	return n, nil
}
type ParseErr = Empty | BadNumber{text: Str}

fn parse_port(s: Str) -> Int ! fail[ParseErr]
= do
  t = s.trim
  if t.is_empty then raise Empty
  n = t.to_int.ok_or(BadNumber{text: t})
  if n < 1 or n > 65535 then raise BadNumber{text: t}
  n

fn port_or_default(s: Str) -> Int
= catch parse_port(s)
  | Empty => 8080
  | BadNumber => 0

There is no if err != nil and no ?: a call to a function that raises E raises E too, and the checker requires the caller to declare fail[E] or catch it. A catch that covers every variant removes the effect, so port_or_default is infallible.

Traits and generics

trait Area
  fn area(s: Self) -> Int

type Square = {side: Int} derive Eq, Show

impl Area for Square
  fn area(s: Square) -> Int = s.side * s.side

fn total[T: Area](xs: List[T]) -> Int
= xs.map(area).sum

As in Rust, trait calls resolve statically and bounded functions are compiled once per type, so there is no dynamic dispatch cost. Coherence works like Rust’s orphan rule: an impl lives with its trait or its type.

Concurrency

fn sum_range(lo: Int, hi: Int) -> Int
= (lo..hi).sum

fn parallel_sum(n: Int) -> Int
= do
  (a, b) = par(sum_range(0, n / 2), sum_range(n / 2, n))
  a + b

par is scoped: every task finishes before par returns. The checker rejects a task that assigns a variable from outside it, so data races don’t compile, without lifetimes. Pure map and filter pipelines are parallelized automatically.

$ sspur test from-go-rust.ssp
3 passed, 0 failed

Performance and systems code

sspur run compiles to native code through C and clang. Bounds, overflow and contract checks are always on, and the compiler proves most of them away. On the native benchmarks it runs in 0.14x to 0.89x the time of C++ with the same checks, with peak memory near or below C++ on every benchmark but the GPU one.

For code that owns file descriptors or raw memory, profile sys adds move-only resources with destructors, borrows (&T, &mut T as parameter types only), Ptr[T], inline asm and fixed arrays. profile bare goes further, to kernels and firmware with no runtime. The bare-metal tutorial builds one, and C interop calls C both ways.