Skip to content

Janus Language Reference

Version: 2026-04-22 (v2026.4.8) Profiles covered: :core (complete), :service (complete), :script (shipping), :cluster (draft) Purpose: Single-source reference for humans and AI agents writing Janus code


IDENT <- [_A-Za-z][_A-Za-z0-9]*
// Integers
42 // decimal
0xFF // hexadecimal
0b1010 // binary
0o77 // octal
1_000_000 // underscores for readability
// Floats
3.14
2.5e10
1.0e-3
// Strings
"hello" // UTF-8
"line\nnewline" // escape sequences
$"Value: {x}" // string interpolation
$"Pi is {pi:.2f}" // with format spec
// Boolean
true
false
// Line comment
//! Module-level doc comment (describes the file)
/// Item-level doc comment (describes the next declaration)

All compiler builtins use § prefix. User functions MUST NOT use §.

§size_of(T) // compiler-phase operation
§compile_error(msg) // halts compilation

Law 1: do...end for control flow (imperative, vertical)

if cond do ... end
while cond do ... end
for iter do |x| ... end
func name() do ... end
comptime do ... end
test "name" do ... end

Law 2: { } for data structures (declarative, containers)

struct { field: type }
enum { Variant }
match expr { pattern => value }
extern struct { field: type }

These two laws are non-negotiable. No exceptions.


TypeSizeDescription
i8, i16, i32, i641-8 bytesSigned integers
u8, u16, u32, u641-8 bytesUnsigned integers
usizeplatformPointer-sized unsigned
f32, f644-8 bytesIEEE 754 floats
bool1 bytetrue or false
void0 bytesNo value

:core universal integer model: i64 is the default integer type.

// Arrays (fixed-size, comptime-known length)
var buf: [32]u8 = undefined;
const zeros: [4]i64 = .{ 0, 0, 0, 0 };
// Slices (runtime-length view into array)
name: []const u8 // immutable byte slice (string)
data: []u8 // mutable byte slice
// Pointers
ptr: *T // single-item pointer
ptr: *const T // const pointer
ptr: *[N]u8 // pointer to fixed-size array
ptr: [*]const u8 // many-item pointer (C interop)
// Borrow expressions
parseConsume[i32](* input) // borrow mutable value
format[i32](42, * buf[..]) // borrow mutable slice view
// Optional
value: ?T // T or null
field: ?u64 = null // optional with default

Janus borrows existing values with prefix * at expression sites. It does not use &mut, and it does not take references to literals inline. Bind the value first, then borrow or slice it.

// Compiler-native syntax (what `janus build` accepts):
struct TreeEntry {
name: []const u8,
entry_cid: [32]u8,
mode: u8,
}
// Optional fields with defaults
struct Change {
message: []const u8,
timestamp: i64,
sequence: ?u64 = null, // optional, defaults to null
lamport: ?u64 = null,
}
// NOTE: `pub const Name = struct { ... };` is Zig heritage migration input.
// The canonical Janus form is `struct Name { ... }` (no pub const =, no trailing ;).
// Treat the heritage form as refactor debt, not target syntax for normal .jan code.

3.4 Extern Struct (Wire-Compatible Layout)

Section titled “3.4 Extern Struct (Wire-Compatible Layout)”
// Fixed memory layout, matches C ABI. Used for SBI, FFI, binary protocols.
extern struct RequestFrame {
version: u8,
msg_type: u8,
_pad: [2]u8 = .{ 0, 0 },
payload_len: u32,
seq: u64,
}

extern struct guarantees field order, alignment, and size match C ABI rules. Use §size_of, §align_of, §offset_of for comptime layout verification.

