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.
// 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.ystruct Point x: int y: int
main() -> int // Heap allocation with a refcount header. Frees itself at rc = 0. val p = new Point(10, 20) // type: &Point
p.x + p.ystruct Point x: int y: int
main() -> int var p = Point(10, 20) val q: *Point = &p // raw, unmanaged
q.x + q.y // auto-deref on field accessref → 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 + 1Range-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..150type Even = int where value % 2 == 0Tagged 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 = 0x10000000const 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.
# Tree-walk interpreter — great for tests, development, REPL-style work.sysl run hello.syslFastest turnaround. Runs anywhere Scala runs (JVM, Node, native).
# Compile to TRISC assembly, assemble, link, run in the emulator.sysl compile hello.sysl --emit tof -o hello.toftrisc run hello.tofTRISC is a 16-bit RISC teaching ISA — five instruction formats, twenty exception vectors, predictable cycle counts. You can step through the generated code instruction by instruction.
# Compile to LLVM IR, link against a runtime, produce a native binary.sysl compile hello.sysl --backend llvm --emit ll -o hello.llclang hello.ll -o helloFull LLVM optimisation pipeline. Same source code, native performance.