Ruta graveolens  ·  notes from a language experiment  ·  cultivated since 2025

Comparison Operators

A comparison operator takes two operands of the same type and produces a bool (core calculus docs/formal/01-core-calculus.md §5.8, rule (Eq) for ==/!=; §6.4, rules (D-Eq) and the ordering compares): the value produced is true exactly when the two operand values stand in the operator's relation, and false otherwise.

Equality Operators

Equality operators work on integers, floating-point values, booleans, strings, the unit type, and the aggregate types: structs, arrays, and enums. On floating-point operands they are the IEEE comparisons of 3.12:27, not the total equivalence the other leaf types give.

OperatorNameDescription
==EqualTrue if operands are equal
!=Not equalTrue if operands are not equal

Two strings are equal if they have the same length and identical byte content. This holds wherever a string is reached, not only as a whole operand: a string that is a struct field, an array element, or an enum payload field, at any depth, is compared by this rule and not by the representation of its header (a string is a leaf of the structural recursion in rule 4.3:3b, so rule 4.3:3e never applies to the pointer inside one).

Two unit values are always equal.

Equality on the aggregate types is structural: two aggregate values are equal if and only if they have the same type and their components — determined recursively by this rule down to scalar leaves — are equal (core calculus docs/formal/01-core-calculus.md §6.4, the structural-equality relation ≈ of rule (D-Eq)). Specifically, two struct values are equal if and only if they have the same struct type and all corresponding fields are equal.

Every leaf the recursion reaches is compared by its own type's equality — never by a byte-wise comparison of the aggregate's storage. An integer, bool, or unit leaf compares by value; a string leaf by content (4.3:3); a raw-pointer leaf by address (4.3:3e); and a floating-point leaf by the IEEE comparison of 3.12:27, so -0.0 and +0.0 leaves are equal although their bits differ, and a NaN leaf is not equal to itself. For every leaf type 4.3:2 also admits as a whole operand, that leaf relation is the one a top-level comparison of the type would use, so a component's equality never disagrees with that component's own ==. The raw pointer is the one leaf kind with no top-level comparison of its own — it is not among 4.3:2's operand types — which is why 4.3:3e defines it only in field position. A floating-point leaf is the only leaf whose equality is not reflexive: an aggregate that reaches a NaN at any depth is therefore not equal to itself (3.12:29, 4.3:3g).

Two array values are equal if and only if they have the same element type and length and their elements are equal index-by-index. (Two array types of different lengths are distinct types and cannot be compared; see rule 4.3:10.)

Two enum values are equal if and only if they have the same enum type, are the same variant, and — for a variant carrying a payload — their payload fields are equal field-by-field. Two values of different variants are never equal.

A raw-pointer leaf (a ptr const T or ptr mut T field or element reached while comparing an aggregate) compares by address: two raw pointers are equal if and only if they hold the same address. The pointees are not examined.

Equality borrows its operands: evaluating a == b or a != b reads both operands without consuming them. When an operand is a named place it is read through a comparison-scoped shared loan rather than moved (core calculus docs/formal/01-core-calculus.md §4.1, §5.4, and the operand side condition of rule (Eq) in §5.8; dynamically §6.3). An affine or linear value may therefore be compared without discharging its move obligation, and both operands remain usable afterward.

Equality is structural by default. Trait-based refinement of equality — opting a type out of comparison, or giving it a user-defined equality (a PartialEq-style mechanism) — is deferred until traits exist (RUE-246).

Structural equality is symmetric and transitive everywhere, and reflexive everywhere except through a NaN: a NaN is the one value unequal to itself (3.12:27), and a value that reaches one at any leaf inherits that (3.12:29). It is therefore a partial equivalence rather than a total one — but the exception is exactly the NaN leaf. Equality is total on every type with no floating-point leaf, which is every type Rue had before f32 and f64, and on every value of a float-carrying type that holds no NaN.