// Compiler-native syntax (what `janus build` accepts):
enum HexError {
InvalidChar,
InvalidLength,
}
// Enum with explicit backing type (planned — currently all enums are i64-backed)
enum ObjectKind {
blob,
tree,
change,
checkpoint,
}
// Non-exhaustive enum (catch-all for unknown values)
enum MsgType {
submit,
ping,
shutdown,
}
// NOTE: `pub const Name = enum { ... };` is Zig heritage migration input.
// The canonical Janus form is `enum Name { ... }` (no pub const =, no trailing ;).
// Treat the heritage form as refactor debt, not target syntax for normal .jan code.
// Compiler-native syntax:
error DiffError {
DiffFailed,
InvalidTree,
StorageError,
}
// NOTE: `pub const Name = enum { ... }` also works for error sets,
// but the dedicated `error Name { ... }` keyword is preferred
// as it communicates intent (these are failure modes, not data variants).
// A function that can fail returns Error!Success
pub fn readRef(dir: Dir, io: Io, name: []const u8) RefError![32]u8
// The error union type: left is error, right is success
HexError!u8 // returns u8 on success, HexError on failure
![]u8 // inferred error set, returns []u8 on success

// Immutable (preferred)
const x = 42;
const name: []const u8 = "graf";
let result = compute(); // let also works for immutable
// Mutable
var counter: usize = 0;
var buf: [64]u8 = undefined; // uninitialized fixed buffer
const CID_LEN: usize = 32;
const PROTOCOL_VERSION: u8 = 1;
const HEX_CHARS = "0123456789abcdef";
pub const GTP_CAP_SBI: u8 = 0x08;
// Compiler-native syntax (what `janus build` accepts):
// Uses `func` keyword, `do...end` blocks, `-> ReturnType` arrow syntax.
func add(a: i64, b: i64) -> i64 do
return a + b
end
// Function with error return
func decodeNibble(c: u8) -> HexError!u8 do
if c >= '0' and c <= '9' do return c - '0' end
return HexError.InvalidChar
end
// Function with allocator parameter
func diffTrees(
store: *Store,
old_cid: ?[32]u8,
new_cid: [32]u8,
allocator: std.mem.Allocator,
) -> ![]Transition do
// ...
end
// Method (self parameter)
func deinit(self: *GrafRuntime) do
self.scheduler.stop()
end
// Comptime parameter
func encodeFix(comptime N: usize, bytes: *const [N]u8) -> [N * 2]u8 do
// N is known at compile time; array sizes are comptime-determined
end
// NOTE: `pub fn name() { ... }` is Zig heritage syntax still accepted by the compiler
// for Zig-interop code. The canonical Janus form is `func name() do ... end`.
extern fn blake3_hash(data_ptr: i64, data_len: i64) -> i64;
extern fn blake3_to_hex() -> i64;
// Test blocks use do...end (imperative logic, Law 2)
test "writeRef + readRef round-trip" do
var tmp = testing.tmpDir(.{})
defer tmp.cleanup()
try writeRef(tmp.dir, testing.io, "main", test_cid)
const read_cid = try readRef(tmp.dir, testing.io, "main")
try testing.expectEqualSlices(u8, &test_cid, &read_cid)
end
test "basic addition" do
assert(1 + 1 == 2)
end

// No parentheses around condition. Uses do...end.
if count > 0 do
process()
else
skip()
end
// Single-line
if c >= '0' and c <= '9' do return c - '0'; end
// Optional unwrap (if-do with capture)
if old_tree_cid do |otc|
old_tree = store.getTree(otc) catch return DiffError.DiffFailed;
end
var i: usize = 0;
while i < N do
out[i] = compute(i);
i = i + 1;
end
// Iterate with capture
for entries do |entry|
process(entry.name, entry.cid);
end
// Range iteration
for 0..N do |i|
buf[i] = 0;
end
// Exclusive range
for 0..<10 do |i|
// i = 0, 1, ..., 9
end
// Exhaustive pattern matching (uses { } not do...end)
match kind {
.blob => decode_blob(data),
.tree => decode_tree(data),
.checkpoint => decode_checkpoint(data),
else => return error.UnknownKind,
}
// With guards
match request {
.Create(data) when data.is_valid => process(data),
.Delete(id) when id > 0 => delete(id),
else => reject("invalid"),
}

