Equality and Ordering Contracts¶
Accepted
Accepted for V1 equality and ordering contract names, laws, operator lowering, and primitive numeric conformance shape.
This document records the standard contract families used by equality and ordering.
Contract Families¶
Equality syntax is contract-backed, like indexing syntax. == is available when the left operand's type implements PartialEq(RightType):
fn PartialEq(comptime Other: Type) Type {
require_concrete_sized(Other, "PartialEq operand type must be concrete and sized")
return contract {
fn eq(self: *const Self, other: *const Other) bool
fn neq(self: *const Self, other: *const Other) bool {
return !self.eq(other)
}
}
}
Same-type equality is spelled explicitly:
impl Point as PartialEq(Point) {
}
For concrete values, a == b lowers to the statically resolved PartialEq(RightType).eq operation. a != b lowers to PartialEq(RightType).neq. The neq operation has a default implementation that negates eq, but implementations may override it when there is a strong reason.
This accepts the verbosity of PartialEq(Point) for same-type equality to keep the contract model uniform. Contract parameters are explicit; Catalyst does not add a default-to-Self shorthand for same-type contract applications.
Equality operations take pointers to avoid unnecessary copies and to remain compatible with future non-copy/resource types. Implementations and compilers should still optimize small scalar equality to efficient value comparisons where possible.
Operator lowering may materialize const temporaries for rvalue operands so pointer-based operations can be called:
foo() == bar()
is conceptually checked like:
const lhs_tmp = foo()
const rhs_tmp = bar()
lhs_tmp.eq(&rhs_tmp)
These temporaries are the semantic model. Optimized machine code should not introduce unnecessary storage or overhead when values can stay in registers.
This deliberately hides a function call behind familiar operator syntax. The ergonomics match indexing and ordinary equality syntax in other languages, but diagnostics and tooling should make the lowering visible enough that users can understand which conformance operation is being called.
Primitive numeric equality and ordering conformances are declared in the prelude as ordinary generic impls over the primitive numeric type factories documented in Built-In Types. Their operation bodies call compiler primitive comparison intrinsics so static operator lowering remains aggressively optimizable.
For dynamic contract objects, == does not mean raw fat-pointer descriptor identity. It may lower to a dynamic eq vtable call only when both operands have a compatible dyn-safe equality-capable contract type. Otherwise equality is not available.
PartialEq(SelfType) means same-type equality operator support and may be non-reflexive. For example, floating-point NaN makes f32 equality non-reflexive. The separate marker contract Eq represents a true equivalence relation suitable for APIs such as hash maps:
fn Eq(comptime T: Type) => contract(PartialEq(T)) {
// semantic law: reflexive, symmetric, transitive
}
The compiler cannot prove the Eq laws. Implementing Eq is a trusted semantic promise.
Hashing is separate from equality and belongs to std, not the prelude:
const Hash = contract {
fn hash(self: *const Self, state: *Hasher) void
}
Hash does not directly depend on Eq. APIs that need hash-key semantics should require both, usually from std collection APIs:
K: Eq(K) & Hash
The semantic law for the combination is that values equal under Eq must produce equal hash results for the same hasher. The compiler cannot prove this law.
See std.hash for the hashing placement boundary.
Ordering operators are separate from equality. <, <=, >, and >= are backed by a partial-order comparison contract. Ord is reserved for APIs that require a true total order, such as sorting and ordered maps.
const Ordering = enum {
less,
equal,
greater,
}
fn PartialOrd(comptime Other: Type) Type {
require_concrete_sized(Other, "PartialOrd operand type must be concrete and sized")
return contract {
fn partial_cmp(self: *const Self, other: *const Other) ?Ordering
}
}
fn Ord(comptime Other: Type) Type {
require_concrete_sized(Other, "Ord operand type must be concrete and sized")
return contract(PartialOrd(Other)) {
fn cmp(self: *const Self, other: *const Other) Ordering
fn partial_cmp(self: *const Self, other: *const Other) ?Ordering {
return self.cmp(other)
}
}
}
Ordering is an ordinary prelude enum, not a compiler-intrinsic enum. The compiler knows comparison operators lower to the PartialOrd operations; it does not need special layout knowledge for Ordering beyond normal enum semantics and the prelude contract definition.
For concrete values, a < b, a <= b, a > b, and a >= b lower to the statically resolved PartialOrd(RightType).partial_cmp operation on the left operand's type, followed by fixed operator-specific interpretation of the optional Ordering result. Like PartialEq(Other), PartialOrd(Other) and Ord(Other) are cross-type: an implementation may compare Self against a different right-hand operand type when that relationship is explicit and meaningful. Same-type comparison is spelled PartialOrd(SelfType) or Ord(SelfType).
partial_cmp is the only operation in PartialOrd(Other). The boolean operators are not contract methods and cannot be overridden independently. cmp is the required operation for Ord(Other). Generic algorithms that require total ordering can rely on cmp as the canonical complete comparison operation.
Comparison operator availability is left-operand driven. a < b requires the left operand type A to implement PartialOrd(B); it does not require B to implement PartialOrd(A). If a library wants both operand orders, it must provide both implementations explicitly. The compiler does not invent bidirectional comparison relationships.
A PartialOrd(Other) implementation may report operands as unordered by returning null from partial_cmp. All four comparison operators return false for unordered operands: <, <=, >, and >=. This is intentional standards-aligned behavior for primitive floats and similar partially ordered domains. An Ord(Other) implementation is a trusted semantic promise of total ordering for the participating operand types: comparisons are consistent, antisymmetric, transitive, and total over the values the implementation accepts.
Ord(Other) does not depend on PartialEq(Other). cmp(...) == .equal means the operands are equivalent for this ordering relation, not necessarily that == returns true. APIs that need both ordering and equality must require both contracts explicitly and document the relationship they expect.
Primitive floats implement PartialOrd(f32) / PartialOrd(f64) with ordinary IEEE numeric comparison behavior: ordered comparisons involving NaN return false, and partial_cmp returns null for unordered operands. Primitive floats do not need to implement Ord for the comparison operators to exist. A future total-float-order API or wrapper may use IEEE 754 totalOrder, but that is separate from ordinary float operators.
PartialOrd(Other) and Ord(Other) follow the ordinary dyn-safety rules. When Other is a concrete runtime type and the active surface is otherwise dyn-safe, dyn PartialOrd(Other) and dyn Ord(Other) are valid:
fn less_than_zero(x: *const dyn PartialOrd(i32)) bool {
return x < 0
}
This compares an erased left operand against a concrete right operand type. It does not create a general "compare two arbitrary erased values" operation; that requires an explicitly represented compatible dynamic surface.