Janus vs Odin vs Zig
Janus vs Odin vs Zig
Section titled “Janus vs Odin vs Zig”Odin and Zig are two systems languages that have spent the last several years fighting over the same philosophical territory: who gets to be the “better C.” Their communities debate every month on podcasts, Discord, and forums. The debates are useful — but they’re about a battlefield Janus is not on.
This page is the honest comparison. Where Odin wins a round, we say so. Where Zig wins a round, we say so. Where Janus wins, we explain why — and where the other two are arguing over something we’ve already ratified, we point that out too.
1. The Three Battlefields
Section titled “1. The Three Battlefields”| Language | Battlefield | Core stance |
|---|---|---|
| Odin | Ergonomic systems programming | ”Pragmatism over purity. Fast compile, sane defaults, minimal surface.” |
| Zig | Explicit systems programming | ”Every operation visible. No hidden machinery. You pay only for what you use.” |
| Janus | Profile-stratified sovereign programming | ”Profiles gate capabilities. Ownership is structural. Sovereignty is the product.” |
Odin and Zig both target “better C” — the same user with the same use cases, debating ergonomics vs explicitness as tradeoffs on the same spectrum. Janus isn’t on that spectrum. Janus is on a different axis.
The thesis of this comparison:
What’s policy in Odin is a capability in Janus; what’s implicit in Odin and explicit in Zig is profile-gated in Janus.
Read that again. It’s the whole page.
2. The Three-Axis Analysis
Section titled “2. The Three-Axis Analysis”Axis 1 — Policy in Odin, Capability in Janus
Section titled “Axis 1 — Policy in Odin, Capability in Janus”Odin’s “context allocator.” Odin has a thread-local context struct that carries a default allocator. Most standard library procedures implicitly consume it. You can override the context, but the default path is “the ambient allocator handles it.”
This is ergonomic. It is also hidden state dressed as convenience. Every call to an Odin stdlib function is a potential silent allocation through an allocator you may not have named.
Zig rejects this. Every allocation in Zig takes an explicit Allocator parameter. No ambient anything.
Janus agrees with Zig on the principle, and goes further: allocator access is a capability, not a parameter passed around by convention. A :core function cannot touch the global allocator without explicitly declaring the capability. A :sovereign function that wants heap access must pledge it. The compiler refuses to compile code that reaches for memory it wasn’t granted.
Where this lands:
- Odin makes the default path ergonomic but hides the cost.
- Zig makes the default path explicit but leaves policy-by-convention in caller code.
- Janus makes the default path enforced at compile time. Policy violations are type errors.
Axis 2 — Implicit in Odin, Explicit in Zig, Profile-Gated in Janus
Section titled “Axis 2 — Implicit in Odin, Explicit in Zig, Profile-Gated in Janus”Error propagation. Odin’s or_return, or_else, or_break operators work on bool, enum, union, pointer, and named types. Propagation is uniform across the type system. Zig’s try / catch requires a distinguished error-union shape (E!T) — the error channel is syntactically marked but requires a specific type constructor.
Odin wins this round. Error propagation should work on any sum type the user cares to use — a bool, a result-ish enum, a variant union.
Janus’s commitment: !T propagation operators work on any type that can structurally represent failure, not just a distinguished error-union kind. Where SPEC-032 implied otherwise, it’s being audited.
Axis 3 — Explicit in Zig 0.16 (by retreat) = Already Ratified in Janus
Section titled “Axis 3 — Explicit in Zig 0.16 (by retreat) = Already Ratified in Janus”FFI discipline. Zig 0.16 literally removed @cImport from the language and moved it into the build system. The in-language form was unsound: positional, re-evaluating on every usage site, polluting scope unpredictably.
Janus’s graft c doctrine forbade this shape from day one. FFI lives at module scope. It is a declarative statement, not an expression. Zig 0.16’s “retreat” is Janus’s origin position, reached by Zig via experience rather than by design.
When a competitor language spends multiple major versions discovering what your language ratified in its first draft, you’re on the right axis.
3. What Janus Adopts From Zig 0.16
Section titled “3. What Janus Adopts From Zig 0.16”Zig 0.16 shipped several genuinely excellent ideas. Janus is open about importing them. Syntactic Honesty requires acknowledging good work where it appears.
IO-as-interface
Section titled “IO-as-interface”Zig 0.16 made std.Io a pluggable interface with implementations threaded, evented (green threads, M:N), io_uring, kqueue, dispatch, and failing (for testing). Functions take an Io parameter; callers choose the backend.
Janus position: Profiles and IO-backends are orthogonal, not rival. Profiles gate whether a function may touch IO. Backends decide how that IO is realized once permitted. The upcoming std.sync design will make the executor substrate swappable from day one — the same :cluster program will emit against threaded, io_uring, or a Nexus-native scheduler without source changes.
”Juicy main” destructuring
Section titled “”Juicy main” destructuring”Zig 0.16 ships a main signature that opt-in destructures init, allocator, io, env, args, preopens. Ask for what you need; omit the rest at zero overhead.
Janus position: This is perfect for :script. A script entry point like:
func main(.{ args, env, io }) do // script bodyendgives ergonomic single-file scripting without magic globals, without a runtime wrapper, and without breaking the profile ladder. :script’s entry-point syntax is being amended to match this shape.
Forbid returning address of local
Section titled “Forbid returning address of local”Zig 0.16 added a single-pass compile error for return &x where x is a stack local. Obvious safety improvement. Janus :core must catch the same pattern structurally via ownership analysis, not as a heuristic. If it doesn’t, that’s a soundness bug.
Cancellation as first-class
Section titled “Cancellation as first-class”Zig 0.16’s futures carry await and cancel. Nearly every IO op has error.Canceled in its error set. Cancellation propagates through group / batch abstractions.
Janus position: Nursery-scoped cancellation was always the design. Every IO-typed effect at :service and above surfaces Canceled deterministically. Actor message handlers at :cluster must be cancellable at the supervisor boundary. std.sync primitives must surface cancellation through explicit channels, not thread signals.
Named type-constructor builtins
Section titled “Named type-constructor builtins”Zig 0.16 replaced @Type(.{.Int = ...}) with @Int, @Enum, @Struct, @Union, @Pointer. Pure discoverability.
Janus position: If we ever need to construct types at compile time, constructors are named after the type kind, not bundled into a single sum-typed dispatcher. Compile-time type construction is rare in Janus (profiles + generics handle most use cases) but when needed, the discoverable form is correct.
Float ↔ int conversion ergonomics
Section titled “Float ↔ int conversion ergonomics”Zig 0.16 lets @floor, @ceil, @round, @trunc convert float → int directly. Deprecates @intFromFloat.
Janus position: std.conv already handles this via truncate[T](x). A future v1.1 ergonomic pass-through (std.math.round.to[T]) is additive, not doctrinal — it calls the right std.conv variant underneath. Mechanism preserved.
Test timeouts
Section titled “Test timeouts”Zig 0.16 adds --test-timeout. Five lines in a test harness, catches 90% of deadlock bugs. Janus’s std.test ships with timeout support from day one — not “later.”
Directory walk with selective recursion
Section titled “Directory walk with selective recursion”Zig 0.16’s Dir.walkSelectively avoids open/close syscalls on skipped subtrees. The default walker takes a predicate; the skip-tree decision is made without descending.
Janus position: When std.fs lands for :service, copy this shape exactly.
4. What Janus Refuses
Section titled “4. What Janus Refuses”Not every idea on the table is a good one. Some are actively worth marking as anti-patterns.
Odin’s context allocator
Section titled “Odin’s context allocator”Hidden state dressed as ergonomics. Violates Mechanism over Policy. Janus refuses to introduce any ambient allocator, thread-local or otherwise. Every allocation either flows through an explicit parameter or declares the capability that reaches for memory.
Zig’s pre-0.16 @cImport inside function bodies
Section titled “Zig’s pre-0.16 @cImport inside function bodies”Positional import chaos. Zig’s own migration path deprecated it. Janus graft declarations sit at module scope, period. Never embedded in expressions, never position-dependent, never re-evaluated.
Odin’s no-incremental-compilation stance
Section titled “Odin’s no-incremental-compilation stance”The stated Odin position: “A debug build takes 1.1 seconds, 52k lines, that’s fast.” This is the answer of someone who has never worked on a 500k-line codebase. Janus requires incremental compilation via ASTDB as a first-class goal. Zig 0.16 is now moving toward incremental; Odin is still arguing “whole-program rebuild is fine.” The argument does not survive contact with serious codebases.
”Sane defaults” as a marketing claim
Section titled “”Sane defaults” as a marketing claim”In Odin discussions, “sane defaults” appears three times in the first five minutes. Sane defaults are policy. Janus refuses to ship policy as a language feature. We give you mechanisms and profiles; your defaults are yours to configure. If someone ships a “Janus sane defaults” distribution later, that’s their opinionated package — not the language’s commitment.
5. Honest Round-by-Round Scorecard
Section titled “5. Honest Round-by-Round Scorecard”No language wins every round. Here’s the scorecard, neutral.
| Topic | Odin | Zig | Janus |
|---|---|---|---|
| Compile speed (small projects) | Excellent | Good | Good (once warm) |
| Compile speed (large projects) | Poor (no incremental) | Improving | Target: excellent via ASTDB |
| Minimal syntax surface | Excellent | Moderate | Moderate |
| Explicit allocation discipline | Weak (context allocator) | Excellent | Excellent (capability-gated) |
| FFI safety | Good (bindings) | Excellent (0.16) | Excellent (graft at module scope) |
| Error propagation on any type | Excellent (or_return) | Weak (error-union only) | Target: excellent (in audit) |
| IO abstraction | Weak | Excellent (0.16) | Target: excellent (std.sync) |
| Structured concurrency | Weak | Good | Excellent (nurseries, SPEC-021) |
| Profile-gated capabilities | None | None | Unique |
| Ownership in type system | None | None | Unique (CTRC ~T) |
| Comptime | Weak | Excellent | Excellent (SPEC-027) |
| Teaching-first profile | None | None | Unique (:core) |
| Sovereign supply chain | None | None | Unique (Hinge + Graf) |
Honest summary: Odin wins ergonomics and compile speed on small codebases. Zig wins explicitness and the most mature safety machinery currently shipping. Janus wins profiles, ownership, sovereignty — the axes that Odin and Zig are not competing on.
6. When to Choose What
Section titled “6. When to Choose What”Use Odin when:
- Fast iteration on a small systems-level codebase matters more than scale
- You want minimal syntax and don’t mind hidden default allocator
- You’re writing a game jam, a small CLI, or a systems-programming learning project
- Your team is comfortable with “sane defaults” as policy
Use Zig when:
- Every operation must be explicitly visible in the source
- You want the most mature allocator-everywhere discipline shipping today
- You’re targeting WASI, embedded, or anywhere the toolchain story is first-class
- You want zero hidden machinery and are willing to pay the keystroke tax
Use Janus when:
- Code will span multiple capability tiers (teaching → service → cluster → sovereign)
- You need profile-gated compilation: a
:coremodule that cannot accidentally drift into:service - Ownership must live in the type system, not in reviewer discipline (CTRC
~T) - You want a sovereign supply chain (Hinge package manager, Graf version control) end-to-end
- You’re building long-lived infrastructure where “we’ll rewrite later” is not an option
- The code will be read by humans and AI agents together — Syntactic Honesty is non-negotiable
7. The Debate Itself Is the Signal
Section titled “7. The Debate Itself Is the Signal”When two communities debate the same philosophical territory for years, a third path becomes legible. Odin says “less typing.” Zig says “no surprises.” A careful listener of both debates discovers — as one podcast guest did — that they actually want no surprises over less typing. That’s Syntactic Honesty by a different name.
The Odin/Zig debate is valuable because it resolves in public the tradeoff Janus has already committed on. Every time that debate flares, it clarifies what we chose and why. We don’t need to win the debate. We just need to keep shipping the language that made the debate unnecessary.
Further reading
Section titled “Further reading”- Janus Language Reference — the canonical doctrine
- Why Janus? — the shorter philosophical tour
- Governed Metaprogramming — the comptime comparison with Go, Rust, and Zig
- Polar & Signal Paradigm — where Janus diverges from the dominant ML/graphics data models