Doctrine: else, _, and discard — one symbol, one meaning

SymbolLevelPurposeValid context
elseExpressionMatch fallback (catch-all)match x { else => ... }
_PatternDiscard bindinglet _ = expr, catch |_|, (_, 0, _) in destructure
discardStatementDiscard expression resultdiscard some_function()
  • Use else for match fallbacks (catch-all arm)
  • Use _ when you don’t need a value in a pattern context (variable binding, catch, destructuring)
  • Use discard when calling a function for side effects and ignoring its return value at statement level

.Variant patterns (e.g., .blob, .Create(data)) are type-inferred from the match subject and encouraged — the compiler already knows the enum type, so repeating it is noise.

const value = blk: {
if condition do break :blk 42; end
break :blk 0;
};

// try: propagate error to caller
const data = try store.read(cid);
const hi = try decodeNibble(hex[0]);
// ? suffix: same as try (alternative syntax)
const result = fallible()?
// catch with fallback value
const cid = fromHex(content) catch return RefError.InvalidRef;
// catch with error capture
const sched = Scheduler.init(alloc, 4) catch |err| {
log.error($"init failed: {err}")
return error.RuntimeInitFailed;
}
// catch unreachable (assert success)
var rt = GrafRuntime.init(alloc, 2) catch unreachable;
// Return an error
return HexError.InvalidChar;
return error.DiffFailed;
// fail keyword (alternative)
fail DivisionError.DivisionByZero

The discard keyword explicitly discards the result of an expression. This is used when calling a function for its side effects but ignoring its return value.

// Discard a return value
discard some_function()
// Discard with error handling
discard may_fail() catch |err| do
log_error(err)
end
// Common use case: logging
discard logger.write("message")
// Multiple discards in sequence
discard posix.close(fd)
discard allocator.free(buf)

Note: Unlike Zig’s _ = expr syntax, Janus uses discard expr for explicit clarity. The discard keyword makes the intent obvious: “I am intentionally ignoring this result.”

// defer: runs on scope exit (success or error)
defer allocator.free(matched);
defer rt.deinit();
// errdefer: runs ONLY on error exit
var out: std.ArrayListUnmanaged(u8) = .empty;
errdefer out.deinit(allocator);
try encodeValue(allocator, &out, value);
return out.toOwnedSlice(allocator);
// Conditional defer
defer if owns_old do freeTree(allocator, old_tree); end

Type parameters in square brackets. Always explicit at call site (no type inference).

// Definition
func identity[T](x: T) -> T do
return x
end
// Call site (type always explicit)
let n = identity[i64](42)
let s = identity[[]const u8]("hello")
func min[T: Ord](a: T, b: T) -> T do
if a <= b do return a end
return b
end
// Multiple bounds
func clamp[T: Ord + Numeric](value: T, lo: T, hi: T) -> T do
if value < lo do return lo end
if value > hi do return hi end
return value
end
trait Eq {
func eq(self, other: Self) -> bool
func ne(self, other: Self) -> bool
}
trait Ord: Eq { // Ord implies Eq (supertrait)
func lt(self, other: Self) -> bool
func le(self, other: Self) -> bool
func gt(self, other: Self) -> bool
func ge(self, other: Self) -> bool
}
trait Numeric: Ord + Eq { // Numeric implies Ord and Eq
func add(self, other: Self) -> Self
func sub(self, other: Self) -> Self
func mul(self, other: Self) -> Self
func div(self, other: Self) -> Self
}

All integer and float primitives satisfy all three traits intrinsically. bool satisfies Eq only.

Each unique (function, type_args) pair compiles to a separate specialized function:

min[i32](3, 5) // emits: min_i32
min[f64](3.14, 2.7) // emits: min_f64

Trait bounds are verified once at definition, not per instantiation.


Compile-time evaluation. Code runs in the compiler, vanishes before the binary exists.

