Cutlass is MerTek's systems language, and the language BenthOS, MerFS and the platform's lowest layers are written in. It takes Trident's grammar and kinds, compiles straight to machine code for a declared target, links no C library and has no undefined behavior.
Worked example
A probe service whose memory is known before it ships
Below the Trident application on each pump-station gateway runs a small Cutlass service. It reads the chlorine probe, calibrates each raw count and hands batches up to the host. It runs for months without a reboot, so it must not run out of memory, race between threads or read past a buffer.
01
Calibration is an ordinary function that can fail. A raw count below the probe's offset fails with a named error. Every caller writes what happens then: else use, else fail, or an arm that inspects the error.
# A raw probe count below the offset is a fault the caller must handle.
enum Probe { belowOffset = 1 }
u32 calibrate(u32 count, u32 offset) {
if (count < offset) { fail Probe.belowOffset }
return count - offset
}
export u8 main() {
u32 good = calibrate(4120, 120) else use 0
u32 low = calibrate(80, 120) else (e) {
when (e) {
is Probe.belowOffset { use 0 }
else { use 1 }
}
}
if (good != 4000 || low != 0) { return 1 }
return 0
}
02
Each batch lives in an explicit region bounded at 64 KiB. The loop pass and the function body are regions too. When the block ends, the region's chunks go back in one splice, so there is no free to forget.
# One batch of calibrated readings lives in a region bounded at 64 KiB.
u64 total(u32[] readings) {
u64 sum = 0
foreach (r in readings) {
sum = sum + u64(r)
}
return sum
}
u64 batchTotal() {
u64 sum = 0
region (bytes = 64KiB) {
u32[] readings = [4000, 3975, 4013]
readings.add(3981)
sum = total(readings) else fail
}
return sum
}
03
The sampler and the uplink run on declared threads joined by a bounded channel of 256 readings. The compiler knows every sender and receiver, so it picks the ring, and the author never names a queue type.
Every build prints each function's frame and each context's memory and root figures. The team reads the memory a gateway reserves before the image ships.
The service ran on 300 gateways for 90 days without a fault.memory; every region's peak was bounded and printed at build time. A later edit that let the sampler and the uplink both write one counter was refused at compile time. The message named the four declared forms that make sharing legal.
What Cutlass is
Cutlass is MerTek's systems language. It takes Trident's grammar and kinds wherever both languages have the construct. A struct, an enum, a foreach or an else arm reads the same in both. Its semantics, lowering and machine types are its own.
A Cutlass program compiles straight to machine code for a declared target and links with nothing: no C library and no hidden runtime. Memory is regions, devices and interrupts are declarations, and every thread a program will ever run is known at compile time.
No undefined behavior: overflow fails with fault.overflow, a bad conversion with fault.convert, an index past the end with fault.range.
Errors are values. A function's error set is inferred, and every call to a function that can fail takes an else arm.
Unchecked operations live in raw blocks, so a reviewer finds every one by searching for one word.
A target description names the architecture, the environment and the privilege, so one source builds a hosted program, a loader or a kernel.
Cutlass
The host surface without a C library
cut.host reserves memory and writes output through the host's own kernel calls, never through libc. A refusal is a named error the caller must handle.
export enum HostError with { width = u8 } {
refused = 1
}
# The Windows x64 arm: reserve address space, commit nothing yet.
export uword rootMemory(uword bytes) fails HostError {
uword base = cutarch.windows.x64.VirtualAlloc(0, bytes, 0x2000, 1)
if (base == 0) {
fail HostError.refused
}
return base
}
The compiler builds itself
The bootstrap compiler is written in C. It is one library whose areas run in a fixed order: lexer, parser, checker, lowering, the x64 code emitter, the object writers and the driver. A later area reads an earlier one and never the reverse. Nothing recurses, so the parser keeps an explicit stack with a declared depth.
The bootstrap compiler builds a compiler written in a subset of Cutlass, and that compiler builds the full Cutlass compiler. The full compiler then compiles itself and must reproduce itself byte for byte. Past that fixed point, the C and subset compilers are frozen inputs. Anyone with a host C compiler and the source can rebuild the whole chain without a saved binary.
Programs link with nothing. The object writers emit Linux ELF and Windows PE executables directly. A hosted program's only foreign calls are the rows of its host's closed import registry.
Lexer and parser
Source text becomes tokens, then a syntax tree with byte positions on every node. A refusal names the repair.
Checker
Names, types, initialization, region escapes, inferred error and effect sets, the data-race rule, stack and memory figures, and an acyclic call graph.
Lowering and emission
One internal form for every back end, then instruction selection, register allocation and encoding per architecture.
Object writers and driver
Executables, archives and placed images, plus the interface record each library publishes to the programs that link it.
cutarch: the machine floor
cutarch holds what differs per machine: each architecture's intrinsics, each target's start-up unit, and each host's kernel-entry mechanism. An intrinsic is a function declared with no body. The compiler owns its lowering, and one registry row states its signature, privilege and effects.
Architecture code is fenced. A reach into cutarch.x64 is admitted only inside an arm or unit that names x64. Code that compiles on one architecture therefore compiles on every one its library supports. Programs reach the machine floor only through cut and cutrun; a direct reach is refused.
The same registries name wider machines: arm64 and rv64 are spelled beside x64. SIMD lanes, atomics with ordering, MMIO, system registers and interrupt tables each have a typed spelling with declared effects.
Cutlass
Intrinsics are declarations with no body
Each line is a whole declaration. The signature must match its registry row exactly, or the checker refuses it.
# The time-stamp counter's 64-bit count, ordered against no instruction
# around it.
export intrinsic u64 rdtsc()
# The spin-wait hint.
export intrinsic void pause()
# The system-call instruction: the service number and six argument words in,
# in the Linux kernel's registers, and the kernel's result word out.
export intrinsic u64 systemCall(u64 number, u64 a0, u64 a1, u64 a2, u64 a3, u64 a4, u64 a5)
C
One row for the checker, one for the back end
rdtsc is user-privileged and carries the machine effect, so a function marked intent deterministic cannot call it. The x64 back end lowers each call to three instructions in place.
/* the checker's row: id, name, module, member, signature, arch, privilege, effects, meaning */
CUTLASS_CHECK_INTRINSIC(4, RDTSC, "cutarch.x64", "rdtsc", "u64()", "x64", "user", "machine", "the time-stamp counter's 64-bit count, read with no ordering against the instructions around it")
/* the x64 back end's row: the sequence a call lowers to */
CUTLASS_EMIT_LOWER_X64(4, RDTSC, 4, "none", "rdtsc; shl rdx, 32; or rax, rdx")
Cutlass
A program cannot reach the floor directly
This program calls the intrinsic inside a correct x64 arm and is still refused. Only cut and cutrun reach cutarch; portable code reads time through cutrun.clock.
when (target.arch) {
is x64 {
u64 cycles() {
raw { return cutarch.x64.rdtsc() }
}
}
is arm64, rv64 {
}
}
# check: stamp.cut:4:26: `cutarch.x64.rdtsc` reaches the node `cutarch`, and no edge leads there; `stamp` reaches `cut` and `cutrun`
cut: regions and the core library
cut is the core library every Cutlass program links without declaring it. It holds the portable mechanism the compiler lowers through, the region allocator and Trident's kinds written in Cutlass. It also holds the common library: text, collections, math and files. A binary keeps only what it reaches.
A region is the one allocator. Every function body is a frame region and every loop pass is a pass region; region (bytes = 64KiB) { ... } adds an explicit region with a bound. Nothing is freed one value at a time: a closed region returns its whole chain of chunks in one splice.
The destination owns the value. An expression assigned to a binding is built in that binding's region, so assigning to an outer binding is the visible escape. A return builds into the caller's region. Exceeding a bound fails the allocation with fault.memory at the same point in every build.
Cutlass
Frame and pass regions
trimmed and words die at the end of each pass. out lives in the frame region because it is the destination, and return hands the result to the caller's region.
text summarize(text[] lines) { # the body is a region: the frame
text out = "" # out lives in the frame region
foreach (line in lines) { # each pass is a region
text trimmed = trim(line) # dies at the end of the pass
text[] words = split(trimmed) # dies at the end of the pass
if (words.count() > 3) {
out = out + words[0] + " " # built in out's region, because out is the destination
}
}
return out # return is the frame escape: the result region is the caller's
}
cutrun: threads, channels, coroutines, fork-join and time
cutrun lowers what a program declares and nothing else: the thread table, channels, coroutine resume, fork-join, the clock, shared segments and capability tables. There is no scheduler, no task runtime and no garbage collector.
A thread is a declaration bound to one function, one core, one stack and one memory. A channel is a bounded typed queue whose ring the compiler chooses from the senders and receivers it can see. A wait statement sleeps on several sources at once and runs the first ready arm, in source order.
A value reaches another thread only through a channel, an atomic, a declared shared static, an owner static or a static frozen at start. Because those are the only paths, a tape can record every input concurrency adds, and a replay reproduces a multi-core run.
Cutlass
One wait over several sources
The arms are tried in source order and exactly one runs. A deadline fails with fault.timeout, and a closed channel with fault.closed, so the loop ends cleanly when the sampler stops.
The compiler builds the state machine; there is no stack switching. A bare yield hands the slot back when the socket would block, and yield f publishes one complete frame.
coroutine Frame session(host.Socket s) with { memory = 16KiB } {
u8[8192] input
uword pending = 0
loop {
uword n = host.receive(s, input.tail(pending)) else (e) {
when (e) {
is HostError.wouldBlock { yield }
else { return }
}
continue
}
if (n == 0) { return }
pending = pending + n
Frame f = frame(input, pending) else (e) { continue }
pending = 0
yield f
}
}
Cutlass
Structured fork-join
Branch k runs on member k of the group and the block ends when every branch returns. A branch that blocks, touches a channel or writes what another branch writes is refused, so parallel and in-order runs give one result.
BenthOS, the device operating system, and MerFS, its filesystem, are Cutlass programs. Kernel entry points, interrupt tables, devices and boot-handoff memory are declarations. The compiler that checks a hosted service also checks a kernel.
A target description names the environment. benthos is a host environment beside linux, windows and darwin, and none is freestanding code such as the BenthOS loader and kernel.
BenthOS
Boots on the hardware, verifies each startup step, and checks a capability before any device action.
MerFS
Stores complete, checked file versions in scopes an authority service grants. A granted scope is the only way in.