v2026.6.24: Four-Axis Equality Model
v2026.6.24: Four-Axis Equality Model
Section titled “v2026.6.24: Four-Axis Equality Model”Janus now distinguishes four kinds of equality that most languages conflate.
Three get keyword operator syntax; the fourth lives behind a :sovereign
profile gate because it is too sharp for everyday use.
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 |
The first three are everyday tools. They get keyword operators (continuing
Janus’s and/or/not keyword-operator doctrine). Axis 4 is a sharp
tool — it lives in std.mem.raw next to reinterpret, pointer_from,
and compare_bytes, gated under :sovereign.
Why four axes
Section titled “Why four axes”Languages that conflate these axes force programmers to guess:
- Python’s
==/iscovers two of the four axes but misses sequence and representation equality. - Java’s
==means identity for objects but value for primitives — the worst of all worlds. - JS’s
==/===distinguishes coercive vs strict value equality, which is not the same distinction as semantic vs representation. - Rust’s
==/PartialEqis semantic only; identity isArc::ptr_eqand representation equality is viamemcmp.
Janus separates all four. Each axis answers a different question; knowing one answer doesn’t determine the others.
let nan_a: f32 = f32.nanlet nan_b: f32 = f32.nan
nan_a == nan_b // false (IEEE 754)nan_a eql nan_b // true (4-byte sequence compare — if via reinterpret)nan_a is nan_b // false (distinct bindings)All three answers are correct. They answer different questions.
Operator reference
Section titled “Operator reference”== — Semantic equality
Section titled “== — Semantic equality”a == bSame semantics as before. For user-defined types, the type’s Eq
implementation defines what “equal” means. For primitives, it’s the
obvious value equality. No behavior change for existing code.
is — Identity
Section titled “is — Identity”a is bSame-object comparison. True if a and b are the same allocation,
handle, or binding. O(1). Mirrors Python’s is, Java’s == for
objects, Rust’s Arc::ptr_eq.
Identity gate (E1249): is on primitive value types (integers,
bools, floats) is rejected at compile time. Primitives don’t have
identity — two bindings of the same integer are bitwise-identical,
so is would just duplicate ==. Use == for value equality.
let x: i32 = 42let y: i32 = 42if x is y do ... end // ❌ E1249 — primitives have no identityif x == y do ... end // ✅eql — Sequence equality
Section titled “eql — Sequence equality”a eql bElement-wise equality for sequence types (slices, arrays, byte strings).
O(n) with early exits. Lowers to std.mem.eql(a, b).
Sequence gate (E1247): eql on non-sequence types is rejected.
Structs like Vec[u8] are NOT language-level sequences — use ==
for semantic equality or extract the underlying slice
(v.data eql v.data) for byte comparison.
let v1 = Vec[u8].init(allocator, [_]u8{1, 2, 3})let v2 = Vec[u8].init(allocator, [_]u8{1, 2, 3})
if v1 eql v2 do ... end // ❌ E1247 — Vec is not a sequenceif v1 == v2 do ... end // ✅ semantic Vec equalityif v1.data eql v2.data do ... end // ✅ byte equality on the slicesstd.mem.eql — Library form
Section titled “std.mem.eql — Library form”use std.mem as mem
if mem.eql(slice_a, slice_b) do ... endThe library function form of the eql operator. Useful when you need
to pass sequence equality as a function argument, or when working in
contexts where the operator form doesn’t parse cleanly.
std.mem.raw.eql — Representation equality (gated)
Section titled “std.mem.raw.eql — Representation equality (gated)”// In :sovereign profile only:use std.mem.raw as raw
if raw.eql(&buf_a[0], &buf_b[0], 64) do ... endRaw byte comparison including padding bytes, NaN payloads, vtables, and capacity metadata. Almost never what people mean. Reserved for forensics, hash table internals, cryptographic tag verification, and similar sharp-tool cases.
Profile-gated to :sovereign. Cannot be called from :core or
:service profiles. Has no operator form — calling it requires
writing the full function name and providing explicit raw pointers
plus a byte count.
Migrating existing code
Section titled “Migrating existing code”The four-axis model is additive. Existing code that doesn’t use
eql or is continues to work unchanged. The only migration
concern is for programmers who want to adopt the new operators:
| Old pattern | New pattern |
|---|---|
| 16-line byte-by-byte fingerprint comparison | if fp eql expected do ... end |
slice_a == slice_b (reference equality) | if slice_a is slice_b do ... end |
std.mem.raw.compare_bytes(...) == 0 | if a eql b do ... end |
Verification
Section titled “Verification”./scripts/zb test-mem-eql-smoke # library function./scripts/zb test-eql-operator-smoke # operator positive cases./scripts/zb test-eql-operator-type-reject # E1247 on primitives./scripts/zb test-eql-operator-struct-reject # E1247 on structs./scripts/zb test-is-operator-smoke # operator positive cases./scripts/zb test-is-operator-primitive-reject # E1249 on primitivesDoctrine
Section titled “Doctrine”The philosophical anchor for this model lives at
doctrines/equality-model.md
in the compiler repo. Quotable lines:
Three deserve syntax. One deserves fear.
Padding, NaN payloads, and vtables are not bugs. They are the defining features of the model.
A compile error is the doctrine’s kindness.
Implementation commits
Section titled “Implementation commits”Seven commits on feature/voxis, awaiting merge to unstable:
| Commit | Phase |
|---|---|
f1f6159e | std.mem.eql library function |
5f2051fb | eql operator (parser + lowering) |
571dd97a | is operator (parser + lowering) |
f65a3f81 | E1247 SequenceEq trait gate (primitives) |
c7e6a220 | E1247 tightened (structs) |
b2c9ed0f | E1249 Identity trait gate |
Soft-keyword pattern (matches Janus’s existing orelse precedent):
the tokenizer keeps eql and is as .identifier tokens. The parser
detects them by text in the binary-operator position and assigns
equality precedence. The lowerer detects them by text and dispatches
to the appropriate lowering path. Existing identifiers named eql
(including the std.mem.eql library function) continue to parse
correctly.