// Module scope: evaluated once during compilation
comptime do
assert(PROTOCOL_VERSION >= 1, "requires protocol v1+")
end
// Expression form: single comptime value
const SIZE = §{ calculate_optimal_size(PAGE_SIZE) }
func hex_encode(comptime N: usize, input: [N]u8) -> [N * 2]u8 do
var output: [N * 2]u8 = undefined
inline for 0..N |i| do
output[i * 2] = charset[input[i] >> 4]
output[i * 2 + 1] = charset[input[i] & 0x0F]
end
return output
end
// comptime T: type gives full introspective access
func decode(comptime T: type, bytes: []u8) -> ?T do
const info = §type_info(T) // requires :service
// ...
end

:core+ builtins (available everywhere):

BuiltinInputOutputPurpose
§size_of(T)typeusizeByte size of type
§align_of(T)typeusizeAlignment requirement
§offset_of(T, field)type, stringusizeField byte offset in struct
§is_integral(T)typeboolTrue for integer types
§is_float(T)typeboolTrue for float types
§fmt(args...)variadicstringComptime string construction
§compile_error(msg)stringneverHalt compilation with message

:service+ builtins (type introspection):

BuiltinInputOutputPurpose
§type_info(T)typeTypeInfoFull type metadata
§type_name(T)typestringFully qualified name
§type_id(T)typeu64Stable hash identifier
§fields(T)type[]FieldInfoStruct field list
§has_field(T, name)type, stringboolField existence check
§has_decl(T, name)type, stringboolDeclaration existence
§field(val, name)any, stringfield typeAccess field by name
§field_type(T, name)type, stringtypeType of named field
// inline for: unrolls at compile time
inline for §fields(T) |field| do
const field_value = §field(value, field.name)
process_field(field_value)
end
// inline switch: eliminates dead branches
inline switch §type_info(T) do
.Struct => |s| encode_struct(value, s)
.Int => |i| encode_int(value, i)
else => comptime do §compile_error("unsupported") end
end
// if comptime: conditional compilation
if comptime §is_integral(T) do
optimized_int_path()
else
generic_path()
end
extern struct ResponseFrame do
seq: u64
status: u8
has_tip: u8
_pad: [2]u8
detail_len: u32
final_tip: [32]u8
end
comptime do
assertsize_of(ResponseFrame) == 48, "frame must be 48 bytes")
assertoffset_of(ResponseFrame, "seq") == 0, "seq at offset 0")
assertoffset_of(ResponseFrame, "status") == 8, "status at offset 8")
assertoffset_of(ResponseFrame, "detail_len") == 12, "detail_len at offset 12")
assertoffset_of(ResponseFrame, "final_tip") == 16, "final_tip at offset 16")
end

All conversions are explicit. Janus never silently coerces between types.

func toInt[T](value: i64) !T

Returns error if value doesn’t fit in T.

let narrow = toInt[u8](200)? // Ok: 200 fits in u8
let fail = toInt[u8](256) // Err: Overflow
let neg = toInt[u8](-1) // Err: Overflow
func truncate[T](value: i64) -> T

Discards high bits. Always succeeds.

let byte = truncate[u8](0x1FF) // 0xFF (low 8 bits)
let zero = truncate[u8](256) // 0x00
func as[T](value: i64) -> T

Zero-cost identity or sign/zero-extend.

let wide = as[i64](narrow_i32) // safe widening
func bitcast[T](value: auto) -> T

Same bits, different type. Source and target must have identical size and alignment.

let f = bitcast[f32](0x42280000) // 42.0
// Integer to enum (checked)
func toEnum[T](value: auto.Underlying) -> !T
let color = toEnum[Color](1)? // Color.Green
let bad = toEnum[Color](99) // Err: Overflow
// Enum to integer (zero-cost)
func fromEnum(value: auto) -> auto.Underlying
let n = fromEnum(Color.Green) // 1 (type is u8 if Color: u8)

