Skip to content

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.

AxisQuestionSyntaxLowers toProfile
1Same meaning?==Eq.eq(a, b) (trait)default
2Same object?isidentity primitivedefault
3Same sequence?eqlstd.mem.eql(a, b)default
4Same 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.

Languages that conflate these axes force programmers to guess:

  • Python’s ==/is covers 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 ==/PartialEq is semantic only; identity is Arc::ptr_eq and representation equality is via memcmp.

Janus separates all four. Each axis answers a different question; knowing one answer doesn’t determine the others.

let nan_a: f32 = f32.nan
let 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.

a == b

Same 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.

a is b

Same-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 = 42
let y: i32 = 42
if x is y do ... end // ❌ E1249 — primitives have no identity
if x == y do ... end // ✅
a eql b

Element-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 sequence
if v1 == v2 do ... end // ✅ semantic Vec equality
if v1.data eql v2.data do ... end // ✅ byte equality on the slices
use std.mem as mem
if mem.eql(slice_a, slice_b) do ... end

The 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 ... end

Raw 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.

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 patternNew pattern
16-line byte-by-byte fingerprint comparisonif fp eql expected do ... end
slice_a == slice_b (reference equality)if slice_a is slice_b do ... end
std.mem.raw.compare_bytes(...) == 0if a eql b do ... end
Terminal window
./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 primitives

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.

Seven commits on feature/voxis, awaiting merge to unstable:

CommitPhase
f1f6159estd.mem.eql library function
5f2051fbeql operator (parser + lowering)
571dd97ais operator (parser + lowering)
f65a3f81E1247 SequenceEq trait gate (primitives)
c7e6a220E1247 tightened (structs)
b2c9ed0fE1249 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.