That exception is deliberate. The alternative — comparing an aggregate's float leaves by their bits, or by @total_cmp (3.12:32), so that structural equality stayed a total equivalence — was rejected because it would make a field's equality disagree with that field's own ==: Sample { value: nan } would be equal to itself while the nan == nan inside it stayed false. Applying IEEE at the leaf (4.3:3b) keeps the two the same operator, and matches what a derived structural equality does over IEEE floats in other languages. The partiality is the concrete motivation for the eventual PartialEq/Eq split; until that lands, @total_cmp is the total order available for sorting and hashing, and it is the tool to reach for when a container needs reflexivity.

fn main() -> i32 {
    let a = 1 == 1;    // true
    let b = 1 != 2;    // true
    let c = true == false;  // false (bool equality)
    let d = "hello" == "hello";  // true (string equality)
    let e = () == ();  // true (unit equality)
    if a && b && !c && d && e { 1 } else { 0 }
}
struct Point { x: i32, y: i32 }

fn main() -> i32 {
    let p1 = Point { x: 1, y: 2 };
    let p2 = Point { x: 1, y: 2 };
    let p3 = Point { x: 1, y: 3 };
    if p1 == p2 && p1 != p3 { 1 } else { 0 }
}

Arrays compare element-by-element; nested aggregates recurse. Comparing does not consume the operands, so a and b remain usable afterward.

fn main() -> i32 {
    let a = [1, 2, 3];
    let b = [1, 2, 3];
    let equal = a == b;      // borrows a and b
    if equal && a[0] == b[0] { 1 } else { 0 }
}

A float leaf carries IEEE equality into the aggregate around it (4.3:3b). An aggregate holding a NaN is not equal to itself; every other aggregate is, so the only reflexivity failure is the one the NaN introduces.

struct Sample { value: f64 }

fn id(v: f64) -> f64 { v }

fn main() -> i32 {
    let zero: f64 = id(0.0);
    let nan = zero / zero;
    let a = Sample { value: nan };
    let b = Sample { value: nan };
    let c = Sample { value: 1.5 };

    @dbg(a == a);   // false: the NaN leaf is not equal to itself
    @dbg(a == b);   // false: two NaN leaves are unordered, not equal
    @dbg(a == c);   // false: ordinary inequality, no NaN rule needed
    @dbg(a != c);   // true
    @dbg(c == c);   // true: no NaN, so equality is reflexive here
    0
}

Ordering Operators

Ordering operators work on integers and on floating-point values. On integers they compare the two operands by their integer values, respecting the signedness of the shared operand type — a signed type orders negatives below non-negatives, an unsigned type orders by magnitude (core calculus docs/formal/01-core-calculus.md §6.4: scalars compare by their integer value, respecting signedness). On floating-point operands they are the IEEE partial comparisons of 3.12:27.

OperatorNameDescription
<Less thanTrue if left < right
>Greater thanTrue if left > right
<=Less or equalTrue if left <= right
>=Greater or equalTrue if left >= right

Ordering operators on boolean, string, unit, or aggregate (struct, array, or enum) values are a compile-time error. Implementations MUST reject such programs. (Floating-point operands are ordered, by 4.3:5.)

fn main() -> i32 {
    let a = 1 < 2;     // true
    let b = 5 >= 5;    // true
    if a && b { 1 } else { 0 }
}

Precedence

Comparison operators have lower precedence than arithmetic, shift, and bitwise operators, and higher precedence than the logical operators && and ||. (The complete precedence ladder, which matches Rust's, is given in rule 4.3a:13.)

fn main() -> i32 {
    if 1 + 2 == 3 { 1 } else { 0 }  // 1 (comparison after arithmetic)
}

Type Checking

Both operands of a comparison MUST have the same type.

When one operand has a known type, the other is inferred to have the same type.

Associativity

Comparison operators cannot be chained. Expressions like a < b < c or a == b == c are compile-time errors. The restriction is syntactic: it applies when a comparison expression is directly an operand of another comparison. Explicit parentheses break a chain — (a < b) == c is an ordinary equality whose left operand is a parenthesized boolean expression, and is legal whenever its operand types are (a parenthesized boolean operand of an ordered comparison such as (a < b) < c is instead rejected by the ordinary operand typing rules).

To compare multiple values, use logical operators:

fn main() -> i32 {
    let a = 1;
    let b = 2;
    let c = 3;
    if a < b && b < c { 1 } else { 0 }  // correct way to chain comparisons
}