fromEnum returns the enum’s declared backing type, not i64. No generic parameter needed.


10. Memory Operations (std.mem.raw and bridge helpers)

Section titled “10. Memory Operations (std.mem.raw and bridge helpers)”
// Raw byte operations
func copyBytes(dst: *u8, src: *const u8, count: usize)
func moveBytes(dst: *u8, src: *const u8, count: usize)
func fillBytes(dst: *u8, value: u8, count: usize)
func zeroBytes(dst: *u8, count: usize)
func compareBytes(a: *const u8, b: *const u8, count: usize) -> i32
// Raw pointer/address verbs
func reinterpret[T](p: *auto) -> :sovereign *T
func realign[T](p: *auto) -> !*T
func pointerFrom[T](addr: usize) -> :sovereign *T
func addressOf[T](p: *auto) -> usize
// FFI boundary adapters
func constBytesPtr(buf: []const u8) -> *const u8
func bytesPtr(buf: []u8) -> *u8
func cStringPtr(z: []const u8) -> *const u8
func opaqueFrom[T](p: *T) -> *anyopaque
func opaqueInto[T](p: *anyopaque) -> ?*T
func constViewAt[T](buf: []const u8, offset: usize) -> ?*const T

Raw ptrCast / alignCast syntax is not canonical Janus source. Normal .jan files should use named memory verbs or a bridge helper that owns the foreign boundary explicitly.


use is the Janus-native module import mechanism. It is the ONLY way to import Janus modules. @import("path") is Zig’s private plumbing and MUST NOT appear in Janus source code outside of use zig graft declarations.

The use statement creates a namespace binding named after the last path component. The compiler resolves the path by joining identifiers with / and appending .jan, relative to the source file’s directory.

// Import a module — binds the name "types" to the module's exports
use identity.types
// Access symbols through the namespace
let did = types.parse_did("did:key:z6Mk...")
let err = types.ResolveError.NotFound
// Import from same directory
use resolver
use envelope
// Import from subdirectory
use method.key
use vc.normalize
// Import from Janus stdlib
use std.encoding.cbor
use std.crypto.mldsa65

Resolution algorithm (pipeline Stage 2.5):

  1. Join path components with /, append .jan → e.g., identity/types.jan
  2. Resolve relative to source file’s directory
  3. Fallback: resolve relative to CWD
  4. Future (SPEC-041): resolve by CID via content-addressed registry

Use use zig only for explicit bridge modules or compiler-generated wrappers. Do not treat it as a general-purpose import mechanism for normal Janus application code. This is the ONLY context where file paths appear in import statements.

// Import narrow Zig bridge files
use zig "bridge/blake3_bridge.zig"
use zig "bridge/net_bridge.zig"

For C libraries (per Grafting Doctrine GD-1: explicit, contained, replaceable):

graft c "oqs/oqs.h" as oqs link "oqs"
graft c "argon2.h" as argon2 link "argon2"
// Use via the alias
let rc = oqs.OQS_SIG_ml_dsa_65_verify(...)

Sovereign Index files re-export sub-module symbols for consumers:

// identity.jan — Sovereign Index
pub use identity.types
pub use identity.resolver
pub use identity.registry
// With alias (for name conflicts)
pub use gateway.jsonrpc_handler as jsonrpc
pub use gateway.sbi_handler as sbi
// ❌ WRONG — @import is Zig plumbing, not Janus syntax
const types = @import("../identity/types.jan");
// ✅ CORRECT — use is Janus-native module loading
use identity.types
// ❌ WRONG — const assignment from use
const types = use identity.types;
// ✅ CORRECT — use binds the namespace directly
use identity.types
// Access as: types.DID, types.parse_did(), etc.
// ❌ WRONG — Zig borrow syntax
func format[T](value: T, buf: &mut [u8]) -> !usize
// ✅ CORRECT — Janus borrows with prefix *
func format[T](value: T, buf: *[]u8) -> !usize
let written = format[i32](42, * buf[..])
// ❌ WRONG — inline borrowed literal
const empty_bytes = &[0]u8{}
// ✅ CORRECT — bind the value, then slice it
const empty_bytes: [0]u8 = .{}
const empty_slice = empty_bytes[0..]

