Janus Language Reference
Janus Language Reference
Section titled “Janus Language Reference”Version: 2026-06-07b — Vendor resolution clarified. graft vendor resolves against system-installed libraries (pkg-config, /usr/lib/) with sha256 verification against curated capsule manifests. No network access during resolution; --vendor-fetch=allow opt-in for prebuilt fallback. Diagnostic codes E2720 (hash mismatch), W2721 (library not found). 2026-06-07a retained: Odin Theft (vendor batteries, overload sets, vec types).
Profiles covered:
:core— complete (locked 2026-03-07):service— complete (channels, select, nursery, spawn, row polymorphism,std.syncparker/mutex):script— almost complete - SPEC-045 ratified; stdlib build-out in progress (SPEC-048 → 046 → 047 → 049) Lowering progress ~80%:cluster— DRAFT (RFC-021); P0 prerequisites: numeric tower inllvm_emitter.zig,std.sync. Lowering progress ~20%:compute— DRAFT (SPEC-050 polar/relative types shipped; tensor + GPU/NPU kernels WIP):sovereign— DRAFT (SPEC-080 Phase A.0 held by Virgil verdict; pull-on-demand on dated OpenBSD target)
Purpose: Single-source reference for humans and AI agents writing Janus code
0. The Three Sigil Worlds (SPEC-050)
Section titled “0. The Three Sigil Worlds (SPEC-050)”This is the foundational law of Janus syntax. Every sigil encodes a phase. No sigil crosses phase boundaries.
| Sigil | World | Purpose | Examples |
|---|---|---|---|
§ | Compile-time | Structural computation, type reflection, code generation | §size_of(T), §type_info(T), §{ ... } |
$ | Extraction | Runtime pattern capture (regex, PEG, structured parsing) | $1, $2, $* |
@ | Metadata | FFI bindings, attributes, capability declarations | @ffi(...), @requires(...), @replicate(...) |
The Desugaring Law: § lowers to compile-time AST nodes. $ lowers to runtime pattern values. @ attaches declarative metadata. No cross-phase aliasing exists.
Migration: $size_of → §size_of (deprecated, W5001 warning). The $ sigil is now exclusively reserved for runtime extraction.
1. Lexical Structure
Section titled “1. Lexical Structure”1.1 Identifiers
Section titled “1.1 Identifiers”IDENT <- [_A-Za-z][_A-Za-z0-9]*1.2 Literals
Section titled “1.2 Literals”// Integers42 // decimal0xFF // hexadecimal0b1010 // binary0o77 // octal1_000_000 // underscores for readability
// Floats3.142.5e101.0e-3
// Strings"hello" // UTF-8"line\nnewline" // escape sequences§"Value: {x}" // string interpolation§"Pi is {pi:.2f}" // with format spec
// Booleantruefalse1.3 Comments
Section titled “1.3 Comments”// Line comment//! Module-level doc comment (describes the file)/// Item-level doc comment (describes the next declaration)1.4 Documentation Comments (Sovereign Docs)
Section titled “1.4 Documentation Comments (Sovereign Docs)”Documentation comments use key: syntax (no @ sigil):
/// Opens a file for reading.////// param: path Filesystem path to open/// returns: File handle or error/// error: FsError.NotFound Path does not existfunc open_file(path: str) !File do // ...endReserved keys: param:, returns:, error:, capability:, since:, see:, deprecated:, safety:, complexity:
2. The Two Structural Laws — Haiku Block Discipline
Section titled “2. The Two Structural Laws — Haiku Block Discipline”Canonical since: 2026-06-06. This section is Janus Doctrine — non-negotiable.
Janus has two block worlds. A reader must know what a construct is before understanding its contents. A parser must not guess whether { ... } means executable code or data.
Law 1: do...end — Executable Code
Section titled “Law 1: do...end — Executable Code”All control-flow and executable bodies use do...end. No exceptions.
func add(a: i64, b: i64) -> i64 do return a + bend
if count > 0 do process()else skip()end
while i < len do i = i + 1end
for entries do |entry| process(entry)end
comptime do assert(§size_of(Header) == 32, "bad layout")end
test "round-trip" do try round_trip(test_data)endThe following is ILLEGAL:
{ return 42}Anonymous braced executable blocks do not exist in Janus.
Law 2: { } — Declarative Data
Section titled “Law 2: { } — Declarative Data”Braces are exclusively for data-shaped constructs — structs, enums, match arms, struct literals. Never executable code.
struct Coord { col: i32, row: i32,}
enum ObjectKind { blob, tree, change,}
let c = Coord { col: 4, row: 2,}
match kind { .blob => decode_blob(data), .tree => decode_tree(data), else => fail DecodeError.UnknownKind,}Law 2a: Struct Literals Require a Type Head
Section titled “Law 2a: Struct Literals Require a Type Head”A struct literal MUST begin with a type path. There are no anonymous struct literals.
Valid:
Coord { col: x, row: y }std.crypto.Hash { algorithm: .blake3 }Invalid:
{ col: x, row: y }Law 2b: Named Expression Blocks (The Escape Hatch)
Section titled “Law 2b: Named Expression Blocks (The Escape Hatch)”The ONLY braced form that contains executable statements is a named expression block. The label tells reader and parser: this is an expression block, not a struct literal.
let value = choose: { if ready do break :choose 42 end break :choose 0}Named expression blocks are rare and explicit. They are the sole exception to Law 2.
Statement Boundaries
Section titled “Statement Boundaries”A statement ends at the first of: newline, ;, end, else, }, EOF.
An expression may continue across lines only when the line is visibly incomplete:
- After an operator
- Inside parentheses, brackets, or braces
- After a comma,
|, or=>
return x + y // one statement
return x + // expression continues — line ends with operator y // completes the return
return // complete statement — bare returnfoo() // separate statement — never return foo()Doctrine Summary
Section titled “Doctrine Summary”verbs stand upright (do...end)data is held in braces ({ })escape hatches have names (label: { })No anonymous fog. No inherited brace swamp. One visual shape, one meaning.
Parser test:
{ return 1 }→ REJECT (anonymous executable block)blk: { return 1 }→ ACCEPT (named expression block)Foo { x: 1 }→ ACCEPT (struct literal with type head){ x: 1 }→ REJECT (anonymous struct literal)
2.1 Janus 1.0 Semantic Contract — The Six Laws
Section titled “2.1 Janus 1.0 Semantic Contract — The Six Laws”Ratified: 2026-06-07. Authority: Markus Maiwald. Doctrine tier: Constitutional. Governed by: SPEC-101 (Janus 1.0 Semantic Contract Amendment).
Janus is strict by default, pure by contract, effectful by row, authorized by capability, bounded by refinement/totality, and zero-cost unless the source explicitly asks for runtime machinery.
These six laws are non-negotiable. They are the semantic floor for all Janus 1.0 profiles. No profile may weaken them; profiles may only strengthen proof obligations within them.
Law 1 — Public Effect Row Law
Section titled “Law 1 — Public Effect Row Law”All exported public functions MUST write their effect row explicitly.
Public pure APIs write
!{}. Public effectful APIs write!{IO},!{Net},!{Ui}, etc. Private functions may still infer.
A public function contract is incomplete unless its effect row is explicit. No public API may hide world contact behind inference.
// ✅ Correct — public API declares puritypub func canonical_hash(view bytes: [u8]) -> [32]u8 !{} do return blake3(bytes)end
// ✅ Correct — public API declares effectspub func load_config(view fs: FilesystemCap, view path: [u8]) -> Config !FsError !{IO} do let bytes = std.fs.read(fs, path)? return parse_config(bytes)?end
// ❌ Illegal for public API — effect row inferredpub func load_config(view fs: FilesystemCap, view path: [u8]) -> Config !FsError do // E2610: public API with inferred effects let bytes = std.fs.read(fs, path)? return parse_config(bytes)?endThis gives Janus a better API audit surface than Haskell. Haskell tells you “this is IO.”
Janus tells you “this may read files, touch the network, allocate, render UI, or use randomness”
and requires actual authority tokens for those effects.
Law 2 — Strict Evaluation Law
Section titled “Law 2 — Strict Evaluation Law”Janus evaluates function arguments strictly. Laziness exists only through named types such as
Lazy[T]andStream[T]. A lazy value MUST carry the effects required to force it.
// Strict: argument evaluated before calllet x = compute()use(x)
// Explicit laziness — Lazy[T !{Effects}] carries forcing effectslet delayed: Lazy[Config !{IO}] = Lazy.defer do read_config(fs, "app.toml")end
// ❌ Illegal — hidden IO behind lazy typefunc pure_looking() -> Lazy[Config] !{} do return Lazy.defer do read_config(fs, "app.toml") // E2620: hidden effect in lazy value endend
// ✅ Correct — lazy type carries its forcing effectsfunc delayed_config(view fs: FilesystemCap) -> Lazy[Config !{IO}] !{} do return Lazy.defer do read_config(fs, "app.toml") endendThis takes Haskell’s purity without inheriting lazy cost opacity. No thunk fog. No space-leak surprises hidden behind “pure” syntax.
Law 3 — Zero-Cost Lowering Law
Section titled “Law 3 — Zero-Cost Lowering Law”Every abstraction has one of three lowering classes:
- proof-only — erased after checking
- specialized — monomorphized / inlined / statically dispatched
- runtime — explicit allocation, pointer, vtable, callback, actor, channel, or handler value
No fourth category. No invisible heap. No invisible ARC. No invisible dynamic dispatch. No “the compiler might allocate because convenient.”
The compiler MUST expose cost through janus explain-cost:
$ janus explain-cost module.symbolcanonical_hash effects: erased refinements: erased dispatch: static allocation: none dynamic calls: none result passing: register$ janus explain-cost button_on_pressbutton_on_press dispatch: callback indirect call environment: borrowed allocation: none context: explicit effects: UiThis takes C++‘s zero-overhead principle and makes it mechanically auditable. No more inferring cost from folklore, optimizer behavior, and ABI details.
Law 4 — Application Authority Classes
Section titled “Law 4 — Application Authority Classes”Application development uses the same
:serviceprofile, extended with application-specific capabilities and effects. No new:appprofile exists.
Standard application capability/effect classes:
@capability(class: .application, scope: .lifecycle)struct AppCap { _token: u64,}
@capability(class: .ui, scope: .main_surface)struct UiCap { _token: u64,}
effect Ui { func invalidate(view ui: UiCap) -> void func render(view ui: UiCap, take tree: ViewTree) -> void}
effect AppLifecycle { func quit(view app: AppCap, code: i32) -> void}Entry point:
pub func main( ctx: Context, view app: AppCap, view ui: UiCap, view fs: FilesystemCap,) -> i32 !{AppLifecycle, Ui, IO} do let state = CounterState { count: 0 } return std.app.run(ctx, app, ui, CounterView { state: state })endApplications are :service programs with an application runtime and UI/resource capabilities.
The same language, the same semantics, the same promotion path from :script prototype
to :service production.
Law 5 — Callback Authority Rule
Section titled “Law 5 — Callback Authority Rule”A callback environment may contain state. A callback environment MUST NOT contain authority. Effectful callbacks receive
Contextand capabilities explicitly.
// ✅ Correct — environment holds state onlystruct CounterEnv { count: *Signal[i32],}
func increment(env: *CounterEnv, ctx: Context, view ui: UiCap) -> void !{Ui} do env.count.set(env.count.get() + 1) std.ui.invalidate(ui)end
// ❌ Illegal — authority smuggled in callback closurefunc bad_callback() -> Callback do let fs = get_fs_cap() return Callback.new(func() -> void !{IO} do std.fs.read(fs, "/etc/passwd") // E2630: smuggled authority end)endGUI programming is callback-heavy. Janus must not let callbacks smuggle authority
through captured closures. Context carries cancellation, deadline, allocator,
and dependency values explicitly — it is the “thread of trust,” not thread-local magic.
Law 6 — No Default ARC/ORC
Section titled “Law 6 — No Default ARC/ORC”The Janus language has no implicit reference-counting memory model. ARC/ORC-like behavior is allowed ONLY as explicit stdlib/runtime types.
// ✅ Systems-grade default — ownership + Destroy + explicit allocationfunc parse_packet(view bytes: [u8]) -> Packet !{} do ... end
// ✅ Application world — explicit Rc/Weaklet model = Rc[CounterModel].new(app_alloc, CounterModel { count: 0 })let weak_model = model.weak()
// ❌ No invisible retain/release in ordinary Janus code.Core language: ownership + Destroy + explicit allocation.
Application toolkit: region/arena + Rc/Weak/Signal as explicit library types.
UI runtime: may use ORC-like cycle cleanup internally, behind UiCap.
No global ARC. No hidden retain/release in ordinary Janus code. This prevents every program from paying app-runtime costs when writing a codec, kernel service, crypto primitive, or data pipeline.
Profiles and the Six Laws. These laws apply uniformly across all profiles.
:script may relax ambient defaults (implicit capabilities for ergonomics)
but never the underlying semantic contracts. A :script function’s public API
still requires explicit effect rows. The profile determines what’s available,
not what’s provable.
3. Types
Section titled “3. Types”3.1 Primitive Types
Section titled “3.1 Primitive Types”| Type | Size | Description |
|---|---|---|
i8, i16, i32, i64 | 1-8 bytes | Signed integers |
u8, u16, u32, u64 | 1-8 bytes | Unsigned integers |
usize | platform | Pointer-sized unsigned |
f32, f64 | 4-8 bytes | IEEE 754 floats |
bool | 1 byte | true or false |
void | 0 bytes | No value |
:core universal integer model: i64 is the default integer type.
3.2 Compound Types
Section titled “3.2 Compound Types”// 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
// Pointersptr: *T // single-item pointerptr: *const T // const pointerptr: *[N]u8 // pointer to fixed-size arrayptr: [*]const u8 // many-item pointer (C interop)
// Optionalvalue: ?T // T or nullfield: ?u64 = null // optional with default3.3 Struct
Section titled “3.3 Struct”struct TreeEntry { name: []const u8, entry_cid: [32]u8, mode: u8,}
// Optional fields with defaultsstruct Change { message: []const u8, timestamp: i64, sequence: ?u64 = null, // optional, defaults to null lamport: ?u64 = null,}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.
3.5 Enum
Section titled “3.5 Enum”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,}3.6 Error Type
Section titled “3.6 Error Type”error DiffError { DiffFailed, InvalidTree, StorageError,}The dedicated error keyword communicates intent: these are failure modes, not data variants.
3.7 Error Union
Section titled “3.7 Error Union”// A function that can fail returns Error!Successfunc readRef(dir: Dir, io: Io, name: []const u8) RefError![32]u8
// The error union type: left is error, right is successHexError!u8 // returns u8 on success, HexError on failure![]u8 // inferred error set, returns []u8 on success3.8 Small Vector Types
Section titled “3.8 Small Vector Types”Inspired by Odin’s vector arithmetic. Janus provides first-class small vector types for data-oriented compute, graphics, and game development. Vectors are value types — no hidden allocation, no heap, no GC. They live on the stack or in structs.
Type constructor:
// vec[T, N] — vector of N elements of type Ttype Vec2i = vec[i32, 2]type Vec3f = vec[f32, 3]type Vec4f = vec[f64, 4]type Vec4u = vec[u8, 4]Literal construction:
let v: Vec2i = .{ 10, 20 }let w: Vec3f = .{ 1.0, 2.0, 3.0 }Scalar arithmetic. Vectors support element-wise operations with scalars:
let pos: Vec2i = .{ 640, 480 }let half = pos / 2 // Vec2i { 320, 240 }let doubled = pos * 2 // Vec2i { 1280, 960 }
let color: Vec3f = .{ 0.5, 0.5, 0.5 }let bright = color * 2.0 // Vec3f { 1.0, 1.0, 1.0 }let dim = color / 4.0 // Vec3f { 0.125, 0.125, 0.125 }Element access. Indexed access via [N]:
let x = v[0]let y = v[1]Conversion to struct:
struct Coord { col: i32, row: i32,}
func coord_from_vec2(v: Vec2i) -> Coord do return Coord { col: v[0], row: v[1], }endNamed lanes (future, pending :compute SIMD). The long-term target is
first-class named lane access with no hidden cost:
// Future — named lane access (requires SIMD lane tracking)func cell_coord_from_position(pos: Vec2i) -> Coord do let cell = pos / GRID_PX_SIZE return Coord { col: cell.x, // v[0] as named lane row: cell.y, // v[1] as named lane }endNamed lanes are a syntax target, not the current surface. Until the compiler
can prove zero-cost lane naming (identical codegen to v[0]/v[1]), explicit
index access is the required form. No illusion. No language-design perfume sprayed
over a missing feature.
Profile availability:
| Profile | Vector types | Scalar ops | Named lanes | SIMD lowering |
|---|---|---|---|---|
:core | vec[T, N] declaration | Scalar multiplication/division | Future | No |
:service | Full vec types | Full scalar ops | Future | No |
:compute | Full vec types | Full scalar + vec-vec ops | Yes (future) | simd[T; N] via SPEC-237 |
Guarantees:
- No hidden allocation.
Vec2iis 8 bytes on the stack — same as[2]i32. - Value semantics. Assignment copies; no sharing, no ref-counting.
- Strict element types.
vec[f32, 3]≠vec[i32, 3]— no implicit cross-type arithmetic. - Static dispatch. All operations resolve at compile time. No runtime vtable.
- Odin’s compression, Janus’s honesty. Scalar division
pos / GRID_PX_SIZEcompresses intent beautifully — but only because the type system knows exactly what kind of creature the result is.
The Odin line is beautiful because it compresses intent. Janus steals the compression —
after the type system knows exactly what pos / GRID_PX_SIZE is.
4. Declarations
Section titled “4. Declarations”4.1 Variables
Section titled “4.1 Variables”// Immutable (preferred)const x = 42;const name: []const u8 = "graf";let result = compute(); // let also works for immutable
// Mutablevar counter: usize = 0;var buf: [64]u8 = undefined; // uninitialized fixed buffer4.2 Constants
Section titled “4.2 Constants”const CID_LEN: usize = 32;const PROTOCOL_VERSION: u8 = 1;const HEX_CHARS = "0123456789abcdef";pub const GTP_CAP_SBI: u8 = 0x08;4.3 Functions
Section titled “4.3 Functions”// Functions use `func` keyword, `do...end` blocks, `-> ReturnType` arrow syntax.func add(a: i64, b: i64) -> i64 do return a + bend
// Function with error returnfunc decodeNibble(c: u8) -> HexError!u8 do if c >= '0' and c <= '9' do return c - '0' end return HexError.InvalidCharend
// Function with allocator parameterfunc 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 parameterfunc encodeFix(comptime N: usize, bytes: *const [N]u8) -> [N * 2]u8 do // N is known at compile time; array sizes are comptime-determinedend
// Always use `func` (not `fn`).4.4 Extern Functions (FFI via @)
Section titled “4.4 Extern Functions (FFI via @)”FFI bindings use the @ metadata sigil (SPEC-050). This is declaration-time linkage intent, not arbitrary inline foreign code execution.
// Zig bridge@ffi(path: "crypto.zig", lang: .zig)extern func blake3_hash(data_ptr: i64, data_len: i64) -> i64
// C bridge@ffi(path: "sqlite3.c", lang: .c)extern func sqlite3_open(path: *u8, db: **Sqlite3) -> i32Incorrect (do NOT use):
let x = @ffi("foo.c") // WRONG — @ does not evaluate values@ introduces declarative attributes. It does NOT evaluate values and does NOT participate in normal expression syntax.
4.5 Test Blocks
Section titled “4.5 Test Blocks”// 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)end4.6 Parameter Intents (SPEC-085)
Section titled “4.6 Parameter Intents (SPEC-085)”Function parameters carry an intent qualifier declaring how the function uses the argument. The compiler chooses the optimal lowering (register-passed value, const-pointer, noalias-pointer, sret-pointer) based on intent + type + size. Programmers express what the function does; the compiler chooses how the bytes move. There are no references and no lifetime annotations in user-facing code.
The four intents:
| Intent | Meaning | Caller Retains? | Callee Mutates? | Aliasing? |
|---|---|---|---|---|
view | Read-only borrow (default) | Yes | No | Yes |
edit | Exclusive mutable borrow | Yes (frozen) | Yes | No |
take | Ownership transfer (sink) | No (consumed) | Yes | N/A |
make | Uninitialized output | After call: Yes | Yes (must initialize) | No |
Default intent is view. Omitting the qualifier means read-only borrow. This matches the immutability-by-default doctrine.
// view (default) — read-only borrowfunc length(buf: [u8]) -> usize do return buf.len endfunc length(view buf: [u8]) -> usize do return buf.len end // identical
// edit — exclusive mutable borrowfunc write(edit buf: [u8], byte: u8) do buf[buf.len] = byteend
// take — ownership transfer (caller binding marked Dead after call)func close(take f: ~File) do f.flush() f.release()end
// make — uninitialized output; callee MUST fully initialize via bulk-write primitivefunc init_zero(make buf: [256]u8) do @memzero(buf)endmake discipline (v0.1 lockdown). A make parameter is uninitialized on entry. Initialize via bulk intrinsic (@memset, @memcpy, @memzero), whole-binding assignment, or declarative literal. Element-level writes (buf[i] = x) are forbidden (E2709); reads at any point are forbidden (E2704). The bulk-write discipline reflects what the v0.1 escape analyzer can prove. SPEC-085 v0.2 may relax E2709 once flow-sensitive init tracking lands.
Composition with reference capabilities (SPEC-029). Intents and capabilities are orthogonal axes. Capabilities govern cross-actor sendability (iso/val/ref/tag); intents govern parameter passing at call sites. They compose freely:
func inspect(view buf: iso Buffer) do ... end // read-only borrow of unique reffunc mutate(edit data: ref WorkingSet) do ... end // exclusive write to local mutablefunc swallow(take buf: iso Buffer) do ... end // consume iso (≡ SPEC-029 `consume`)Forbidden combinations produce specific errors: edit val (cannot edit immutable, E2706), take ref across actor boundary (E2707), etc. See SPEC-085 §4.1 for the full composition matrix.
Composition with affine types (SPEC-015 §3, retained). Linear types ~T interact with intents naturally. SPEC-015 §3 (Affine Types) is retained in full; SPEC-015 §4 (Borrowing &T/&mut T) and §5 (Lifetimes 'a) are superseded by SPEC-085.
func use_open(view f: ~File) do ... end // non-consuming borrow of linear resourcefunc amend(edit f: ~File) do ... end // exclusive mutable borrowfunc close(take f: ~File) do ... end // canonical consumption pointFFI boundary. Raw pointers *T and [*]T survive at the FFI boundary. extern declarations retain pointer syntax — intents do NOT propagate across the seam:
@ffi(path: "crypto.zig", lang: .zig)extern func blake3_hash(data_ptr: *const u8, data_len: usize, out: *[32]u8) -> i32Within sovereign Janus, intents are the canonical surface; *T is reserved for FFI and :sovereign.
Tensor descriptors with shape sigils (:compute). Tensor and polar parameters accept shape sigils for partial shape genericity:
| Sigil | Meaning |
|---|---|
Integer literal (128, 768) | Static dimension (compile-time known) |
_ | Dynamic dimension — runtime value, compile-time rank |
* | Rank-erased — runtime rank (whole-tensor descriptor) |
// Static shape (rank + dims known at compile time)func dot(view a: tensor[f32, 128], view b: tensor[f32, 128]) -> f32 do ... end
// Rank known, dimensions runtimefunc relu(edit t: tensor[f32, _, _]) do ... end
// Rank-erased — accepts any-rank f32 tensorfunc total(view t: tensor[f32, *]) -> usize do var n: usize = 1 for k in 0..t.rank do n = n * t.dim(k) end return nend
// Polar embedding with dynamic Nfunc similarity(view a: polar[_], view b: polar[_]) -> f32 do ... endConstraints: * SHALL NOT combine with explicit dimensions or _ (E2710). polar[*] is structurally undefined (E2711) — polar embeddings have no rank concept per SPEC-070 §2.1. Named-identifier dimensions (e.g., tensor[f32, batch, ch, h, w]) are reserved for SPEC-086 (Dimensional Algebra) and rejected in v0.1 (E2712).
Migration from SPEC-015 / SPEC-017-P (deprecated):
| Old (deprecated) | New (canonical, SPEC-085) |
|---|---|
func f(x: T) | func f(x: T) (no change — implicit view default) |
func f(x: &T) | func f(view x: T) |
func f(x: &mut T) | func f(edit x: T) |
func f<'a>(x: &'a T) -> &'a T | func f(view x: T) -> T (lifetime erased; escape analysis verifies) |
tensor<T, Dims> | tensor[T, Dims...] |
tensor<f32, [N, M]> | tensor[f32, N, M] |
Lifetime annotations ('a) are eliminated from user-facing code. The &T / &mut T reference syntax is deprecated — & survives as the bitwise-AND operator and as the SPEC-017-P trait-union operator only.
Deprecation schedule (SPEC-085 §17.5):
- v2026.5.X — both old and new syntax accepted; old emits deprecation warning
- v2026.6.X — old syntax errors uniformly across all profiles
- v2026.7.X — old syntax removed from parser; migration tool ceases recognition
The aggressive timeline is justified by the empirical migration footprint: zero functional code changes required across the Janus stdlib + Graf as of 2026-05-07 (SPEC-085 §17.2 grep evidence).
4.6.1 Explicit Procedure Overloading
Section titled “4.6.1 Explicit Procedure Overloading”Inspired by Odin’s proc{...} overload sets. Janus adopts explicit overloading
with clear scope, referential transparency, and the ability to reference a specific
procedure when needed. No ambient overload soup.
Declaration:
// Overload set declaration — explicit, named listoverload to_string = { bool_to_string, int_to_string, float_to_string,}
// Usage — dispatch by argument typelet s1 = to_string(true) // -> bool_to_stringlet s2 = to_string(42) // -> int_to_stringlet s3 = to_string(3.14) // -> float_to_string
// Specific procedure referencelet f = to_string.int_to_stringlet n = f(99)Rules:
- An
overloadset is a named group of functions that share the same semantic operation. - Dispatch is static, resolved at compile time by argument types. No runtime vtable.
- Each member function MUST have a distinct signature — ambiguous overloads are rejected at declaration time (E2710).
- Overload sets are first-class values — they can be passed as parameters, stored in structs, and re-exported through sovereign indexes.
- The set name is the dispatch name; individual members are accessed via
set_name.member_namedot syntax.
// Re-export overload set through sovereign indexpub overload to_string // re-exports the set
// Pass overload set as parameterfunc format[T](val: T, fmt: overload) -> str do return fmt(val)end
let s = format(42, to_string) // dispatches to to_string.int_to_stringWhy explicit overloading beats ambient overload:
| Property | Odin (ambient) | Janus (explicit) |
|---|---|---|
| Declaration | to_string :: proc{bool_to_string, int_to_string} | overload to_string = { bool_to_string, int_to_string } |
| Scope | Module-wide, implicit | Explicit set, named members |
| Ref transparency | Must jump to declaration to see members | Members listed at declaration site |
| Specific reference | Requires workaround | to_string.int_to_string direct access |
| AI readability | Members scattered across file | All overloads visible in one place |
This fits Janus’s “Revealed Complexity” doctrine: the cost of overload resolution is explicit in the declaration, not hidden in compiler magic.
Interaction with parameter intents. Overload resolution considers the full signature including intents:
overload process = { process_view, // func process_view(view x: T) -> U process_edit, // func process_edit(edit x: T) -> U process_take, // func process_take(take x: ~T) -> U}
let v = process(my_val) // my_val matches view -> process_viewlet e = process(mut &my_val) // mut & matches edit -> process_editlet t = process(consume my_val) // consume matches take -> process_takeOverload sets vs trait dispatch. An overload set is a static name-resolution mechanism. A trait is a type-class contract with generic dispatch. They serve different purposes:
// Overload set: compile-time name resolution, same semantic operationoverload serialize = { serialize_json, serialize_cbor, serialize_sbi }
// Trait: type-class contract, generic dispatch over Selftrait Serializable { func serialize(self) -> [u8] !SerialError}Overload sets are NOT a replacement for traits. They are a replacement for
“multiple functions with different types but the same semantic name” — the
pattern that C programmers fake with foo_int, foo_float, foo_string naming
conventions. Janus makes this pattern first-class and compiler-checked.
4.7 Algebraic Effects (SPEC-090)
Section titled “4.7 Algebraic Effects (SPEC-090)”Functions declare the side-effect classes they can transitively perform. The compiler tracks effects through the call graph and rejects programs that drop unhandled effects on the floor. Effects are to function signatures what intents are to parameters.
Governed by Law 1 (§2.1): All exported public functions MUST write their effect row explicitly. Public pure APIs write !{}. Public effectful APIs write !{IO}, !{Net}, !{Ui}, etc. Private functions may still infer. A public function contract is incomplete unless its effect row is explicit — no public API may hide world contact behind inference.
This is the third leg of the static-analysis tripod (alongside SPEC-085 parameter intents and SPEC-029 reference capabilities). It supersedes SPEC-030’s language surface; SPEC-030’s semantic foundation (static dispatch, monomorphization, capability bridging, visitor API) is retained verbatim by SPEC-090.
Effect declaration. Effects are declared with the effect keyword, mirroring error:
effect IO { func stdin_read(edit buf: [u8]) -> usize func stdout_write(view bytes: [u8]) -> void}
effect Random { func next_int(max: i64) -> i64}Effect operations use SPEC-085 parameter intents natively. Default implementations are forbidden — effects are pure contracts. Effect names are PascalCase; operation names are snake_case.
The !{Effects} return-type modifier. A function that performs effects declares them on its return type, parallel to !Error:
// Pure function — no errors, no effectsfunc add(a: i64, b: i64) -> i64 do return a + bend
// Single effectfunc read_config(view path: [u8]) -> Config !{IO} do let bytes = IO.stdin_read(input_buf) return parse(bytes)end
// Error union + multiple effectsfunc fetch_blob(view cid: [32]u8, allocator: std.mem.Allocator) -> [u8] !FetchError !{IO, Net, Alloc} do // ...end
// Explicitly pure — !{} is a contract that introduces effects later trigger E2509pub func canonical_hash(view bytes: [u8]) -> [32]u8 !{} do return blake3(bytes)endThe braces follow Law 2 ({ } for data structures — the row is a set of effect tags). Order is not significant; duplicates dedupe silently.
The handle...with E do...end form. Handlers use do...end block form (Law 1, control flow), with each handler operation as a func definition:
let cfg = handle do read_config("janus.toml")?end with IO do func stdin_read(edit buf: [u8]) -> usize do return std.os.posix.read(0, buf) end func stdout_write(view bytes: [u8]) -> void do discard std.os.posix.write(1, bytes) endendMultiple effects compose via repeated with EffectName do ... end clauses. Each with clause handles exactly one effect (combining is a parse error E2510).
handle do game_turn("Alice")end with IO do func stdout_write(view bytes: [u8]) -> void do log.append(bytes) end func stdin_read(edit buf: [u8]) -> usize do return 0 endend with Random do func next_int(max: i64) -> i64 do return 4 end // deterministicendA handle is an expression; its value is the value of the handled expression. Handlers MUST cover every operation of each handled effect (E2502); innermost handler wins for a given effect; partial handling propagates unhandled effects to the enclosing scope.
Profile ambient handlers. Effects are available in every profile. What varies is the ambient handler table — which effects come with default runtime handlers automatically in scope:
| Profile | Ambient Effects | Notes |
|---|---|---|
:script | IO, Alloc | Permissive default — implicit handlers, GC/RC runtime |
:core | (none) | Effect-clean by default. All effects must be handled explicitly. |
:service | IO, Alloc, Net, Time | Standard service effects from runtime |
:cluster | :service + Send, Recv, Spawn | Actor-related effects |
:compute | :core + GPU, NPU | Hardware-acceleration effects |
:sovereign | All-of-above + Boot, MMU, Hardware | Bare-metal effects |
Effects outside the profile’s ambient set MUST be handled explicitly before reaching main (E2511). Effects are not bound by profile capability — any profile may declare and handle any effect; profile differences live in ambient defaults, not in language gating.
:core is effect-clean by ambient table, not by gate. A :core program may still declare and handle custom effects in lexical scope; the ambient set is empty so unhandled effects at main produce E2511.
Effect propagation. Unhandled effects propagate to the caller’s !{} row:
// greet performs IO — propagatesfunc greet(view name: [u8]) -> void !{IO} do IO.stdout_write("Hello, ") IO.stdout_write(name)end
// run_greeting calls greet — must declare !{IO} or handle itfunc run_greeting() -> void !{IO} do greet(b"World")endCalling an effectful function without handling or declaring its effects is E2501.
Effect polymorphism (?E, narrow form). A function may propagate effects from higher-order parameters via the ?E polymorphism variable:
// map propagates whatever effects f performsfunc map[T, U](view xs: [T], f: func(T) -> U !{?E}) -> [U] !{?E} do var out: [xs.len]U = undef for i in 0..xs.len do out[i] = f(xs[i]) end return outend
// Pure call site — map's monomorphized row is !{}let doubled = map(numbers, func(x: i64) -> i64 do return x * 2 end)
// IO call site — map's monomorphized row is !{IO}let logged = map(numbers, func(x: i64) -> i64 !{IO} do IO.stdout_write(str(x)) return xend)?E is propagation through higher-order parameters only — not Koka-style row polymorphism. Mixing ?E with concrete effects in a single row (!{IO, ?E}) is forbidden (E2512). Multiple higher-order parameters use distinct names (?Ef, ?Eg) and compose by union.
Capability bridge. Effects describe what side effect class; capabilities (SPEC-029 + SPEC-080) describe who is permitted to access. The function declaring !{IO} does NOT require IO capabilities — those shift to the handler site:
// process declares !{IO} — no capability tokens neededfunc process(view input: [u8]) -> ProcessResult !{IO} do IO.stdout_write("processing") return ProcessResult.okend
// At the handler site, the body needs CapWrite for actual filesystem writepub func main() requires CapWrite do let result = handle do process(b"data") end with IO do func stdout_write(view bytes: [u8]) -> void do std.os.fs.write_text_sovereign(wpath_cap, "/var/log", bytes)? end endendThis separation enables purely-tested handlers (no capability ceremony) to coexist with production handlers (full capability requirements).
Migration from SPEC-030 (deprecated):
| Old (deprecated) | New (canonical, SPEC-090) |
|---|---|
func f() -> T with E do | func f() -> T !{E} do |
func f() -> T with E1 + E2 do | func f() -> T !{E1, E2} do |
func f() with E do | func f() -> void !{E} do (void made explicit) |
handle expr with { E.op(args) => body, ... } | handle do expr end with E do func op(args) do body end ... end |
Effects forbidden in :core/:script (E2503) | Effects available in all profiles; ambient table determines defaults |
Lifetime annotations and reference syntax (SPEC-085 deprecation) compose with effect-syntax migration: a function previously written func f<'a>(buf: &mut [u8]) -> &'a [u8] with IO do becomes func f(edit buf: [u8]) -> [u8] !{IO} do.
Deprecation schedule (SPEC-090 §9.4):
- v2026.5.X — both
with Eand!{E}accepted;withemits W2509 deprecation warning; profile gate E2503 demoted to warning - v2026.6.X —
withsyntax errors uniformly; E2503 fully removed - v2026.7.X — old syntax removed from parser
The aggressive timeline is justified by SPEC-030’s RATIFIED-but-NOT-IMPLEMENTED status: there is no production code using the SPEC-030 surface to migrate.
4.8 Resource Capabilities (SPEC-091)
Section titled “4.8 Resource Capabilities (SPEC-091)”Resource capabilities gate access to resources (filesystems, networks, randomness, GPU/NPU contexts, hardware) at the language level. They are the authority leg of the static-analysis tripod: SPEC-090 effects describe what side effect can occur; SPEC-091 capabilities prove the right to perform it on a specific resource.
The defining principle: No ambient authority. Below :script, no resource-touching stdlib API is callable without an explicit capability token in scope. The token comes from the runtime, the hinge.kdl manifest, or a trusted authority-narrowing helper. Never from thin air.
Capability declaration. Resource capabilities are structs marked with @capability:
@capability(class: .filesystem, scope: .read_write)struct FilesystemCap { _token: u64,}
@capability(class: .network, scope: .outbound)struct NetCap { _token: u64,}
@capability(class: .cryptographic_random)struct RandomCap { _token: u64,}The @capability attribute (Three Sigil Worlds: @ is metadata) marks the struct as a capability token. The closed class universe is normative: .filesystem, .network, .cryptographic_random, .system_time, .environment, .process_control, .gpu, .npu, .hardware, .allocator (experimental), .application, .ui. Per-class scope universes (.read, .write, .read_write, .lifecycle, .main_surface, etc.) are also normative.
Application authority classes (Law 4, §2.1). Two capability classes unlock application development on :service:
@capability(class: .application, scope: .lifecycle)struct AppCap { _token: u64,}
@capability(class: .ui, scope: .main_surface)struct UiCap { _token: u64,}Companion effects:
effect Ui { func invalidate(view ui: UiCap) -> void func render(view ui: UiCap, take tree: ViewTree) -> void}
effect AppLifecycle { func quit(view app: AppCap, code: i32) -> void}Applications are :service programs with an application runtime and UI/resource capabilities. No separate :app profile exists. The same promotion path from :script prototype to :service production applies.
Construction restriction. Capability tokens SHALL be constructed only by the runtime (manifest-bound entry), authority-narrowing helpers in std.os.caps, or hazard-flagged forges (std.os.caps.unsafe_forge_*) with ⚠ sigil. All other construction is E2601:
// ❌ E2601: capability construction outside trusted contextlet bad = FilesystemCap { _token: 42 }Intent-only passing. Capability parameters MUST carry SPEC-085 intent qualifiers:
// ✅ view — read-only borrow of capabilityfunc read_file(view fs: FilesystemCap, view path: [u8]) -> [u8] !FsError !{IO} do return std.fs.read(fs, path)end
// ❌ E2602: capability parameter without intent qualifierfunc bad(fs: FilesystemCap) do ... endEffect-capability pairing. A function declaring !{IO} MUST hold a FilesystemCap (or other .filesystem-class capability). A function declaring !{Net} MUST hold a NetCap. Mismatch is E2603:
// ❌ E2603: !{IO} without FilesystemCapfunc leak(view path: [u8]) -> [u8] !{IO} do ... end
// ✅ effect-capability pair satisfiedfunc good(view fs: FilesystemCap, view path: [u8]) -> [u8] !{IO} do return std.fs.read(fs, path)endManifest binding. A binary’s hinge.kdl declares its requested capabilities; the runtime materializes them and passes them to main:
capabilities { filesystem read="/etc/janus/" read_write="/var/lib/janus/" network outbound="forge.janus-lang.org:443" cryptographic_random}pub func main(view fs: FilesystemCap, view net: NetCap, view rng: RandomCap) -> i32 !{IO, Net} do // Binary received exactly the capabilities its manifest declared. let cfg = read_file(fs, "/etc/janus/config.toml")? let conn = connect(net, "forge.janus-lang.org:443")? return 0endA binary cannot acquire a capability it didn’t declare in its manifest. Link-time cross-validation (E2604) enforces this. The manifest is the audit surface — a reviewer reading it knows the binary’s full authority before reading code.
Authority narrowing. The stdlib provides std.os.caps.narrow_* helpers for monotonic capability narrowing:
// Narrow read-write to read-onlylet ro_fs = std.os.caps.narrow_fs_to_read(view fs)
// Narrow to specific subdirectorylet scoped_fs = std.os.caps.narrow_fs_to_subdir(view fs, "/var/lib/myapp/")?Narrowing is one-way; widening is structurally impossible (no narrow_to_read_write exists). Narrowing helpers take view of the original capability — the original remains live; the narrower capability is a separately materialized token.
Composition with SPEC-029 reference capabilities. A capability MAY be wrapped in iso/val/ref/tag for cross-actor sending:
// Send a fs cap to another actor as an iso referencelet fs_for_actor: iso FilesystemCap = consume my_fs_capsend worker <- fs_for_actorThe wrapper’s reference-capability rules apply normally; the underlying capability’s construction-restriction is independent of the wrapper.
Composition with SPEC-080 sovereign pledge/unveil. SPEC-091 sits above SPEC-080. SPEC-080 governs the OS-syscall surface (pledge { wpath } auto-materializes WpathCap); SPEC-091 governs user-space resource authority (FilesystemCap from manifest). A function may hold both:
pub func write_log(view fs: FilesystemCap, view bytes: [u8]) -> !void !{IO} do pledge { wpath } // SPEC-091 fs grants user-space resource authority // SPEC-080 wpath_cap (auto-materialized) grants OS-syscall authority return std.os.fs.write_text_sovereign(wpath_cap, log_path(fs), bytes)endProfile gating. Per the uniform-enforcement principle, the language surface (@capability declarations, capability-typed parameters) is available in every profile. What varies is which profile requires capability discipline in the stdlib:
| Profile | Capability machinery |
|---|---|
:script | Disabled by ambient table — implicit capabilities for ergonomics |
:core | Available but not required (foundational types only) |
:service | Required — all stdlib resource APIs take capability parameters |
:cluster | :service + actor-bound capabilities (capabilities default to iso) |
:compute | :core + GPU/NPU capability tokens |
:sovereign | All — including raw hardware (.hardware class — MMU, interrupts, MMIO) |
The :service row is where Janus claims operational sovereignty: every resource-touching :service stdlib function takes a capability parameter. There is no ambient std.fs.read(path) in :service. Only std.fs.read(fs, path). That is the operational difference between Janus and every other production language.
Stdlib migration (mechanical, post-ratification):
| Pre-SPEC-091 | Post-SPEC-091 |
|---|---|
std.fs.read(path) | std.fs.read(view fs: FilesystemCap, path) |
std.net.connect(addr) | std.net.connect(view net: NetCap, addr) |
std.crypto.random_bytes(buf) | std.crypto.random_bytes(view rng: RandomCap, edit buf) |
std.time.now() | std.time.now(view tcap: SystemTimeCap) |
:script retains ambient handlers backward-compatibly per [CAP:11.1.3]; :service and above mandate explicit passing.
4.9 Refinement Types (SPEC-092)
Section titled “4.9 Refinement Types (SPEC-092)”Refinement types extend the existing where clause (SPEC-026 trait bounds) to value predicates — invariants the compiler proves via an embedded SMT solver (Z3) at compile time. Pay-for-what-you-use: the solver runs only on functions with where clauses on values. Code without refinements pays nothing.
The where clause on values:
// Refinement on a primitivetype PositiveInt = i64 where self > 0
// Refinement at a parameterfunc sqrt(view n: f64 where n >= 0.0) -> f64 do // Compiler proved n >= 0; no runtime check neededend
// Cross-parameter binding (refinement of one param refers to another)func get[T](view arr: [T], view idx: usize where idx < arr.len) -> T do return arr[idx] // bounds check elided — proved at call siteend
// Refinement on return value (using `result` keyword)func abs(view n: i64) -> i64 where result >= 0 do if n < 0 do return -n end return nend
// Refinement with §-comptime predicatestype AlignedAddr[A: usize] = usize where §{ self % A == 0 }The where clause was already in the language for trait bounds. Refinements extend the same syntax to value predicates — no new keyword, no new structural shape.
The result keyword. For return-type refinements, result is the implicit binding for the returned value (mirroring self for receivers). It is bound only inside return-type where clauses; using it elsewhere is E2902.
Predicate restrictions. Refinement predicates SHALL be:
- Pure (effect row
!{}per SPEC-090); calling effectful functions is E2903 - Side-effect-free (no
=assignments, noeditoperations) - Straight-line expressions (no
if/while/forinside the predicate; E2905)
SMT solver integration. The compiler ships with embedded Z3 (CVC5 fallback). Solver is invoked only for proof obligations generated by refinements. Solver invocations are cached across builds. When a proof fails, the compiler emits E2900 with the unprovable predicate, the source location, and a counterexample extracted from the SMT model:
E2900: refinement unprovable at src/main.jan:42:8 expected: idx < arr.len counterexample: arr.len = 0 idx = 0 hint: the call site must establish idx < arr.len before this callThree resolution paths for E2900:
- Strengthen the type — convert the receiving variable to refinement-typed; pushes the obligation upward to the originating call site.
- Add an explicit runtime check —
if x_satisfies_predicate do call(x) end; conditional narrowing inside thethenbranch makes the predicate provable. - Mark with
@trusted— downgrade E2900 to W2900; user accepts proof obligation explicitly. In:sovereign,@trustedproduces a runtime panic if the predicate is observed false.
Conditional narrowing (occurrence typing). Inside a branch, the compiler narrows the refinement based on the branch condition:
func safe_sqrt(view n: f64) -> f64 do if n >= 0.0 do return sqrt(n) // n's refinement narrowed to `n >= 0.0` here; SMT proves it else return 0.0 endendComposition with intents:
func write_at( edit buf: [u8] where buf.len > 0, view idx: usize where idx < buf.len, view byte: u8,) do buf[idx] = byte // both bounds proven, both checks elidedendComposition with capabilities:
type ReadOnlyFs = FilesystemCap where self.scope == .read
func read_audit_log(view fs: ReadOnlyFs, view path: [u8]) -> [u8] !{IO} do ... endProfile gating — uniform language surface, profile-specific enforcement strictness:
| Profile | Refinement enforcement |
|---|---|
:script | Accepted, warnings only (E2900 → W2900) — ergonomic exception |
:core | Optional, fully enforced when present |
:service | Optional, fully enforced when present |
:cluster | Optional, fully enforced when present |
:compute | Recommended — tensor shape predicates valuable |
:sovereign | Recommended + @trusted triggers runtime panic on violation |
Refinements are never required. Code can compile without any where clauses on values; the SMT solver is invoked only when refinements are present. The pragmatic ruling per [REF:10.1.2]: profile gating differs from SPEC-085/SPEC-090 uniform-enforcement because refinements are an opt-in proof discipline — uniform enforcement of an optional feature would be incoherent.
What refinements subsume. Rust’s NonZeroU32, NonNull<T>, NonEmpty<T>, alignment newtypes, and ad-hoc wrapper-per-invariant patterns all reduce to ordinary types plus refinements. One general mechanism replaces a wardrobe of special cases.
4.10 Comptime Trichotomy (SPEC-093)
Section titled “4.10 Comptime Trichotomy (SPEC-093)”SPEC-093 formalizes the three distinct compile-time concerns that were previously aliased in SPEC-027b. Three positions, three forms, one sigil-world (§):
Governed by Law 3 (§2.1): Every abstraction lowers as proof-only (erased after checking), specialized (monomorphized/inlined/statically dispatched), or explicit-runtime. No invisible heap, ARC, or dynamic dispatch. The compiler MUST expose cost through janus explain-cost.
| Position | Form | Role | Lowering |
|---|---|---|---|
| Expression | §{ expr } or §builtin(args) | Returns a value at compile time | proof-only (erased) |
| Statement | comptime do ... end | Performs comptime side effects (asserts, codegen, sema checks) | proof-only (erased) |
| Parameter | comptime <name>: <type> | Marks a parameter as compile-time known | specialized (monomorphized) |
Mixing positions is rejected: §{ assert(...) } as a top-level statement is E2950 (use comptime do ... end); let x = comptime do compute() end is E2951 (use §{ ... }).
The !{Comptime} effect class. Comptime blocks declare effect row !{Comptime}, which is mutually exclusive with runtime effect classes. IO, allocation, networking, randomness, time queries, actor effects, device dispatch are all forbidden inside comptime — composing the existing SPEC-090 effect-graph machinery with the comptime VM:
comptime do // Allowed: pure computation, type introspection, §-builtins let info = §type_info(T) let size = §size_of(T)
// Forbidden: // let f = open_file("foo.txt") // E2940: IO in comptime // let buf = allocator.alloc(64) // E2941: Alloc in comptimeend!{Comptime} implies total (per SPEC-094 [TOT:6.1.3]) — the comptime VM has bounded recursion limits, so any !{Comptime} function is automatically termination-clean. This makes comptime functions callable from total contexts without the infection-rule violation.
Profile gating: comptime is foundational language machinery, available in every profile.
4.11 Total Functions (SPEC-094)
Section titled “4.11 Total Functions (SPEC-094)”A total function is one the compiler proves terminates on every input. The modifier appears before func, parallel to pub:
// Compiler-verified terminatingtotal func factorial(view n: usize) -> usize do if n == 0 do return 1 end return n * factorial(n - 1) // structural recursion on nend
// Explicit non-totalitypartial func event_loop() -> never do while true do // ... endend
// Default — termination not asserted (backwards compatible)func parse_loop(reader: *Reader) -> !void do while true do ... endendThree termination proof strategies:
- Structural recursion — recursive call has a strictly smaller argument by a well-founded ordering (
n - 1,xs.tail(), subterm). Free; no SMT invocation. - Bounded loops —
for i in 0..N dowhereNis comptime-known or refinement-bounded. Free. - Explicit termination measure —
total func f(...) -> T by <expr>declares the decreasing measure. Verified via SPEC-092 SMT solver.
Total infection rule. A total function MAY NOT call a non-total function (E2951). Same propagation as SPEC-090 effect rows — totality is a property the compiler refuses to silently lose.
Composition. Total composes orthogonally with effects: total !{} (pure-total — most restrictive, for cryptographic primitives), total !{IO} (terminating with IO — for SLA-critical paths), partial !{} (pure but may diverge — functional iterators), partial !{IO} (default — most permissive). Refinements provide preconditions; totality provides termination; both verified independently.
Profile gating — uniform language surface, profile-specific mandates:
| Profile | Total guarantees |
|---|---|
:script | Optional, warnings only |
:core | Stdlib hot paths SHALL be total where feasible (std.core.conv, std.core.mem, std.math primitives) |
:service | Optional but recommended for SLA-critical paths |
:cluster | Required for any @realtime-tagged actor (per SPEC-021) |
:compute | Tensor primitives SHALL be total (shape-bounded loops always terminate) |
:sovereign | Cryptographic primitives MUST be total (side-channel discipline; constant-time guarantee) |
When the compiler cannot construct a termination proof, it emits E2950 with diagnostic guidance: strengthen the recursion structure, add a by <expr> measure clause, or downgrade to non-total. The @trusted total annotation downgrades E2950 to W2950 for migration cases; in :sovereign, @trusted produces a runtime panic if non-termination is observed.
4.12 Row Polymorphism / Shape Types (SPEC-095)
Section titled “4.12 Row Polymorphism / Shape Types (SPEC-095)”Shape types are structural type constraints over records. A function declares which fields it depends on; any record containing those fields satisfies the parameter type.
// Function depends on `name: string`; accepts any record with that fieldfunc get_name[r](view rec: shape { name: string, ..r }) -> string do return rec.nameend
// Different concrete records all satisfystruct User { name: string, email: string, age: i32 }struct Account { name: string, balance: f64 }
let n2 = get_name(Account { name: "savings", balance: 1000.0 })// Both compile — each call site monomorphizes shape against the concrete typeThe ..r row variable captures additional fields beyond the listed ones. Declared in the type-parameter list alongside type parameters (lowercase convention to distinguish: r, s for rows; T, U for types). At each call site, r instantiates to the concrete extra-field set; the function is monomorphized per instantiation.
Strict matching — field-type matching is exact; no implicit subtyping (E2961). Order of fields is not significant. A shape without ..r rejects records with extra fields (E2962); the lenient form (with ..r) is the common case for schema-evolution-friendly APIs.
Schema evolution use case — the killer application:
struct ConfigV1 { host: string, port: u16 }struct ConfigV2 { host: string, port: u16, tls: bool } // new field
func describe[r](view cfg: shape { host: string, port: u16, ..r }) -> string do return §"Config: {cfg.host}:{cfg.port}"end
let s1 = describe(ConfigV1 { host: "a", port: 80 }) // ✅let s2 = describe(ConfigV2 { host: "b", port: 443, tls: true }) // ✅ extra field passes throughComposition rules:
- Intents (SPEC-085):
view/edit/takework normally;makeis forbidden on shape types (E2964) — incomplete layout cannot be initialized. - Effects (SPEC-090): orthogonal; shapes may carry any effect row.
- Refinements (SPEC-092): may reference shape’s listed fields (
where idx < rec.items.len); cannot reference row-variable fields (E2965). - Totality (SPEC-094):
totalshape-polymorphic functions verify per monomorphization. - Capabilities (SPEC-091): capability types SHALL NOT appear inside shape types (E2966) — preserves SPEC-091’s construction discipline.
Comptime row introspection:
func describe_extra[r](view rec: shape { id: u64, ..r }) -> string do var s = §"id={rec.id}" inline for §row_fields(rec) |field| do let value = §field(rec, field.name) s = s ++ §" {field.name}={value}" end return sendThe §row_fields builtin returns the comptime list of row-variable-captured fields, composing with SPEC-093’s expression-form §-builtins.
Profile gating — the single explicit exception to uniform language surface. Shape types are available in :service and above; forbidden in :script and :core (E2960). The exclusion is documented honestly in SPEC-095 §10.1.2: row-polymorphic type inference adds compile-time complexity that foundational profiles deliberately omit. This is the single profile-availability exception in tonight’s eight-axis design batch — every other axis preserves uniform language surface.
| Profile | Shape types |
|---|---|
:script | ❌ Forbidden (E2960) |
:core | ❌ Forbidden (E2960) |
:service | ✅ Available |
:cluster | ✅ Available |
:compute | ✅ Available |
:sovereign | ✅ Available |
5. Control Flow
Section titled “5. Control Flow”5.1 If-Else
Section titled “5.1 If-Else”// No parentheses around condition. Uses do...end.if count > 0 do process()else skip()end
// Single-line (statement separators are optional inside do-blocks;// neither `;` nor a newline is required before `end`)if c >= '0' and c <= '9' do return c - '0' end
// Optional unwrap (RFC-019 v0.1 — pattern-binding `with`)// The legacy `if optional do |val| ... end` form was removed pre-1.0// as a Syntactic Honesty violation; `|val|` everywhere else in Janus// means closure parameter, not pattern-bind. Use `with .Some(v) <- expr`:with .Some(otc) <- old_tree_cid do old_tree = store.getTree(otc) catch return DiffError.DiffFailed;end5.2 While
Section titled “5.2 While”var i: usize = 0;while i < N do out[i] = compute(i); i = i + 1;end5.3 For
Section titled “5.3 For”// Iterate with capturefor entries do |entry| process(entry.name, entry.cid);end
// Range iterationfor 0..N do |i| buf[i] = 0;end
// Exclusive rangefor 0..<10 do |i| // i = 0, 1, ..., 9end5.4 Match
Section titled “5.4 Match”// 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 guardsmatch 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
| Symbol | Level | Purpose | Valid context |
|---|---|---|---|
else | Expression | Match fallback (catch-all) | match x { else => ... } |
_ | Pattern | Discard binding | let _ = expr, catch |_|, (_, 0, _) in destructure |
discard | Statement | Discard expression result | discard some_function() |
- Use
elsefor match fallbacks (catch-all arm) - Use
_when you don’t need a value in a pattern context (variable binding, catch, destructuring) - Use
discardwhen 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.
5.5 Labeled Blocks and Break
Section titled “5.5 Labeled Blocks and Break”const value = blk: { if condition do break :blk 42; end break :blk 0;};6. Error Handling
Section titled “6. Error Handling”See also: §21 Algebraic Effects (SPEC-090). Error unions (
!ErrorType) and effect rows (!{Effects}) both sit at the return-type position and stack together:func f(...) -> T !ErrorType !{Effects} do ... end. Errors are control/result alternatives; effects are world-contact obligations. They share return-type position but not ontology.
6.1 Error Propagation (try and ?)
Section titled “6.1 Error Propagation (try and ?)”// try: propagate error to callerconst data = try store.read(cid);const hi = try decodeNibble(hex[0]);
// ? suffix: same as try (alternative syntax)const result = fallible()?6.2 Error Handling (catch)
Section titled “6.2 Error Handling (catch)”// catch with fallback valueconst cid = fromHex(content) catch return RefError.InvalidRef;
// catch with error captureconst sched = Scheduler.init(alloc, 4) catch |err| do log.error("init failed: {}", .{err}); return error.RuntimeInitFailed;end;
// catch unreachable (assert success)var rt = GrafRuntime.init(alloc, 2) catch unreachable;6.3 Error Return
Section titled “6.3 Error Return”// Return an errorreturn HexError.InvalidChar;return error.DiffFailed;
// fail keyword (alternative)fail DivisionError.DivisionByZero6.4 Explicit Discard (discard)
Section titled “6.4 Explicit Discard (discard)”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 valuediscard some_function()
// Discard with error handlingdiscard may_fail() catch |err| do log_error(err)end
// Common use case: loggingdiscard logger.write("message")
// Multiple discards in sequencediscard 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.”
6.5 Defer and Errdefer
Section titled “6.5 Defer and Errdefer”// defer: runs on scope exit (success or error)defer allocator.free(matched);defer rt.deinit();
// errdefer: runs ONLY on error exitvar out: std.ArrayListUnmanaged(u8) = .empty;errdefer out.deinit(allocator);try encodeValue(allocator, &out, value);return out.toOwnedSlice(allocator);
// Conditional deferdefer if owns_old do freeTree(allocator, old_tree); end7. Generics (SPEC-026)
Section titled “7. Generics (SPEC-026)”7.1 Generic Functions
Section titled “7.1 Generic Functions”Type parameters in square brackets. Always explicit at call site (no type inference).
// Definitionfunc identity[T](x: T) -> T do return xend
// Call site (type always explicit)let n = identity[i64](42)let s = identity[[]const u8]("hello")7.2 Trait Bounds
Section titled “7.2 Trait Bounds”func min[T: Ord](a: T, b: T) -> T do if a <= b do return a end return bend
// Multiple boundsfunc 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 valueend7.3 Core Traits
Section titled “7.3 Core Traits”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}
trait Float: Numeric { // Float implies Numeric func sqrt(self) -> Self func sin(self) -> Self func cos(self) -> Self}
trait Integer: Numeric { // Integer implies Numeric // Marker trait — no methods}
trait SignedInteger: Integer {} // i8, i16, i32, i64trait UnsignedInteger: Integer {} // u8, u16, u32, u64trait Enum {} // All enum types, provides E.UnderlyingAll integer and float primitives satisfy all applicable traits intrinsically. bool satisfies Eq only.
7.4 Binary Relation Traits (SPEC-042, compile-time only)
Section titled “7.4 Binary Relation Traits (SPEC-042, compile-time only)”These are comptime-only traits — no methods, no runtime cost. The compiler derives them from type metadata.
trait LosslesslyConvertible[S, T] {} // widen[T](x: S) boundtrait LayoutCompatible[S, T] {} // bitcast[T](x: S) boundtrait SameWidth[S, T] {} // signed/unsigned boundtrait Narrowing[S, T] {} // clampTo SIMD boundUsers cannot impl these traits. They are compiler-derived facts about the type system.
7.4 Monomorphization
Section titled “7.4 Monomorphization”Each unique (function, type_args) pair compiles to a separate specialized function:
min[i32](3, 5) // emits: min_i32min[f64](3.14, 2.7) // emits: min_f64Trait bounds are verified once at definition, not per instantiation.
8. Comptime (SPEC-027)
Section titled “8. Comptime (SPEC-027)”Compile-time evaluation. Code runs in the compiler, vanishes before the binary exists.
8.1 Comptime Blocks
Section titled “8.1 Comptime Blocks”The §{ } syntax is the canonical form for compile-time expression evaluation:
// Expression form: single comptime valueconst SIZE = §{ calculate_optimal_size(PAGE_SIZE) }The comptime do ... end keyword form is accepted as an alias and desugars to §:
// Module scope: evaluated once during compilationcomptime do assert(PROTOCOL_VERSION >= 1, "requires protocol v1+")end8.2 Comptime Parameters
Section titled “8.2 Comptime Parameters”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 outputend8.3 Comptime Type Parameters
Section titled “8.3 Comptime Type Parameters”// comptime T: type gives full introspective accessfunc decode(comptime T: type, bytes: []u8) -> ?T do const info = §type_info(T) // requires :service // ...end8.4 Builtin Table
Section titled “8.4 Builtin Table”:core+ builtins (available everywhere):
| Builtin | Input | Output | Purpose |
|---|---|---|---|
§size_of(T) | type | usize | Byte size of type |
§align_of(T) | type | usize | Alignment requirement |
§offset_of(T, field) | type, string | usize | Field byte offset in struct |
§is_integral(T) | type | bool | True for integer types |
§is_float(T) | type | bool | True for float types |
§fmt(args...) | variadic | string | Comptime string construction |
§compile_error(msg) | string | never | Halt compilation with message |
:service+ builtins (type introspection):
| Builtin | Input | Output | Purpose |
|---|---|---|---|
§type_info(T) | type | TypeInfo | Full type metadata |
§type_name(T) | type | string | Fully qualified name |
§type_id(T) | type | u64 | Stable hash identifier |
§fields(T) | type | []FieldInfo | Struct field list |
§has_field(T, name) | type, string | bool | Field existence check |
§has_decl(T, name) | type, string | bool | Declaration existence |
§field(val, name) | any, string | field type | Access field by name |
§field_type(T, name) | type, string | type | Type of named field |
8.5 Inline Control Flow
Section titled “8.5 Inline Control Flow”// inline for: unrolls at compile timeinline for §fields(T) |field| do const field_value = §field(value, field.name) process_field(field_value)end
// inline switch: eliminates dead branchesinline switch §type_info(T) do .Struct => |s| encode_struct(value, s) .Int => |i| encode_int(value, i) else => comptime do §compile_error("unsupported") endend
// if comptime: conditional compilationif comptime §is_integral(T) do optimized_int_path()else generic_path()end8.6 Wire Layout Verification Pattern
Section titled “8.6 Wire Layout Verification Pattern”extern struct ResponseFrame do seq: u64 status: u8 has_tip: u8 _pad: [2]u8 detail_len: u32 final_tip: [32]u8end
comptime do assert(§size_of(ResponseFrame) == 48, "frame must be 48 bytes") assert(§offset_of(ResponseFrame, "seq") == 0, "seq at offset 0") assert(§offset_of(ResponseFrame, "status") == 8, "status at offset 8") assert(§offset_of(ResponseFrame, "detail_len") == 12, "detail_len at offset 12") assert(§offset_of(ResponseFrame, "final_tip") == 16, "final_tip at offset 16")end9. Type Conversion (std.core.conv)
Section titled “9. Type Conversion (std.core.conv)”All conversions are explicit. Janus never silently coerces between types.
9.1 The Trichotomy
Section titled “9.1 The Trichotomy”Every conversion belongs to exactly ONE of three categories:
| Category | Function | Return | Cost |
|---|---|---|---|
| Total | widen[T] | T | Free |
| Fallible | to[T] | !T | Range check |
| Lossy | truncate[T] | T | Free, info loss |
9.2 Widening (Total)
Section titled “9.2 Widening (Total)”func widen[T](value: auto) -> T where LosslesslyConvertible[auto, T]Source provably fits in target. Zero-cost, compiler-checked.
let n: i32 = 42let f: f64 = widen[f64](n) // Always safe, no runtime cost9.3 Checked Narrowing (Fallible)
Section titled “9.3 Checked Narrowing (Fallible)”func to[T](value: auto) -> !TReturns error if value doesn’t fit in T.
let wide: i64 = 42let narrow = to[u8](wide)? // Ok(42)let fail = to[u8](256) // Err(Overflow)let neg = to[u8](-1) // Err(Overflow)9.4 Unchecked Truncation (Lossy)
Section titled “9.4 Unchecked Truncation (Lossy)”func truncate[T](value: auto) -> TDiscards high bits. Always succeeds.
let byte = truncate[u8](0x1FF) // 0xFF (low 8 bits)let zero = truncate[u8](256) // 0x009.5 Rounding
Section titled “9.5 Rounding”func round[T: Integer](x: f64, mode: RoundMode) -> !TRound a float to integer with specified mode. RoundMode variants: .Nearest, .TowardZero, .TowardPosInf, .TowardNegInf.
let n: !i32 = round[i32](3.7, .Nearest) // Ok(4)let m: !i32 = round[i32](3.7, .TowardZero) // Ok(3)9.6 Bit Reinterpretation
Section titled “9.6 Bit Reinterpretation”func bitcast[T](value: auto) -> T where LayoutCompatible[auto, T]Same bits, different type. Source and target must have identical size and alignment.
let f = bitcast[f32](0x42280000) // 42.09.7 Sign Reinterpretation
Section titled “9.7 Sign Reinterpretation”func signed[T](value: auto) -> T where SameWidth[auto, T], auto: UnsignedIntegerfunc unsigned[T](value: auto) -> T where SameWidth[auto, T], auto: SignedIntegerFlip sign interpretation without changing bits.
let u: u32 = 0xFFFFFFFFlet i = signed[i32](u) // -1let j: i32 = -1let v = unsigned[u32](j) // 0xFFFFFFFF9.8 Enum Conversion
Section titled “9.8 Enum Conversion”// Integer to enum (checked)func toEnum[T](value: auto.Underlying) -> !T
let color = toEnum[Color](1)? // Color.Greenlet 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.
9.9 Boolean Conversion
Section titled “9.9 Boolean Conversion”func fromBool(value: bool) -> u8 // 0 or 1func toBool(value: auto) -> bool // non-zero is true9.10 String Parsing (Zero-Allocation)
Section titled “9.10 String Parsing (Zero-Allocation)”func parse[T](input: str) -> !Tfunc parseConsume[T](edit input: str) -> !Tparse is strict — trailing input is an error. parseConsume advances the slice past the consumed prefix; the edit intent (SPEC-085) declares exclusive mutable access to the input slice for the duration of the call.
let n: !i32 = parse[i32]("42") // Ok(42)let bad: !i32 = parse[i32]("42 ") // Err(TrailingInput)9.11 Value Formatting (Zero-Allocation, Atomic)
Section titled “9.11 Value Formatting (Zero-Allocation, Atomic)”func format[T](value: T, edit buf: [u8]) -> !usizefunc formatLen[T](value: T) -> usizeformat writes into a caller-owned buffer via the edit intent (SPEC-085). Returns Err(BufferTooSmall { needed }) if buffer too small — buffer is unchanged on error (atomic). formatLen returns exact byte count without touching any buffer.
var buf: [32]u8let written: !usize = format[i32](42, buf[..]) // Ok(2), buf[0..2] == "42" — edit intent inferred at calllet len = formatLen[i32](42) // 29.12 Error Type
Section titled “9.12 Error Type”error ConvError { Overflow, Underflow, NegativeUnsigned, InvalidDigit, InvalidEnum, EmptyInput, TrailingInput, BufferTooSmall { needed: usize },}10. Memory Operations (std.core.mem_generic)
Section titled “10. Memory Operations (std.core.mem_generic)”// Slice copy (non-overlapping, equal length)func copy[T](dst: []T, src: []T) !i64
// Slice fillfunc set[T](dst: []T, value: T)
// Pointer casts (unsafe)func ptrCast[T](ptr: ptr) -> Tfunc constCast[T](ptr: ptr) -> Tfunc alignCast[T](ptr: ptr) !T // checked alignment11. Imports and Module System
Section titled “11. Imports and Module System”11.1 The use Doctrine
Section titled “11.1 The use Doctrine”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 exportsuse identity.types
// Access symbols through the namespacelet did = types.parse_did("did:key:z6Mk...")let err = types.ResolveError.NotFound
// Import from same directoryuse resolveruse envelope
// Import from subdirectoryuse method.keyuse vc.normalize
// Import from Janus stdlibuse std.encoding.cboruse std.crypto.mldsa65Resolution algorithm (pipeline Stage 2.5):
- Join path components with
/, append.jan→ e.g.,identity/types.jan - Resolve relative to source file’s directory
- Fallback: resolve relative to CWD
- Future (SPEC-041): resolve by CID via content-addressed registry
11.2 Zig Grafting (use zig)
Section titled “11.2 Zig Grafting (use zig)”When bridging to Zig modules (stdlib or local), use the use zig graft syntax.
This is the ONLY context where file paths appear in import statements.
// Import Zig stdlib module (zero FFI overhead — Janus compiles through Zig)use zig "std/hash/blake3.zig"use zig "std/service/ns_msg/transport.zig"
// Import a local Zig bridge fileuse zig "bridge/net_bridge.zig"Think of use zig as a filesystem bridge resolver:
-
use zig "path"is resolved by Janus using four strategies:- absolute path
- path relative to the current Janus source file directory
std/...paths under Janus root (JANUS_RUNTIME_DIRis used to infer Janus root as its parent)- bare names:
std/<name>/mod.zigif it existsstd/<name>.zigfallback
- cwd fallback as the final fallback
-
The resolved absolute file is parsed into the extern registry before codegen.
Concrete example (assuming JANUS_RUNTIME_DIR=/repo/janus/runtime):
// Janus sourceuse zig "std/net/http/client.zig"Janus resolves this as:
JANUS_RUNTIME_DIRpoints at/repo/janus/runtime- Janus root is
/repo/janus - final Zig source path becomes
/repo/janus/std/net/http/client.zig
- In Stage 6b, Janus compiles each gathered Zig file with
zig build-objand links the resulting object back into the final binary.
For the Zig file itself, normal Zig import rules apply at compile time:
// In the gathered Zig source (or another nearby module)const client = @import("std/net/http/client.zig");Because the module is already being compiled from Janus std tree roots, this import resolves via Zig’s own module rules to the same Janus runtime-standard layout.
Janus additionally injects a small compatibility module wiring set during this compile step for known bridge dependencies (noise, sys_random, compat_fs, compat_time) so those imports keep deterministic object-caching behavior.
11.3 Grafting (graft)
Section titled “11.3 Grafting (graft)”Grafting Doctrine (GD-1): Foreign libraries are explicit, contained, and replaceable.
Separate grammar forms for each graft type. The = assignment syntax binds a local alias;
the right-hand side declares the foreign source.
The Five-Form Taxonomy:
| Form | Syntax | Purpose |
|---|---|---|
use jan | use std.crypto.blake3 | Native Janus modules, content-addressed |
use zig | use zig "std/hash/blake3.zig" | Zig module bridge, stdlib/bootstrap |
graft c | graft ip = c "ip-stack.h" link "ip-stack" | C header + linked library |
graft rust | graft blake3 = rust "crates.io:[email protected]" | Cargo crate through C-ABI wrapper |
graft vendor | graft rl = vendor "graphics:[email protected]" | Curated Janus-blessed foreign package bundle |
11.3.1 C Graft
Section titled “11.3.1 C Graft”graft oqs = c "oqs/oqs.h" link "oqs"graft argon2 = c "argon2.h" link "argon2"graft ip = c "ip-stack.h" link "ip-stack"
// Use via aliaslet sig = oqs.OQS_SIG_ml_dsa_65_sign(...)let hash = argon2.argon2id_hash(...)11.3.2 Rust Graft
Section titled “11.3.2 Rust Graft”Rust crates are compiled through C-ABI exports. Cargo builds a static library; Janus generates bindings.
let hash = blake3.hash(data)The Rust graft spec already establishes: C-ABI exports, Cargo static library build,
Janus-side binding generation. Unknown functions default to :sovereign profile —
foreign code is a loaded gun in a clown mask.
11.3.3 Vendor Graft — Curated Foreign Batteries
Section titled “11.3.3 Vendor Graft — Curated Foreign Batteries”This is Odin’s best idea, stolen and hardened for Janus. A vendor package is NOT “random thing downloaded from the internet.” It is a curated graft capsule — convenience with CID pinning, generated wrappers, profile tags, and capability manifests.
Syntax:
// Curated vendor graft — alias = vendor "namespace:name@version"graft curl = vendor "net:curl@8"
// Use via aliasrl.init_window(1280, 720, "Janus")sdl.poll_event(...)lua.dostring("print('hello')")Vendor Capsule Manifest (KDL):
Each vendor graft resolves against a curated capsule definition:
vendor "graphics:raylib" { version "5.5" source "upstream:raylib" license "Zlib" abi "c" headers ["raylib.h"] libs ["raylib"] profile "service" capabilities ["graft.c", "gpu", "filesystem.read"] sha256 "b3f8c1d9a2e4..." wrapper_cid "bafkreia7q..."}The manifest provides:
- version — pinned version, never floating
- source — upstream origin for rebuild verification
- abi — C, C++, or raw symbol export
- headers/libs — compilation units
- profile — minimum required profile (
:script,:service,:sovereign) - capabilities — required capability tokens (checked at link time)
- sha256 — content integrity pin
- wrapper_cid — content-addressed Janus wrapper module
Vendor Library Resolution. graft vendor does NOT download libraries from the internet.
It resolves against system-installed libraries already present on the host. The KDL
manifest provides metadata and integrity verification; the actual .so/.a/.dylib bytes
live on the filesystem, installed through the platform’s native package manager.
Resolution chain (in order):
- pkg-config lookup —
pkg-config --libs raylibreturns-lraylib -lm - System library paths —
/usr/lib/libraylib.so,/usr/local/lib/libraylib.a - Platform-specific —
/opt/homebrew/lib/libraylib.dylib(macOS),/usr/lib/x86_64-linux-gnu/libraylib.so(Debian multiarch) - Manual path override —
--vendor-path=/custom/libcompiler flag
The Janus compiler never reaches out to the network during vendor resolution.
The source field in the manifest records the upstream origin for audit and rebuild
verification — it is NOT a download URL.
$ janus build --vendor-path=/usr/local/lib app.jan# Resolves graft rl = vendor "graphics:[email protected]"# → /usr/local/lib/libraylib.so.5.5.0# → sha256 matches manifest → ACCEPTIntegrity verification. Before linking, the compiler hashes the resolved library
and compares against the manifest’s sha256. Mismatch produces E2720 — the
system-installed library does not match the curated capsule. The user must either
install the correct version or pin a different version in the graft declaration.
Fallback to prebuilt (opt-in only). If no system library is found, the compiler emits W2721 with the upstream source URL. The user may then explicitly request a prebuilt download:
janus build --vendor-fetch=allow app.janThis downloads the prebuilt from a Janus-curated mirror, verifies the hash, and
caches it under ~/.cache/janus/vendor/. Network access is gated behind an explicit
flag — no silent downloads, no ambient internet dependency.
This is the operational difference from Odin: Odin’s vendor library is bundled with the compiler distribution. Janus’s vendor system trusts the host’s package manager and cryptographically verifies what it finds. The capsule manifest is the bridge between “the system already has raylib” and “the compiler can prove it’s the right one.”
Generated wrapper namespace. After grafting, use the alias directly or via
the graft namespace:
func main(view gfx: GpuCap) -> i32 !{IO} do rl.init_window(1280, 720, "Janus") // ... or equivalently: // graft.rl.init_window(1280, 720, "Janus")endVendor vs Package — The Trust Boundary:
graft foo = package "hinge:somebody/[email protected]" // external Hinge package, signed/CID-pinned, less trusted| Property | vendor | package |
|---|---|---|
| Curation | Janus-reviewed | Community-submitted |
| Wrapper | Checked-in, maintained | Auto-generated |
| Stability | Stable, versioned API | Best-effort |
| Capabilities | Manifest-declared, verified | Self-declared |
| Use case | Graphics, game dev, crypto, compression | Ecosystem libraries |
This matters because Raylib, SDL, curl, Lua, Vulkan bindings are things we can bless. “Somebody’s AI-generated websocket library” should not wear the same priest robe.
11.3.4 Odin Theft — What We Steal and What We Don’t
Section titled “11.3.4 Odin Theft — What We Steal and What We Don’t”Steal:
- Vendor batteries for graphics/game/app development
- Explicit overload sets (§4.6.1)
- Data-oriented ergonomics (vec types, §3.9)
- Zero-drama C interop
- Fast “first 30 minutes” experience
Do NOT steal blindly:
usingas silent field promotion everywhere- Ambient vendor trust (npm with a nicer coffin)
- Public-by-default package culture
- “Manual memory is fine because adults are present” as the whole safety story
Janus does better: explicit capability tokens, manifest authority, profile escalation, and generated wrappers where FFI danger is visible. SPEC-091’s direction is exactly that: resource access is gated by manifest-bound capability tokens, not ambient calls from anywhere.
Odin is worth studying because it is joyful. Janus must not become so doctrinally armored that nobody wants to touch it. A sovereign language still needs a “build the damn thing” path.
graft vendoris that path.Steal Odin’s dopamine. Keep Janus’s teeth.
11.4 Re-exports (Sovereign Index Pattern)
Section titled “11.4 Re-exports (Sovereign Index Pattern)”Sovereign Index files re-export sub-module symbols for consumers:
// identity.jan — Sovereign Indexpub use identity.typespub use identity.resolverpub use identity.registry
// With alias (for name conflicts)pub use gateway.jsonrpc_handler as jsonrpcpub use gateway.sbi_handler as sbi11.5 What NOT to Use
Section titled “11.5 What NOT to Use”// ❌ WRONG — @import is Zig plumbing, not Janus syntaxconst types = @import("../identity/types.jan");
// ✅ CORRECT — use is Janus-native module loadinguse identity.types
// ❌ WRONG — const assignment from useconst types = use identity.types;
// ✅ CORRECT — use binds the namespace directlyuse identity.types// Access as: types.DID, types.parse_did(), etc.12. SBI (Sovereign Binary Interface)
Section titled “12. SBI (Sovereign Binary Interface)”Zero-encoding binary serialization. Wire bytes = memory bytes on same architecture.
12.1 API
Section titled “12.1 API”use std.sbi
// Encodevar buf: [§size_of(Sensor) + 24]u8 = undefinedconst 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) }12.2 Wire Format
Section titled “12.2 Wire Format”[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 bytes13. Structured Concurrency (:service Profile)
Section titled “13. Structured Concurrency (:service Profile)”13.1 Scheduler and Nursery
Section titled “13.1 Scheduler and Nursery”pub const Scheduler = std.Scheduler;pub const Nursery = std.Nursery;pub const Budget = std.Budget;pub const SpawnOpts = std.SpawnOpts;
// Initialize runtimevar 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)13.2 Fiber Stack Sizes
Section titled “13.2 Fiber Stack Sizes”| Profile | Default Stack |
|---|---|
:core | 64 KB |
:service | 256 KB |
:sovereign | 512 KB |
Override via SpawnOpts.stack_size.
13.3 Cancellation
Section titled “13.3 Cancellation”Nurseries support cooperative cancellation via CancelToken. Cancellation propagates transitively through nested nurseries.
13.4 std.sync: Parker and Mutex[T] (SPEC-057-ter)
Section titled “13.4 std.sync: Parker and Mutex[T] (SPEC-057-ter)”std.sync provides the v0.1 blocking substrate for :service: a permit-model parking primitive and a generic futex-style mutex.
use std.sync.parker as parkeruse std.sync.mutex as mtxParker
Section titled “Parker”Parker has the same lost-wakeup-resistant contract as Java LockSupport, Rust parking_lot, and the Go runtime parking layer:
parker_unpark(&p)deposits one permit. Depositing while a permit already exists is a no-op.parker_park(&p)consumes an existing permit and returns immediately, or blocks until one is deposited.parker_park_timeout(&p, ns)consumes a permit before the deadline or returnsParkResult.TimedOut.
pub enum ParkResult { Unparked, TimedOut }pub struct Parker { ... }
pub func parker_new() -> Parkerpub func parker_park(edit self: *Parker) -> voidpub func parker_park_timeout(edit self: *Parker, ns: u64) -> ParkResultpub func parker_unpark(edit self: *Parker) -> voidTarget status: v0.1 dispatches to std.sync.parker_linux only. -Dsync-target=nexusos is rejected by build.zig until the NexusOS backend lands.
Mutex[T] protects a Janus value with Drepper Take-3 futex semantics: uncontended lock acquisition uses an atomic compare-and-swap, contended acquisition parks through Parker, and release unparks one waiter when the waiter bit is present.
pub enum LockError { WouldBlock, Cancelled }
pub struct Mutex[T] { ... }pub struct MutexGuard[T] { ... }
pub enum LockResult[T] { Ok(MutexGuard[T]), Err(LockError),}
pub func mutex_new[T](initial: T) -> Mutex[T]pub func mutex_lock[T](edit self: *Mutex[T]) -> MutexGuard[T]pub func mutex_try_lock[T](edit self: *Mutex[T]) -> LockResult[T]pub func mutex_release[T](edit guard: *MutexGuard[T]) -> voidpub func guard_value_of[T](edit guard: *MutexGuard[T]) -> *Tuse std.sync.mutex as mtx
func bump() -> u32 do var mu = mtx.mutex_new[u32](41) var guard = mtx.mutex_lock(&mu) let value = mtx.guard_value_of(&guard) value.* = value.* + 1 mtx.mutex_release(&guard) return 42endCaller contracts for v0.1:
- After
mutex_release(&guard)returns, using that guard again is undefined behavior. - A
Mutex[T]address must remain stable while tasks may park on it. Heap-allocate or stack-pin by call-site discipline until Pin / move-on-drop support lands.
Status: ./scripts/zb test-mutex-smoke and ./scripts/zb test-parker-roundtrip lock the single-threaded smoke surface. Multi-threaded proof, adaptive spin-then-park, poisoning, and type-system-enforced pinning are deferred.
14. Common Patterns
Section titled “14. Common Patterns”14.1 Allocator Threading
Section titled “14.1 Allocator Threading”Nearly every allocation-using function takes an explicit allocator:
func encode(allocator: std.mem.Allocator, value: anytype) ![]u8 do var out: std.ArrayListUnmanaged(u8) = .empty; errdefer out.deinit(allocator); try encodeValue(allocator, &out, value); return out.toOwnedSlice(allocator);end14.2 ArrayList Linear Scan
Section titled “14.2 ArrayList Linear Scan”For small collections, ArrayList with linear scan replaces HashMap:
var list = std.ArrayListUnmanaged(Entry).empty;defer list.deinit(allocator);try list.append(allocator, entry);
// Searchfor list.items do |item| if std.mem.eql(u8, item.name, target) do return item; endend14.3 Test Fixtures
Section titled “14.3 Test Fixtures”test "round-trip" { var tmp = testing.tmpDir(.{}); defer tmp.cleanup();
// ... use tmp.dir for file operations}14.4 Struct Initialization
Section titled “14.4 Struct Initialization”Per Haiku Block Discipline (§2, Law 2a): Struct literals MUST begin with a type path. There are no anonymous struct literals in Janus.
// Valid — type head required:let rt = GrafRuntime { scheduler: sched, allocator: allocator }
let frame = RequestFrame { version: PROTOCOL_VERSION, msg_type: MsgType.submit, payload_len: 1024, seq: 42,}
// Dotted type paths are valid:let hash = std.crypto.Hash { algorithm: .blake3 }
// Explicit leniency — accept defaults for unlisted fieldslet cfg = Config { host: "prod.example.com", ..defaults }
// ILLEGAL — no anonymous struct literals:// let x = { field: value }
// 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 — OKlet s2 = Server { host: "prod", ..defaults } // explicit leniency — OKlet s3 = Server { host: "prod", port: 443 } // ERROR: missing 'tls'14.5 Optional Handling
Section titled “14.5 Optional Handling”// Unwrap with RFC-019 pattern-binding `with`with .Some(val) <- optional_value do use(val);end
// Null coalescingconst result = maybe_value ?? default_value;
// Optional chainingconst name = user?.profile?.name;
// catch null (error to optional)const val = fallible() catch null;15. Operators
Section titled “15. Operators”15.1 Arithmetic
Section titled “15.1 Arithmetic”+, -, *, /, %
15.2 Comparison
Section titled “15.2 Comparison”==, !=, <, <=, >, >=
15.3 Logical
Section titled “15.3 Logical”and, or, not (NOT &&, ||, !)
15.4 Bitwise
Section titled “15.4 Bitwise”& (AND), | (OR), ^ (XOR), ~ (NOT), << (left shift), >> (right shift)
15.5 Special
Section titled “15.5 Special”| Operator | Purpose |
|---|---|
? | Error propagation (suffix) |
?? | Null coalescing |
?. | Optional chaining |
|> | Pipeline |
.. | Inclusive range |
..< | Exclusive range |
16. Naming Conventions
Section titled “16. Naming Conventions”| Context | Convention | Example |
|---|---|---|
| Modules, Structs, Enums | PascalCase | TreeEntry, ObjectKind |
| Variables, Functions | snake_case | read_ref, payload_len |
| Constants | UPPER_SNAKE_CASE | CID_LEN, MAX_OBJECTS |
| Local immutable values | snake_case | user_count, entry_count |
| Type parameters | Single uppercase | T, N |
| Comptime builtins | §snake_case | §size_of, §type_info |
| Private fields/padding | _prefix | _pad, _pad2 |
| Capability tokens (SPEC-080 §9.6.4) | <promise>Cap (PascalCase type) / <promise>_cap (snake_case binding) | WpathCap / wpath_cap, InetCap / inet_cap |
| Sovereign facade variants (SPEC-080 §9.5) | <base_fn>_sovereign accepting matching cap as first arg | write_text / write_text_sovereign(WpathCap, ...), exec_sovereign(ExecCap, ...) |
17. Profile Summary
Section titled “17. Profile Summary”:core — Available
Section titled “:core — Available”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.
:service — Available (adds to :core)
Section titled “:service — Available (adds to :core)”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, and std.sync Parker / Mutex[T].
:cluster — Draft (adds to :service)
Section titled “:cluster — Draft (adds to :service)”All of :service plus:
Actors and Grains:
actor Name(msg: T) do ... end— node-pinned actor with typed mailboxgrain Name(id: Id, msg: T) do ... end— virtual identity with owned state; activation is actor-shapedmessage Name { ... }— sealed algebraic message protocolspawn Name(args) with { ... }— spawn with optionsspawn Name(args) on { ... }— cluster-aware placementsend ref, msg— send message to actor/grainreceive do ... end— blocking message receive with pattern matchingreceive ... after N => ...— receive with timeout
Supervision:
supervisor Name, strategy: .one_for_one do ... end— restart strategychild Type, args: [...], restart: .permanent— supervised child
Memory Sovereignty:
alloc[Local.Exclusive](value)— owned memory, serialized on migratealloc[Session.Replicated](value)— async-replicated to cluster peersalloc[Session.Consistent](value)— sync-replicated via consensusalloc[Volatile.Ephemeral](value)— dropped on migrate, reconstructed
Capability Gating:
requires CapX, CapY— function requires capabilities from caller (between return type anddo)@requires(cap: [...])— grain declares required hardware/software capabilities@mailbox(capacity: N, overflow: .drop_oldest)— mailbox policy@replicate(scope: .wing)— replication scope annotation@persist(via: GrainStoreBytes)— bind grain-owned state to the v1 local persistence substrate@lifecycle(activation: .lazy, deactivation: .idle_timeout(ms))— declare activation/passivation policy
Syscall-Surface Pledging (see §19 / SPEC-080):
pledge { stdio, rpath, inet }— block-form pledge declares OS syscall surfacepledge { ... }on agrain— committed pledge (OS-enforced at spawn, placement-constraining)pledge { ... }on atransition— effective pledge (narrower; per-transition compile-time proof)unveil { path : { .read } }— filesystem whitelist; migratable grains usecap::...handles- Sovereign stdlib facades:
func_name_sovereign(cap, ...)— typed-capability-gated variants
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)
Not Yet Available
Section titled “Not Yet Available”: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)”18.1 Overview
Section titled “18.1 Overview”The :cluster profile provides distributed systems primitives: typed actors, grains with virtual identity and owned state, supervision trees, and capability-gated placement. Every construct desugars to :service primitives. No hidden runtime.
18.2 Typed Message Protocols
Section titled “18.2 Typed Message Protocols”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.
18.3 The Reply[T] Channel
Section titled “18.3 The Reply[T] Channel”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.
18.4 Actors
Section titled “18.4 Actors”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 endend18.5 Grains
Section titled “18.5 Grains”Grains are virtual identities with owned state. They activate as typed actors when work arrives, but the identity and its state outlive any one activation. Persistence is the first v1 capability; migration and placement are later runtime behavior under the same source contract.
@persist(via: GrainStoreBytes)@lifecycle(activation: .lazy, deactivation: .idle_timeout(300_000))@requires(cap: [.network])grain StorageService(id: StorageId, msg: StorageMsg) do var state: StorageState = StorageState.empty() var index = alloc[Session.Replicated](StorageIndex.new()) var write_cache = alloc[Volatile.Ephemeral](WriteCache.new(capacity: 4096))
on_activate(stored: StorageState) -> StorageState do return stored end
on_deactivate(state: StorageState) -> StorageState do return state end
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) endendNormative shortcut: grain Name(msg: T) remains valid for runtime-assigned identity, but namespace lookup and durable ownership use grain Name(id: Id, msg: T). A runtime must enforce exactly one active activation per durable identity.
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 CapFsReaddo return ctx.fs.read("config.toml")end
func sync_backup(ctx: *Context) !void requires CapFsRead, CapFsWrite, CapNetHttpdo // ...endConjunction semantics: requires CapFsRead, CapFsWrite means caller must grant both.
18.7 Supervision Trees
Section titled “18.7 Supervision Trees”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: .temporaryendRestart strategies: .one_for_one, .one_for_all, .rest_for_one
Restart policies: .permanent (always), .transient (abnormal only), .temporary (never)
18.8 Symbols (Atoms)
Section titled “18.8 Symbols (Atoms)”Lightweight interned strings for tags and status codes:
:ok, :error, :timeout, :normal, :shutdownDesugars to Symbol.intern("string") — GC-managed, safe unlike BEAM atoms.
18.9 Memory Tags
Section titled “18.9 Memory Tags”| Tag | Migration | Replication |
|---|---|---|
Local.Exclusive | Serialized + moved | Never |
Session.Replicated | Async to peers | Continuous |
Session.Consistent | Sync via consensus | Raft/PBFT |
Volatile.Ephemeral | Dropped | Never |
18.10 Honest Desugaring
Section titled “18.10 Honest Desugaring”:cluster Surface | Desugars to |
|---|---|
message Foo { ... } | enum Foo { ... } + Serialize check |
actor Name(msg: T) do ... end | Message loop + typed mailbox |
grain Name(id: Id, msg: T) do ... end | Virtual identity + actor activation + owned-state + Serialize |
@persist(via: Store) | Grain-owned state binding to runtime persistence substrate |
@lifecycle(...) | Activation/passivation policy metadata plus hook validation |
func f() !T requires CapX do ... end | Capability gate on call graph |
supervisor Name ... end | supervisors.start_link(ctx, Spec) |
receive do ... end | actors.receive().match(...) |
Reply[T].new() | channel.oneshot[T]() |
alloc[Tag](v) | actors.alloc(v, ReplicationPolicy { tag: ... }) |
:symbol | Symbol.intern("symbol") |
pledge { P } on grain | Committed-pledge metadata + placement constraint + virtual-pledge gate (§19.4) |
pledge { P } on transition | Effective-pledge verification; effect set ⊆ P proven at compile time |
unveil { path : { perms } } | Backend-dispatched Landlock / OpenBSD unveil / NexusOS VFS narrowing |
send ref, msg under pledge | Typed-mailbox admission check: message handler pledge ⊆ target committed pledge |
19. Pledge & Unveil — OS-Surface Promises (SPEC-080)
Section titled “19. Pledge & Unveil — OS-Surface Promises (SPEC-080)”“A pledge is not a wish. It is a contract the compiler enforces before the binary exists, and the kernel enforces after it runs.”
Normative source: SPEC-080 v0.1.2. This section is a quick reference; when in doubt, read the spec.
19.1 The Two Dimensions of pledge
Section titled “19.1 The Two Dimensions of pledge”Janus overloads the pledge keyword along two orthogonal axes, disambiguated by the token immediately after pledge:
| Form | Grammar trigger | Enforcement | Purpose |
|---|---|---|---|
pledge <ident>[(args)] | identifier | Compile-time only | Internal behaviour: pledge no_alloc, pledge stack(4096), pledge wcet(10_000 cycles) (SPEC-P-SOVEREIGN §4) |
pledge { promise, ... } | { brace | Compile-time proof + runtime kernel/supervisor enforcement | OS syscall surface: pledge { stdio, rpath, inet } (SPEC-080) |
Both forms MAY co-exist on the same function. They compose independently.
19.2 The pledge { ... } Block Form
Section titled “19.2 The pledge { ... } Block Form”pub func serve_http(listener: TcpListener) do pledge { stdio, inet } // compiler proves: body performs no syscalls outside { stdio, inet } // runtime: pledge(2) on OpenBSD, seccomp-bpf on Linux, capability narrow on NexusOSend- Accepted lists: comma-separated (
{ stdio, rpath, inet }) or whitespace-separated ({ stdio rpath inet }, OpenBSD-style). - Empty block (
pledge { }) – maximally restrictive; function may only compute, return, or_exit. - Narrowing-only rule – a function may narrow its module’s pledge but never widen (
PU-E003). - Transitive verification – every transitively-called function is checked against the caller’s pledge set.
- Core promises:
stdio,rpath,wpath,cpath,tmppath,inet,unix,dns,getpw,proc,exec,fattr,flock,tty,chown,id,unveil. execis a full-bypass promise. It exists for fidelity with the underlying OS surface, but operators should treat it as dangerous rather than routine (PU-W004).- Extension promises live in
Promise_ext(ext::audio,ext::video,ext::bpf,ext::dpath, …). Platform-gated; see SPEC-080 §4.2.2. ext::errorrequires explicit build-manifest opt-in; forbidden in release builds (PU-E006).
19.3 The unveil { ... } Block Form
Section titled “19.3 The unveil { ... } Block Form”pub func serve_static(req: Request) do pledge { stdio, rpath, inet } unveil { "/var/www/public" : { .read }, "/etc/ssl/certs" : { .read }, "/tmp/upload" : { .write, .create }, } // ... body ...end- Permissions:
.read,.write,.execute,.create– enum-set braces, symmetric with the pledge promise set. - Commit semantics – whitelist is committed at function entry, irrevocable for process lifetime (OpenBSD semantics).
- Path validation – literal paths canonicalized at compile time;
.., null bytes, and conflicting duplicates rejected (PU-E005). - Migratable grains MUST use capability-handle unveil – paths do not survive migration. Use
cap::graf_blob(...),cap::skv_bucket(...),cap::did_artefact(...)instead of string literals (PU-E007otherwise).
19.4 Pledge in :cluster (Grain Integration)
Section titled “19.4 Pledge in :cluster (Grain Integration)”A grain declares a committed pledge at definition scope; each transition MAY declare a narrower effective pledge:
grain ChatSession do pledge { stdio, inet } -- committed: what the OS enforces at spawn
in ReceiveMsg(m: Message) in SendMsg(m: Message)
transition handle_receive(m) do pledge { stdio } -- effective: stricter; effect set ⊆ { stdio } persist(m) end
transition handle_send(m) do pledge { stdio, inet } connect(server) |> send(m) endendGrain-level pledge influences six layers (SPEC-080 §6.5):
- Placement – hosts without the required promise support are rejected as placement targets.
- Migration – target-host pre-handshake verifies pledge support; failure is a placement error, not a runtime panic.
- Message admission – the typed mailbox refuses messages whose handlers require promises outside committed (
PU-E009). - Transition reachability – transitions exceeding committed pledge are unreachable by construction.
- Supervision lattice – child pledge SHALL ⊆ parent pledge; privilege narrows monotonically (
PU-E010). - Virtual-pledge runtime gate – when many grains share a process, the compiler synthesizes SPEC-030 user-defined-effect handler gates enforcing per-grain pledge in-language.
19.5 The _sovereign Suffix Convention
Section titled “19.5 The _sovereign Suffix Convention”Stdlib facades that wrap syscall-producing operations ship two variants at the same call-target:
| Variant | Signature | Use |
|---|---|---|
| Portable | write_text(path, data) | Ambient authority; no capability ceremony |
| Sovereign | write_text_sovereign(wpath_cap, path, data) | Typed-capability-gated; the wpath_cap is a compile-time proof object, zero runtime cost |
- Both variants compile to identical machine code on the hot path.
- The
sys/*layer stays token-free; capabilities are a facade-level concept. - Reference implementation:
std.net.http.http_get_sovereign(predates SPEC-080).
19.6 Capability-Token Provenance
Section titled “19.6 Capability-Token Provenance”Capability tokens (WpathCap, RpathCap, InetCap, …) are obtainable through exactly two mechanisms:
Canonical: a pledge { ... } block auto-materializes the matching token into lexical scope. No user ceremony.
pub func write_config(path: []const u8, data: []const u8) !void do pledge { wpath } -- `wpath_cap: WpathCap` is in scope; no explicit construction needed return std.os.fs.write_text_sovereign(wpath_cap, path, data)endHazard-flagged: std.os.caps.unsafe_forge_<name>_cap() fabricates a token outside a pledge block. Requires ⚠ sigil + one-line justification. Emits PU-W006 at every call site, unconditionally. Intended for migration code only.
There is no third mechanism. No trait impls, no ambient inference, no derive macros.
19.7 Diagnostic Code Prefix
Section titled “19.7 Diagnostic Code Prefix”Pledge/unveil diagnostics use the PU- prefix. E is an error, W is a warning, P is a placement failure, R is runtime, F is a fuzzer-discovered drift. Full table in SPEC-080 §8.3.
20. Anti-Patterns — Decomposition Doctrine
Section titled “20. Anti-Patterns — Decomposition Doctrine”These are the mistakes Janus is designed to prevent. Each is named here so the linter can catch it and the developer can name it.
20.1 The Domain-Model Trap
Section titled “20.1 The Domain-Model Trap”Symptoms: Trait hierarchies with more than two levels; impl blocks that mirror a human ontology (trucks are subclasses of vehicles, therefore Truck extends Vehicle).
The problem: Compile-time hierarchies that mirror the human-perceivable domain model are almost always wrong. The domain model describes what the user experiences. The code’s organization should answer to the machine’s leverage points.
The rule: Operations are added more often than entity types. Code organized around verbs scales; code organized around nouns calcifies.
// ANTI-PATTERN — domain-model traptrait Vehicle { fn drive(self) }trait LandVehicle { fn drive(self) } // Already covered by Vehiclestruct Truck { ... }
// BETTER — operation-orientedstruct Position { x: f64, y: f64 }struct Velocity { dx: f64, dy: f64 }struct Mass { kg: f64 }
system physics_step(entities: Query<(Position, Velocity, Mass)>, dt: f64) do // canonical ECS iteration — no trait hierarchy neededendHeuristic: If your trait hierarchy has more than two levels, you are probably mirroring a human ontology instead of a code structure.
20.2 Trait Dispatch in Hot Loops
Section titled “20.2 Trait Dispatch in Hot Loops”Symptoms: for entity in entities { entity.update(dt) } inside a tight for loop or hot while.
The problem: Virtual dispatch through a trait method adds a branch misprediction and an indirect call per entity. At millions of entities, this is the primary bottleneck.
The rule: See SPEC-082 Decomposition Doctrine §4. Move hot loops to component array iteration. Reserve trait dispatch for configuration, policy objects, and non-performant paths.
20.3 Encapsulation in Compute
Section titled “20.3 Encapsulation in Compute”Symptoms: Struct with private fields and getter/setter methods, used as the unit of iteration in a tight loop.
The problem: Getters and setters prevent the compiler from placing struct fields in contiguous arrays. Cache locality collapses.
The rule: Encapsulation has a placement problem, not a quantity problem. Draw the encapsulation boundary where the leverage lives. For compute-heavy code, this means component arrays with public fields and explicit mutation.
// ANTI-PATTERN — encapsulated entity in hot loopstruct Entity { pos_x: f64, // private with accessor pos_y: f64,}for entity in entities do entity.set_pos(entity.get_x() + dx, entity.get_y() + dy) // indirect callsend
// CANONICAL — component arrays (SPEC-082 §5)for i in 0..count do pos_x[i] += vel_x[i] * dt pos_y[i] += vel_y[i] * dtend20.4 Premature Actor-ization
Section titled “20.4 Premature Actor-ization”Symptoms: Every struct becomes an actor or grain. Messages for every small operation. Supervision trees for tasks that do not need restart semantics.
The problem: Actors carry overhead: mailbox discipline, message serialization, supervision latency. The floor cost exists regardless of entity count.
The rule: Actors are for state that must survive messages, restart, or distribution. Use them when physics enforces encapsulation. Use components and systems when the problem is data transformation inside a single execution context. See SPEC-082 §3 for the decision heuristic.
20.5 Verb Growth Without Surname
Section titled “20.5 Verb Growth Without Surname”Symptoms: Adding a new operation requires adding an impl to every existing type in the hierarchy.
The problem: The classic expression problem. Every new operation creates N changes across N types.
The rule: Design for verb growth, not noun growth. A Query<(Position, Velocity)> { } system can be added once and automatically applies to every entity that has those components.
20.6 Cross-Linking
Section titled “20.6 Cross-Linking”- SPEC-082 — Decomposition Doctrine (full treatment of §20.1 through §20.5)
- SPEC-021 —
:clusterprofile (actor decomposition) - SPEC-050 —
:computeprofile (ECS decomposition) std.ecs— Reference ECS architecture
21. Algebraic Effects (SPEC-090)
Section titled “21. Algebraic Effects (SPEC-090)”Anchor: SPEC-090 (Algebraic Effects & Profile-Ambient Handlers). Phase C LANDED 2026-05-10 — effect graph + visitor API + SPEC-080 syscall_class consumer +
effect_set_of(f)query. Doctrine: doctrines/three-leg-tripod.md. Effects are the third leg of the static-analysis tripod: what does this function do to the world?
21.1 The !{Effects} Return-Type Modifier
Section titled “21.1 The !{Effects} Return-Type Modifier”A function that performs side effects declares them as a brace-delimited row immediately after the return type, optionally after an error union:
// Pure function — no row.func add(a: i64, b: i64) -> i64 do return a + bend
// Single effect.func read_config(view path: [u8]) -> Config !{IO} do let bytes = IO.stdout_write("loading config") return parse(bytes)end
// Error union + effect row. Order is fixed: -> T !ErrorType !{Effects}func fetch_blob(view cid: [32]u8, allocator: std.mem.Allocator) -> [u8] !FetchError !{IO, Net, Alloc} do // ...end
// Multi-effect — order within the braces is not significant.func game_turn(view player: [u8]) -> i64 !{IO, Random} do let roll = Random.next_int(6) + 1 IO.stdout_write(player ++ " rolled: " ++ str(roll)) return rollendThe !{...} syntax is parallel to !Error. The braces follow Law 2 ({ } for data structures) — the effect row is a set of effect tags, not control flow. Duplicates within a row are deduplicated and emit warning W2516 (silent dedup would violate Revealed Complexity).
21.2 The Two Purity Surfaces
Section titled “21.2 The Two Purity Surfaces”A function with !{} and a function omitting the row are not semantically identical:
// Pure-by-default — adding `IO.stdout_write(...)` later just changes the inferred row.func process(view bytes: [u8]) -> Result do ... end
// Explicit purity assertion — adding any effect later produces E2509.pub func canonical_hash(view bytes: [u8]) -> [32]u8 !{} do return blake3(bytes)end!{} is for the contract surface (public API, audit-critical functions, pinned-purity utilities); the no-row form is for everyday composition. Two ways to say “pure” exist precisely because the intent differs — Revealed Complexity, not sugar.
21.3 handle ... with E do ... end Handlers
Section titled “21.3 handle ... with E do ... end Handlers”Effect handlers use do...end block form (Law 1, control flow), not the brace-delimited handler list of SPEC-030. Each with EffectName clause handles exactly one effect (multi-effect-per-clause is E2510):
handle do handled_expressionend with EffectName do func operation_name(params) -> ReturnType do handler_body endendThe handler body is a collection of func definitions — one per operation declared in the effect. Each handler is imperative code, not a data table. handle do ... end is an expression — it produces the value of the handled expression and composes inside let, function arguments, etc.
21.4 Profile-Ambient Handler Tables
Section titled “21.4 Profile-Ambient Handler Tables”Every profile carries a profile-ambient handler table — the set of effects whose handlers the runtime installs by default at program entry:
| Profile | Ambient effects |
|---|---|
:core / :s0 | (empty — pure-by-default) |
:service | IO, Alloc, Net, Time |
:cluster | (empty — Send/Recv/Spawn pending SPEC-021 ratification) |
:compute | (empty — GPU/NPU pending SPEC-017-P device dispatch) |
:sovereign | (empty — Boot/MMU/Hardware pending SPEC-080 §4.4) |
An effect that reaches main and is not in the active profile’s ambient table must be wrapped in an explicit handle...with block; otherwise E2511 fires.
Doctrine: profiles scale proof obligations and ambient powers, not truth. Effect syntax is uniform across every profile. The
:scriptlesson from SPEC-085 §2.8.2 is reapplied — language-surface constructs must not lie. Profile differences live in ambient tables and capability availability, not in language gating.
21.5 Narrow-Form Polymorphism ?E
Section titled “21.5 Narrow-Form Polymorphism ?E”Higher-order parameters propagate their callee’s effects through the wrapper:
func map[T, U](view xs: [T], view f: func(T) -> U !{?E}) -> [U] !{?E} do var result: [U] = [] for x in xs do result.append(f(x)) end return resultendWhen called with f typed func(T) -> U !{IO}, the call site’s ?E resolves to IO. When f is pure, ?E resolves to the empty row.
?E is not unrestricted row polymorphism. The compiler rejects:
?Emixed with concrete effects in the same row → E2512- Row arithmetic on
?E(?E - X,?E & X) → E2514 - Multiple
?Esharing a name → E2515 (use distinct names —?Ef,?Eg) ?Edeclared in a row without a higher-order parameter to source it → E2513- Bounded row variable / row-polymorphism-with-constraints → E2517
?E is restricted to call-site monomorphization through higher-order parameters. Every ?E MUST be sourced by a function-typed parameter’s effect row.
21.6 Effect Declarations
Section titled “21.6 Effect Declarations”effect IO { func stdin_read(edit buf: [u8]) -> usize func stdout_write(view bytes: [u8]) -> void}
effect Alloc { func alloc(size: usize, align: usize) -> *u8 func free(take ptr: *u8) -> void}- Effect names MUST be PascalCase; operation names MUST be snake_case.
- Default implementations are FORBIDDEN — effects are pure contracts.
- Effect operation signatures use SPEC-085 intent qualifiers natively.
- Effect names and module names share a namespace; collision produces E2508.
21.7 Affine-Ledger Semantics Across the Effect-Call Boundary
Section titled “21.7 Affine-Ledger Semantics Across the Effect-Call Boundary”When an effect operation declares a take parameter (or operates on an iso T / ~T argument), the caller’s affine binding is consumed at the effect-call site, not at the handler-body invocation site. The handler is opaque to the caller:
effect Alloc { func free(take ptr: *u8) -> void}
func release(take ptr: *u8) -> void !{Alloc} do Alloc.free(ptr) // ptr's ledger transitions to Dead at THIS call. // ptr.read() // ❌ E2602: use of consumed binding (SPEC-029)end21.8 Effects vs Capabilities
Section titled “21.8 Effects vs Capabilities”- Effects answer: what can this computation do?
- Capabilities answer: who authorized this realization of that effect?
A function may declare !{FileRead} without holding CapFsRead. A handler that realizes FileRead against the actual filesystem requires CapFsRead. A test handler reading from an in-memory table requires no capability.
This separation is the doctrinal core: what a function does (effects) is decoupled from how those effects are fulfilled (capabilities). The same function tests purely and runs in production with real authority.
Effects name the debt. Capabilities authorize payment. Totality proves the ledger closes.
21.9 SPEC-030 Migration
Section titled “21.9 SPEC-030 Migration”The SPEC-030 with E clause syntax is deprecated. During the migration window (v2026.5.X → v2026.7.X), the parser accepts the legacy form and rewrites to !{E} with W2509:
// Old (SPEC-030, deprecated, surfaces W2509):func legacy() -> i32 with IO do ... end
// New (SPEC-090, canonical):func canonical() -> i32 !{IO} do ... endAfter v2026.7.X the with clause produces a parse error.
21.10 Diagnostics
Section titled “21.10 Diagnostics”| Code | Meaning |
|---|---|
| E2502 | Incomplete handler — missing operation (substrate-blocked v1.0) |
| E2509 | Function declares !{} (purity assertion) but body introduces effects |
| E2510 | Multi-effect single with clause (with E1, E2 do is forbidden) |
| E2511 | Effect reaches main but is not in the active profile’s ambient handler table |
| E2512 | ?E mixed with concrete effect in the same row |
| E2513 | ?E polymorphism variable has no source |
| E2514 | Row arithmetic on polymorphism variable |
| E2515 | Multiple ?E polymorphism variables sharing a name |
| E2516 | Handler body declares its own row using row arithmetic |
| E2517 | Bounded row variable / row-polymorphism-with-constraints |
| W2509 | SPEC-030 with clause — deprecated; rewrite as !{...} row |
| W2516 | Duplicate effect in row — likely a copy-paste error |
21.11 Compilation Strategy
Section titled “21.11 Compilation Strategy”Effects compile via static dispatch through monomorphization. No continuations, no coroutines, no CPS transform, no runtime effect stack. The handler is resolved at compile time and inlined as a direct function call.
After lowering, effect-typed code is indistinguishable from hand-written direct-call code. Effect rows are fully erased — they exist only for the compile-time proof.
This zero-runtime-cost stance is doctrinal. Janus does not adopt OCaml-style runtime continuation machinery; that path is closed without an explicit future SPEC reopening it.
21.12 Effect-Set Queries
Section titled “21.12 Effect-Set Queries”The compiler exposes two query entry points over the per-function effect data ([EFF:7.1.4]):
declaredEffectRowOf(f)— the contract surface. The!{...}row the user wrote. What callers see; what auditors read.inferredEffectSetOf(f)— the deep view. Post-handle-elimination callee-propagated set unioned with profile-gate-satisfied visitor contributions. What tooling consults when peering past the contract.
Conflating them would lie to either the user (declared row hides handler-relocated effects) or the auditor (inferred set leaks pre-handle internal state into public APIs).
21.13 Visitor API for Stdlib Extensions
Section titled “21.13 Visitor API for Stdlib Extensions”The compiler exposes a stable visitor API at effect_graph.zig::EffectVisitorRegistry for compiler-internal consumers. The first concrete consumer is SPEC-080’s @syscall_class aggregator:
@syscall_class(stdio)pub func write(fd: Fd, buf: []const u8) -> Result[usize, Errno] do ... end
// Multi-class disjunction (§8.1.2) — readlink is rpath + stdio-adjacent.@syscall_class(rpath, stdio)pub func readlink(path: []const u8, buf: []u8) -> Result[usize, Errno] do ... endUnder :sovereign (the visitor’s profile gate), every call to a @syscall_class-annotated function contributes Syscall.<class> to the calling function’s inferred effect set. Under lower profiles the visitor is silently inactive — no diagnostic, no overhead.
The Janus-side surface (std.compiler.effects) declares the contract types and the registration entry point; Janus-bodied visitor execution lands with Phase D’s comptime evaluator integration. Compiler-internal consumers register Zig-side via EffectVisitorRegistry.register().
21.14 Further Reading
Section titled “21.14 Further Reading”- Public docs: reference/effects, tutorials/effects, release notes v2026.5.10
- Doctrine: three-leg-tripod.md, MUTABLE-VALUE-SEMANTICS.md, capability-gating.md
- Spec: SPEC-090 (Algebraic Effects & Profile-Ambient Handlers), SPEC-030 (User-Defined Effects & Typed Handlers — predecessor; semantic foundation retained verbatim), SPEC-080 (Sovereign Pledge & Unveil — canonical first consumer)
“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.”