Skip to content

Sysl

A systems language for people who want to know where their bytes live. Value types, ref-counted refs, and raw pointers — three ways to own memory, one language from boot ROM to userspace.
// A tagged union, pattern matching, and refcounted recursion —
// just enough to evaluate an arithmetic expression.
enum Expr
Lit(value: int)
Add(left: &Expr, right: &Expr)
Mul(left: &Expr, right: &Expr)
eval(e: &Expr) -> int
*e match
Lit(v) -> v
Add(l, r) -> eval(l) + eval(r)
Mul(l, r) -> eval(l) * eval(r)
main() -> int
// (1 + 2) * 3 = 9. Refs are freed automatically when scope ends.
val tree = new Mul(new Add(new Lit(1), new Lit(2)), new Lit(3))
eval(tree)

No allocator needed for the stack frames. No garbage collector. The &Expr refs carry a ref-count header and free themselves when they fall out of scope. Swap & for * when you need a raw pointer; swap new for a stack var when you don’t need the heap at all.


Kernels and drivers

volatile for MMIO, non-escaping closures that live on the stack, inline assembly, and a calling convention you can read. Sysl is the language the SLIX microkernel is written in.

Compilers and tools

Generic tagged unions, exhaustive match, ? on Result[T, E], and a literate programming format (.lsysl) make it natural to write parsers, type-checkers, and backends in a single file that’s half prose, half code.

Teaching systems

Sysl targets TRISC, a 16-bit RISC teaching ISA with a deterministic emulator. You can see exactly which assembly instructions every language feature lowers to — or switch the backend to LLVM for native speed.

Embedded and bare metal

Opt out of the heap entirely. Non-escaping closures capture on the caller’s stack. Raw pointers, fixed arrays, and const-folded compile-time arithmetic give you C-level control without C’s footguns.


The same struct works three different ways at the use site. Pick the one that fits the problem — the type system tracks which is which, and refuses to mix them by accident.

struct Point
x: int
y: int
main() -> int
// Bitwise copy, lives on the stack, no refcount, no allocator.
var p = Point(10, 20)
p.x + p.y
  • ref → pointer is &r (unsafe, no refcount change).
  • pointer → ref is always an error — you can’t manufacture a refcount.
  • value → ref is new Point(v) — one call, one free.

Design by contract

require, ensure, loop variant / invariant, struct invariant, old(expr) to capture values at function entry — all checked at runtime by default, strippable with --no-contracts.

increment(p: *int)
ensure *p == old(*p) + 1
*p = *p + 1

Range-typed integers

Ada-style within constraints and where predicates on any numeric type, plus T::First, T::Succ, T::Valid, and the rest of the attribute family.

type Age = int within 0..150
type Even = int where value % 2 == 0

Tagged unions + `?`

Rust-style enums with exhaustiveness checking and a postfix ? that unwraps Some / Ok or early-returns the failure variant.

parse_pair(s: string) -> Option[int]
val a = parseInt(s, 0)?
val b = parseInt(s, 3)?
Some(a + b)

`not null` pointers

A subtype of *T with a runtime check at every produce site. Use it at API boundaries; inside, you get the same machine code.

head(xs: *Node not null) -> int
xs.value

`#pure` enforcement

Mark a function #pure and the compiler statically checks it performs no observable side effects. Great for constant folding and for knowing your hashing code can’t accidentally allocate.

Literate programming

.lsysl files are Markdown with indented code blocks. The prose is real prose — bold, tables, KaTeX, block quotes — and the tangled code compiles identically to .sysl.


Sysl’s volatile keyword on struct fields stops the compiler from coalescing or reordering loads and stores. Combined with a *T not null cast from an integer address, it gives you the register-level control kernels need — without assembly.

struct UartRegs
volatile status: u32
volatile data: u32
baud: int // normal field — optimisable
const UART_BASE = 0x10000000
const TX_READY = 0x20
uart_putc(c: int)
val regs = *UartRegs not null(UART_BASE)
while (regs.status & TX_READY) == 0 do { } // poll until ready
regs.data = u32(c)
puts(s: string)
for c in s do uart_putc(int(c))

Swap TX_READY and the register layout for your device. Swap the backend to LLVM if you want an x86 or ARM binary, or stay on TRISC to run it in the teaching emulator.


The standard library in trisc/std/ is written in literate Sysl — you can read the prose and the implementation in the same file.


Terminal window
# Tree-walk interpreter — great for tests, development, REPL-style work.
sysl run hello.sysl

Fastest turnaround. Runs anywhere Scala runs (JVM, Node, native).