Zero-encoding binary serialization. Wire bytes = memory bytes on same architecture.

use std.sbi as sbi;
// Encode
var buf: [sbi.encodedSize(Sensor)]u8 = undefined;
const frame = try sbi.encode(Sensor, * reading, * buf);
// Decode (zero-copy, same arch)
const decoded = try sbi.decode(Sensor, * buf);
// Decode (copy, any alignment)
const value = try sbi.decodeCopy(Sensor, * buf);
// Decode (cross-architecture, in-place endian fixup)
const fixed = try sbi.decodeMut(Sensor, * mutable_buf);
// Schema fingerprint (comptime)
const fp = §{ sbi.schemaFingerprint(Sensor) }
// Encoded frame size (comptime)
const size = sbi.encodedSize(Sensor); // 24 (preamble) + §size_of(T)
// Layout analysis (comptime)
const layout = §{ sbi.fieldLayout(Sensor) }
const padding = §{ sbi.totalPadding(Sensor) }
[0..4) Magic: "SBI\x00"
[4] Version: 0x01
[5] ArchFlags (endianness, pointer width)
[6..8) CapFlags (pure, deterministic)
[8..24) Schema fingerprint (16-byte truncated BLAKE3)
[24..) Raw extern struct bytes

13. Structured Concurrency (:service Profile)

Section titled “13. Structured Concurrency (:service Profile)”
pub const Scheduler = std.Scheduler;
pub const Nursery = std.Nursery;
pub const Budget = std.Budget;
pub const SpawnOpts = std.SpawnOpts;
// Initialize runtime
var rt = GrafRuntime.init(allocator, worker_count) catch unreachable;
defer rt.deinit();
// Create nursery (structured scope)
var nursery = rt.createNursery(Budget.serviceDefault());
defer nursery.deinit();
// Spawn tasks (all must complete before nursery exits)
discard nursery.spawn(task_fn, task_arg)
discard nursery.spawn(other_fn, other_arg)
ProfileDefault Stack
:core64 KB
:service256 KB
:sovereign512 KB

Override via SpawnOpts.stack_size.

Nurseries support cooperative cancellation via CancelToken. Cancellation propagates transitively through nested nurseries.


Nearly every allocation-using function takes an explicit allocator:

pub fn encode(allocator: std.mem.Allocator, value: anytype) ![]u8 {
var out: std.ArrayListUnmanaged(u8) = .empty;
errdefer out.deinit(allocator);
try encodeValue(allocator, &out, value);
return out.toOwnedSlice(allocator);
}

For small collections, ArrayList with linear scan replaces HashMap:

var list = std.ArrayListUnmanaged(Entry).empty;
defer list.deinit(allocator);
try list.append(allocator, entry);
// Search
for list.items do |item|
if std.mem.eql(u8, item.name, target) do return item; end
end
test "round-trip" {
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
// ... use tmp.dir for file operations
}
// Compiler-native syntax — colon separators, no dot prefix, no trailing semicolons:
let rt = GrafRuntime { scheduler: sched, allocator: allocator }
// All fields listed — no defaults needed
let frame = RequestFrame {
version: PROTOCOL_VERSION,
msg_type: MsgType.submit,
payload_len: 1024,
seq: 42,
}
// Explicit leniency — accept defaults for unlisted fields
let cfg = Config { host: "prod.example.com", ..defaults }
// NOTE: Zig-style `.field = value` with dot prefix is NOT Janus syntax.
// Janus uses `field: value` (colon, not equals, no dot).

Struct Initialization Completeness (Law 10):

Struct initializers MUST list all fields unless ..defaults is present. This eliminates silent bugs when fields are added to structs — every callsite is forced to either provide the new field or explicitly opt into defaults.

