Equality Model
Equality Model
Section titled “Equality Model”Janus distinguishes four kinds of equality that most languages conflate. Each axis answers a different question; knowing one answer doesn’t determine the others.
Three deserve syntax. One deserves fear.
The four axes
Section titled “The four axes”| Axis | Question | Syntax | Lowers to | Profile |
|---|---|---|---|---|
| 1 | Same meaning? | == | Eq.eq(a, b) (trait) | default |
| 2 | Same object? | is | identity primitive | default |
| 3 | Same sequence? | eql | std.mem.eql(a, b) | default |
| 4 | Same bits? | (no syntax) | std.mem.raw.eql(...) | :sovereign |
Quick reference
Section titled “Quick reference”let a: []const u8 = "hello"let b: []const u8 = "hello"let same = a
a == b // semantic equality (depends on Eq impl; reference for slices pre-Phase-4)a eql b // sequence equality — TRUE (same bytes)a is b // identity — FALSE (distinct bindings)a is same // identity — TRUE (same binding)For primitives:
let x: i32 = 42let y: i32 = 42
x == y // ✅ truex eql y // ❌ E1247 — primitives are not sequencesx is y // ❌ E1249 — primitives have no identityOperators in detail
Section titled “Operators in detail”== — Semantic equality (Axis 1)
Section titled “== — Semantic equality (Axis 1)”The equality you want 95% of the time. For user types, the Eq trait
implementation defines what “equal” means. For primitives, value
equality. For collections, typically element-wise semantic equality.
Complexity: implementation-defined. O(1) for primitives, O(n) for collections, custom for user types.
Available in all profiles.
is — Identity (Axis 2)
Section titled “is — Identity (Axis 2)”True if a and b are the same object — same allocation, same handle,
same binding. Always O(1).
Mirrors Python’s is, Java’s == for objects, Rust’s Arc::ptr_eq.
Identity gate (E1249): Primitives (integers, bools, floats) are
rejected. They don’t have meaningful identity — two bindings of the
same integer are bitwise-identical, so is would duplicate ==.
let f1 = File.open("/tmp/x")let f2 = File.open("/tmp/x")f1 is f2 // FALSE — distinct file handlesf1 == f2 // depends on File's Eq impl (probably path equality)Available in all profiles.
eql — Sequence equality (Axis 3)
Section titled “eql — Sequence equality (Axis 3)”Element-wise equality for sequence types (slices, arrays, byte strings).
Always O(n) with early exits. Lowers to std.mem.eql(a, b).
For []u8 the elements are bytes; for []u32 they’re words. The
function compares element-by-element, NOT raw representation bytes
(padding, capacity, vtables are not involved because slices have none).
Sequence gate (E1247): Non-sequence types are rejected. Vec[u8]
is a struct that contains a sequence; asking eql on it directly
would compare struct bytes (pointer + len + capacity), which is
meaningless. The compiler tells you to use == (semantic) or
extract the underlying slice (v.data eql v.data).
let v1 = Vec[u8].init(allocator, [_]u8{1, 2, 3})let v2 = Vec[u8].init(allocator, [_]u8{1, 2, 3})
v1 eql v2 // ❌ E1247 — Vec is not a sequence typev1 == v2 // ✅ semantic Vec equality (via Eq trait)v1.data eql v2.data // ✅ byte equality on underlying slicesAvailable in all profiles.
std.mem.raw.eql — Representation equality (Axis 4)
Section titled “std.mem.raw.eql — Representation equality (Axis 4)”Raw byte comparison. Includes everything: padding bytes, NaN payloads, vtables, capacity metadata. Almost never what people mean.
// In :sovereign profile only:use std.mem.raw as raw
if raw.eql(&buf_a[0], &buf_b[0], 64) do ... endNo operator syntax. Forcing the programmer to write the full
function name and provide explicit raw pointers plus a byte count
makes the danger visible. The programmer who writes raw.eql(...)
has to mean it.
Profile-gated to :sovereign. Use cases:
- Forensic / debugging tools that inspect raw memory
- Hash table key comparison where keys have been canonicalized
- Cryptographic tag verification (with explicit context)
- Constant-time comparison primitives (built on top with care)
When all four axes agree: WireSafe types
Section titled “When all four axes agree: WireSafe types”The four axes answer different questions for most types — that’s the
whole point. But for a struct declared @layout(wire) and proven
WireSafe (see Representation & Layout),
all four axes agree:
- The Canonical Representation Principle guarantees exactly one valid byte representation per value (no padding ambiguity, no inactive-variant scratch, no NaN-payload divergence).
- Therefore byte equality ⟺ semantic equality for
T: WireSafe. - The compiler MAY lower
==onWireSafetypes directly tomemcmp.
Outside WireSafe, the four-axis distinction still matters and the
rules above still apply. WireSafe collapses the axes by making
representation itself canonical.
Why four axes
Section titled “Why four axes”Languages that conflate these axes produce bugs:
-
Padding bytes: Two
Foo { x: u8, y: u64 }structs with identical fields may have different bytes due to uninitialized padding. Semantic equality (==) is true; representation equality is false. Both are correct — they answer different questions. -
NaN payloads:
f32.nan == f32.nanisfalseper IEEE 754. But two NaN values may have identical bit patterns. Semantic equality and representation equality diverge. This is the point of having four axes, not a bug in the model. -
Vec identity vs equality: Two
Vec[u8]allocated separately with the same contents are semantically equal (==is true) but not identical (isis false — different allocations). A third question — do their underlying byte slices match? — is yet another operation (v.data eql v.data).
Migration
Section titled “Migration”The four-axis model is additive. Existing code that doesn’t use
eql or is continues to work unchanged.
| If you wrote… | Consider… |
|---|---|
if slice_a == slice_b expecting byte equality | if slice_a eql slice_b |
if ptr_a == ptr_b for pointer identity | if ptr_a is ptr_b |
| 16-line byte-by-byte comparison loop | if a eql b |
std.mem.raw.compare_bytes(...) == 0 | if a eql b |
Diagnostics
Section titled “Diagnostics”| Code | Meaning | Fix |
|---|---|---|
E1247 | eql on non-sequence operands | Use ==, or extract the underlying slice |
E1249 | is on primitive value types | Use == |