Skip to content

Governed Metaprogramming

Janus is the first language where compile-time evaluation is governed by the same capability system as runtime. The profile ladder doesn’t just restrict which APIs you can call — it restricts what the compiler itself is allowed to do on your behalf.

That is Mechanism over Policy applied to the compiler.

No other systems language has attempted a unified comptime design with this structure:

First-class compile-time evaluation with an actual ComptimeEvaluator backed by resource limits. This is not “the compiler happens to fold constants.” It is an explicit, bounded interpreter that runs your code at compile time and tells you when you’ve exceeded its budget.

const TABLE = comptime do
var t: [256]u8 = undefined
inline for 0..256 |i| do
t[i] = compute_entry(i)
end
t
end

The evaluator enforces hard limits: 1,000,000 steps, 256 MB memory, 1,000 recursion depth. Exceed any limit and you get a diagnostic — not a frozen terminal.

$size_of, $align_of, $type_info. The $ sigil screams this is the compiler talking in a way that is visually distinct from runtime code. No ambiguity. No Zig-style @ that collides with 47 other things.

if comptime $is_integral(T) do
optimized_int_path()
else
generic_path()
end

The set is deliberately small. Most operations that Zig hides behind @builtins are regular library calls in Janus.

:core gets the basics — checked arithmetic, $size_of, $align_of. :service unlocks introspection via $type_info, $fields, $has_field. :sovereign gets comptime allocation.

The profile ladder constrains the evaluator, not just the language surface. A :core program cannot accidentally depend on type introspection. A :service program cannot allocate memory at compile time. Capability escalation is explicit, visible, and auditable.

That third layer is what makes this sovereign. Nobody else has it.


DimensionGoRustZigJanus
Comptime modelNone. go generate is a shell script with lipstick.const fn + proc macros (two separate systems)comptime keyword; same language at compile timecomptime do...end blocks with bounded evaluator
Type introspectionreflect package (runtime only)Proc macros operate on token streams, not types@typeInfo returns comptime struct$type_info via profile-gated capability
Code generationgo generate (external tooling)Proc macros (separate crate, separate compilation model)Inline comptime; inline for / inline whileinline for / inline switch; same syntax, same semantics
Resource limitsN/AConst eval has recursion limits (bolted on)None. Comptime can loop forever.Explicit resource budgets with diagnostics
Checked arithmeticRuntime panic on overflow (signed), silent wrap (unsigned)Panic in debug, wrap in releaseExplicit @addWithOverflow builtinFirst-class checked arithmetic with overflow detection
Profile gatingN/AN/AN/A:core / :service / :sovereign capability ladder
Sigil clarityN/A#[attribute] / macro!() / const fn (three syntaxes)@builtin (47+ builtins behind same sigil)$meta sigil; visually distinct, deliberately small set

Go doesn’t have metaprogramming. Full stop. go generate is “please run this Python script before you compile.” The language designers philosophically rejected compile-time evaluation. If you need generic code, you copy-paste or use interface{} and pray. Go 1.18 generics helped, but there is still zero comptime. You cannot compute a lookup table at compile time. You cannot unroll a loop. You cannot introspect a type’s fields without reflect at runtime.

Go chose simplicity over power. Janus chose structured power over false simplicity.

Rust has the most capable metaprogramming of the three — but it is fractured into three incompatible subsystems:

  • const fn evaluates pure functions at compile time but can’t do anything interesting. No trait method calls, limited control flow until recently.
  • Proc macros are powerful but operate on token streams. They don’t see types, they don’t see the semantic graph, they compile as separate crates with separate build steps.
  • #[derive] is a third mechanism with its own rules.

The result: you need syn, quote, proc-macro2 as dependencies just to write a derive macro. The compile-time ecosystem is a dependency swamp.

Janus gives you one airlock to the compiler’s semantic graph. You see types, effects, capabilities. Not token soup.

Zig — Beautiful Concept, Structural Problems

Section titled “Zig — Beautiful Concept, Structural Problems”

Zig is the closest competitor and the one we respect. Zig’s comptime is elegant in concept: the same language runs at both compile time and runtime. But it has structural problems:

47+ @builtins behind one sigil. @intCast, @ptrCast, @typeInfo, @This, @ctz, @popCount. The sigil carries no semantic weight; it just means “here be compiler magic.” Janus splits this into stdlib generics, $meta builtins for compiler introspection, and regular library calls for everything else. Most of Zig’s @builtins become ordinary function calls in Janus.

No resource limits. A Zig comptime block can allocate unbounded memory and loop forever. The compiler just hangs. Janus’s ComptimeEvaluator has explicit budgets. When you exceed them, you get a diagnostic — not a frozen terminal.

No capability gating. In Zig, comptime has full access to everything. In Janus, :core comptime can compute sizes and unroll loops. It cannot allocate or introspect. That requires :service or :sovereign. The profile ladder prevents accidental complexity escalation.


Zig gives you all of comptime and trusts you. Rust gives you three half-systems and hopes you pick the right one. Go gives you nothing and calls it a feature.

Janus gives you a graduated airlock with explicit resource limits, visual sigil distinction, and profile-gated capability escalation.

You can do metaprogramming now. But more importantly: you can do governed metaprogramming. And that is the thing none of them have.


See also: Comptime Reference — full builtin table, profile matrix, and code examples.