struct Server { host: str = "localhost", port: u16 = 8080, tls: bool = false }
let s1 = Server { host: "prod", port: 443, tls: true } // all fields — OK
let s2 = Server { host: "prod", ..defaults } // explicit leniency — OK
let s3 = Server { host: "prod", port: 443 } // ERROR: missing 'tls'
// Unwrap with if-capture
if optional_value do |val|
use(val);
end
// Null coalescing
const result = maybe_value ?? default_value;
// Optional chaining
const name = user?.profile?.name;
// catch null (error to optional)
const val = fallible() catch null;

+, -, *, /, %

==, !=, <, <=, >, >=

and, or, not (NOT &&, ||, !)

& (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), >> (right shift)

OperatorPurpose
?Error propagation (suffix)
??Null coalescing
?.Optional chaining
|>Pipeline
..Inclusive range
..<Exclusive range

ContextConventionExample
Modules, Structs, EnumsPascalCaseTreeEntry, ObjectKind
Variables, Functionssnake_caseread_ref, payload_len
ConstantsUPPER_SNAKE or snake_caseCID_LEN, PROTOCOL_VERSION
Type parametersSingle uppercaseT, N
Comptime builtins§snake_case§size_of, §type_info
Private fields/padding_prefix_pad, _pad2

Functions, variables, control flow, structs, enums, error handling, pattern matching, generics with trait bounds, comptime blocks/params, §size_of/§align_of/§offset_of/§is_integral/§is_float/§fmt/§compile_error, use zig, extern functions, SBI (extern struct encode/decode), tests.

All of :core plus: §type_info, §type_name, §type_id, §fields, §has_field, §has_decl, §field, §field_type, nurseries, fibers, channels, spawn, M:N scheduler, structured concurrency, cancellation tokens, budgets.

All of :service plus:

Actors and Grains:

  • actor Name(msg: T) do ... end — node-pinned actor with typed mailbox
  • grain Name(msg: T) do ... end — migratable actor with memory sovereignty
  • message Name { ... } — sealed algebraic message protocol
  • spawn Name(args) with { ... } — spawn with options
  • spawn Name(args) on { ... } — cluster-aware placement
  • send ref, msg — send message to actor/grain
  • receive do ... end — blocking message receive with pattern matching
  • receive ... after N => ... — receive with timeout

Supervision:

  • supervisor Name, strategy: .one_for_one do ... end — restart strategy
  • child Type, args: [...], restart: .permanent — supervised child

Memory Sovereignty:

  • alloc[Local.Exclusive](value) — owned memory, serialized on migrate
  • alloc[Session.Replicated](value) — async-replicated to cluster peers
  • alloc[Session.Consistent](value) — sync-replicated via consensus
  • alloc[Volatile.Ephemeral](value) — dropped on migrate, reconstructed

Capability Gating:

  • requires CapX, CapY — function requires capabilities from caller (between return type and do)
  • @requires(cap: [...]) — grain declares required hardware/software capabilities
  • @mailbox(capacity: N, overflow: .drop_oldest) — mailbox policy
  • @replicate(scope: .wing) — replication scope annotation

Cluster (runtime – see NexusOS SPEC-030–034 for implementation):

  • Cluster.join(config) — join cluster mesh (NexusOS runtime)
  • cluster.advertise(manifest) — broadcast node capabilities (NexusOS runtime)
  • cluster.migrate(ref, target, strategy) — migrate grain (NexusOS runtime)
  • cluster.locate[T](name) — location-transparent grain lookup (NexusOS runtime)

:compute (GPU/NPU, tensors), :sovereign (raw pointers, unsafe, comptime allocation), :script (REPL, JIT).


18. :cluster Profile — Actors, Grains, and Distribution (Draft)

Section titled “18. :cluster Profile — Actors, Grains, and Distribution (Draft)”

The :cluster profile provides distributed systems primitives: typed actors, migratable grains, supervision trees, and capability-gated placement. Every construct desugars to :service primitives. No hidden runtime.

Every actor and grain declares its messages as a sealed algebraic type:

message StorageMsg {
Read { sector: u64, len: usize, reply: Reply[ReadResult] },
Write { sector: u64, data: []const u8, reply: Reply[WriteResult] },
Flush { reply: Reply[FlushResult] },
Eject,
}

Desugars to enum + compile-time Serialize check.

Synchronous request-response without blocking the actor:

let reply = Reply[ReadResult].new()
storage_ref.send(StorageMsg.Read { sector: 0, len: 512, reply: reply })
let result = reply.await(timeout: 5000)

Desugars to oneshot channel + send + recv.

Node-pinned, never migrates. Use for boot-critical singletons or non-serializable state.

message BootMsg {
MountRoot { device: []const u8 },
StartInit { runlevel: u8 },
}
actor BootLoader(msg: BootMsg) do
var state: BootState = BootState.pre_init
receive do
BootMsg.MountRoot { device } => do
state = try mount_rootfs(device)
end
BootMsg.StartInit { runlevel } => do
state = try start_init_sequence(runlevel)
end
end
end

Location-transparent, migratable actors. State must be serializable.

@requires(cap: [.network])
grain StorageService(msg: StorageMsg) do
var index = alloc[Session.Replicated](StorageIndex.new())
var write_cache = alloc[Volatile.Ephemeral](WriteCache.new(capacity: 4096))
receive do
StorageMsg.Read { sector, len, reply } => do
let data = try read_sectors(index, sector, len)
reply.send(ReadResult.Ok { data })
end
StorageMsg.Flush { reply } => do
let result = try write_cache.commit(index)
reply.send(result)
end
end
reconstruct() do
write_cache = WriteCache.new(capacity: 4096)
end
end

18.6 Capability-Gated Functions (requires)

Section titled “18.6 Capability-Gated Functions (requires)”

Functions that demand capabilities use requires clause — same position as where:

func read_config(ctx: *Context) !Config
requires CapFsRead
do
return ctx.fs.read("config.toml")
end
func sync_backup(ctx: *Context) !void
requires CapFsRead, CapFsWrite, CapNetHttp
do
// ...
end

Conjunction semantics: requires CapFsRead, CapFsWrite means caller must grant both.

supervisor DeviceTree, strategy: .one_for_one,
max_restarts: 5, max_seconds: 10 do
child NetworkDriver, args: [net_config], restart: .permanent
child StorageService, args: [disk_config], restart: .permanent
child DisplayDriver, args: [gpu_config], restart: .transient
child TempLogger, args: [], restart: .temporary
end

Restart strategies: .one_for_one, .one_for_all, .rest_for_one Restart policies: .permanent (always), .transient (abnormal only), .temporary (never)

Lightweight interned strings for tags and status codes:

:ok, :error, :timeout, :normal, :shutdown

Desugars to Symbol.intern("string") — GC-managed, safe unlike BEAM atoms.

TagMigrationReplication
Local.ExclusiveSerialized + movedNever
Session.ReplicatedAsync to peersContinuous
Session.ConsistentSync via consensusRaft/PBFT
Volatile.EphemeralDroppedNever
:cluster SurfaceDesugars to
message Foo { ... }enum Foo { ... } + Serialize check
actor Name(msg: T) do ... endMessage loop + typed mailbox
grain Name(msg: T) do ... endActor + migratable + Serialize
func f() !T requires CapX do ... endCapability gate on call graph
supervisor Name ... endsupervisors.start_link(ctx, Spec)
receive do ... endactors.receive().match(...)
Reply[T].new()channel.oneshot[T]()
alloc[Tag](v)actors.alloc(v, ReplicationPolicy { tag: ... })
:symbolSymbol.intern("symbol")

“The fastest serialization is no serialization. The clearest code needs no comments. The best compiler is the one that catches your mistakes before they exist.”