Skip to content

OQ-002 D2: Decimal Equality

Status: ACCEPTED Accepted: 2026-11-08 Decision: Kyllo's == on Decimal maps to General Decimal Arithmetic (GDA) compare (numeric-value comparison) for finite operands Category: Numeric Types and Semantics

1. Decision

D2 — Decimal equality. Kyllo's == on Decimal maps to General Decimal Arithmetic (GDA) compare (numeric-value comparison) for finite operands: two finite Decimal values are equal if and only if they have the same numerical value, regardless of representation (cohort). 1.50 == 1.5 is true; -0 == 0 is true; 1.50 != 1.5 is false. The ordering operators <, >, <=, >= use the same numerical comparison. Map<Decimal, …> keys and Set<Decimal> elements use this equality, with a hash consistent with it: a Decimal whose value equals an existing key or element counts as that key or element, regardless of cohort; inserting it follows whichever replacement-or-rejection operation the collections specification defines. No comparison or collection operation normalizes a stored Decimal value. GDA compare-total is not exposed as a language operation by this decision; whether a cohort-sensitive comparison exists is a separate, later decision. Comparison involving NaN or Infinity is not decided by D2: D3 decides whether those values can exist in Kyllo programs and, if so, how == and the ordering operators behave for them. Comparisons between Decimal and another type remain a compile error under the existing no-implicit-conversion rule. This decision does not decide literal format or literal typing (D4/D5), the conversion mechanism (D6), the canonical external form (D7), integer division/remainder (OQ-003), or the rounding context (OQ-004). The minimum commitments in §6 of this record are part of this decision.

This decision is one part of OQ-002 ("Exact numeric representation and Decimal conformance target"). It resolves D2, the second foundational question of OQ-002. OQ-002 itself remains Open until D3–D7 are resolved. D1 (the GDA conformance foundation) was accepted 2026-11-08; D2 builds on D1 and changes nothing about it.

2. The equality rule

Kyllo's == on Decimal is GDA compare (specification p. 27): "compare takes two operands and compares their values numerically." For finite operands, compare returns '0' exactly when the two values have the same numerical value, and '−1' or '1' otherwise. == is true exactly when compare returns '0'; != is its negation.

  • 1.50 == 1.5 is true: the two are different cohorts (coefficient 150, exponent −2; coefficient 15, exponent −1) of the same numerical value, and compare ignores representation.
  • -0 == 0 is true: GDA's compare returns '0' when the subtraction result is "zero or negative zero".
  • 1.50 != 1.5 is false. != is the negation of ==; the two are never separated.
  • Comparisons are exact and unrounded. compare is not subject to context precision: a difference beyond 34 digits still distinguishes two values. The fixed Kyllo context (D1) does not round operands before comparing them.

This rule resolves the pull in TECHSPEC-21 §6 between its two sentences. "Decimal comparisons must use their defined decimal values" is the operative rule: equality is based on defined decimal values. The display-similarity sentence — "The language must not treat two different numeric representations as equal merely because they appear similar when displayed" — forbids display-based equality as a basis; it does not forbid value-based equality of different representations. Under D2, 1.50 == 1.5 is true because the two have the same defined decimal value, not because they appear similar when displayed.

3. Ordering operators

The ordering operators <, >, <=, >= on Decimal use the same numerical comparison (GDA compare). They are unaffected by the equality choice: compare and compare-total order primarily by numerical value, and their only divergence is the tie-break between equal values, where the ordering operators are already false (1.50 < 1.5 is false and 1.50 > 1.5 is false under either mapping, because the values are equal). The ordering operators are exact and unrounded, like ==.

4. Map and Set semantics

Map<Decimal, …> keys and Set<Decimal> elements use the equality of §2, with a hash consistent with it: equal values hash equally. A Decimal whose value equals an existing key or element counts as that key or element, regardless of cohort. Concretely:

  • Inserting 1.5 into a map that already has 1.50 as a key is inserting an existing key.
  • A set containing 1.50 already contains 1.5; adding 1.5 does not add a second element.

This satisfies TECHSPEC-24 §8 ("Equality and hashing must remain consistent for map keys and set elements"). What is not decided here: whether inserting an existing key replaces the existing entry or is rejected (TECHSPEC-24 §4). D2 decides what counts as an existing key; the replacement-vs-rejection operation is the collections specification's question.

On the Python backend, this behavior is what the standard library already does: Python's == on Decimal is value equality, and Decimal.__hash__ is value-based (hash(Decimal('1.50')) == hash(Decimal('1.5'))). A Python backend gets correct Map/Set behavior from dict and set with no additional code. A cohort-equality backend would have needed a cohort-sensitive key wrapper everywhere a Decimal is used as a key.

5. No normalization

Comparing two Decimals, or using one as a Map key or Set element, does not change any stored representation. normalize/reduce is not invoked by ==, by the ordering operators, or by Map/Set use. A value's cohort is preserved exactly through comparison and collection operations. This is consistent with D1 (implementations must preserve coefficient and exponent exactly and must not normalize silently) and with TECHSPEC-39 §8 ("Equality must not silently perform conversions or normalization").

6. Minimum commitments

  1. == and != map to GDA compare for finite operands. Two finite Decimal values are equal if and only if they have the same numerical value, regardless of representation. 1.50 == 1.5 is true; -0 == 0 is true; 1.50 != 1.5 is false. != is the negation of ==. Comparisons are exact and unrounded: context precision does not affect them.
  2. The ordering operators use the same numerical comparison. <, >, <=, >= on Decimal are GDA compare (numerical value). They are unaffected by cohort: 1.50 < 1.5 and 1.50 > 1.5 are both false.
  3. Map/Set use == with a consistent hash. Map<Decimal, …> keys and Set<Decimal> elements are compared by the == equality of commitment 1, and their hash is consistent with it: equal values hash equally. A Decimal whose value equals an existing key or element counts as that key or element, regardless of cohort; inserting it follows whichever replacement-or-rejection operation the collections specification defines (that operation itself is not decided here).
  4. No normalization by comparison or collection use. Comparing two Decimals, or using one as a Map key or Set element, does not change any stored representation. normalize/reduce is not invoked by ==, by the ordering operators, or by Map/Set use. A value's cohort is preserved exactly through comparison and collection operations.
  5. compare-total is not exposed as a language operation. D2 does not create a cohort-sensitive comparison operation, operator, function, or method. Whether one exists is a separate, later decision (§7). The semantic reference (D1 report §2.8, category 2) continues to include compare-total as a defined operation that the language does not expose.
  6. NaN and Infinity are out of scope for D2. Comparison involving a NaN or Infinity operand is not decided by D2. D3 must define what ==, !=, and the ordering operators do when an operand is NaN or Infinity — including the consequence that GDA compare returns NaN for a NaN operand, which is not a Boolean, so == cannot be a bare compare call if NaN values can reach it. The backend must treat any comparison path that could receive a NaN operand as governed by D3, not by D2.
  7. Cross-type comparison stays a compile error. == between Decimal and another numeric type (or any other type) remains rejected under the existing no-implicit-conversion rule (TECHSPEC-21 §6; TECHSPEC-39 §7). The Python backend must not implement Kyllo's == by delegating to Python's cross-type == (Python's Decimal('1') == 1 is True); generated code must compare same-type Decimal operands only.

7. What D2 does not decide

  • D3 (failure outcomes, failure representation, special values): whether NaN and Infinity can be produced, stored, compared, or serialized; what == and the ordering operators do with a NaN operand; what Infinity == Infinity is; how special values interact with Map/Set keys. D2's statement is scoped to finite operands precisely so D3 can decide all of this without contradicting D2. D3 should be taken up next after D2.
  • D4/D5 (literal typing/format): what cohort a literal produces, whether a bare integer-looking literal can be a Decimal, what happens to a literal with more than 34 significant digits. Whatever cohorts literals produce, D2 makes them invisible to ==.
  • D6 (conversion mechanism): what cohort a conversion produces (e.g. what CONVERT<Decimal>(150) yields as a representation). A conversion's result cohort is D6's; its equality behavior follows D2 whatever cohort it has.
  • D7 (canonical external form): how cohorts serialize. D2 constrains D7 in one way: TECHSPEC-27 §7 requires that "where deterministic output is required, equivalent values must produce identical serialized representations." Under D2, 1.50 and 1.5 are equal values. If "equivalent" in §7 means "equal by the language's equality", then D7's canonical form must render equal values identically — i.e. it must be cohort-insensitive for equal values — or D7 must define "equivalent" differently and say so explicitly. Either reading is available to D7; what D2 removes is the option of leaving the question unasked.
  • Whether a cohort-sensitive comparison operation exists: deferred by commitment 5. When D5–D7 have said what cohorts exist to compare, a later decision can add one (purely additive; nothing breaks). Its form (function, method, operator) and name are part of that later decision.
  • compareTo (TECHSPEC-38 §5): the interface example's compareTo(other: SELF) -> Integer has no defined sign convention. The natural definition, once D2 is accepted, is that it follows the same numerical comparison as == and the ordering operators — but that is a recommendation for the decision that defines compareTo, not part of D2, because compareTo itself is example-only and its existence as a language feature is undecided.
  • Duplicate-key replacement vs. rejection (TECHSPEC-24 §4): D2 decides what counts as an existing Decimal key; the collections specification must still decide whether inserting one replaces the existing entry or is rejected.
  • Integer equality: no equality rule for Integer is specified anywhere in the sources. It should be stated explicitly (presumably exact value equality) alongside D2's integration, as a documentation completion, not as a new design decision — or, if Blake disagrees that it is exact, as its own small decision.
  • OQ-003 (integer division/remainder): no dependency. If Decimal % is defined, its results' equality follows D2 like any other Decimal values.
  • OQ-004 (rounding context): no dependency for equality — comparisons are exact and unrounded. OQ-004's explicit rounding operations will produce values whose equality follows D2.

8. Rationale

  1. It is what the specification already says, once the display-similarity sentence is read precisely. "Decimal comparisons must use their defined decimal values" (TECHSPEC-21 §6) is a direct statement of value comparison. D2 follows the spec's own words; the alternative would require reading the display-similarity sentence as overriding the defined-values sentence, which nothing in the text supports.
  2. It is what the core example requires. The Bank Account is the MVP's core demonstration and is built entirely on Decimal. Its postconditions and verification checks are written over values ("increase the balance by exactly the deposited amount"). D2 makes those statements true as written; cohort equality would make them cohort-dependent in ways the examples never discuss and no rule anticipates.
  3. It matches every programmer's expectation and every mainstream decimal system's default. Python's ==, GDA's compare, IEEE 754's compareQuiet, and Java's compareTo are all value-based. The one prominent cohort-sensitive equality (Java's BigDecimal.equals) is a documented trap, not a model to follow — Java's own documentation warns that equals is inconsistent with compareTo, a documented source of bugs.
  4. It is the cheapest to specify, implement, and maintain. One mapping sentence; Python delegates directly; no wrapper types; no cohort-awareness in any equality-adjacent rule.
  5. It keeps the hard questions separate. D2 does not decide NaN/Infinity (D3), literal format/typing (D4/D5), conversions (D6), or the canonical form (D7). It decides equality and leaves the rest open, which is what a single decision should do.

The alternatives considered (D2 report §4): D2b cohort equality (== maps to compare-total: 1.50 == 1.5 is false, each cohort a distinct map key, Bank Account postconditions fragile, Java BigDecimal.equals as the cautionary precedent); D2c value equality plus a cohort-sensitive comparison exposed now (deferred — no use case in the spec, and its form would be decided before D5–D7 say what cohorts exist to compare); deferring D2 entirely (not recommended — leaves Map/Set semantics and the Bank Account contracts unstatable).

9. Consequences

  • TECHSPEC-21 §6 is amended: the equality rule is now specified (GDA compare for finite operands), with the display-similarity sentence's reading recorded.
  • TECHSPEC-24 §§4–5, 8 are amended: Map/Set key and element behavior for Decimal is now defined (value equality, consistent hash); replacement-vs-rejection remains open.
  • TECHSPEC-39 §8 is amended: Decimal's specified equality rule is now recorded (the circular "Primitive types use their specified equality rules" now resolves for Decimal); the dangling special-values sentence still awaits D3.
  • TECHSPEC-57 is amended with a D2 register note; OQ-002 remains Open pending D3–D7.
  • The Language Guide Types page is amended to teach the equality rule.
  • The glossary and cross-reference index gain D2 entries.
  • The Unresolved Questions register records D2 as decided; OQ-002 remains Open; the open count stays 28.
  • The Decision Records index gains a D2 row (ACCEPTED); the accepted count becomes 3.
  • The D2 report (reports/oq-002-d2-decimal-equality.md) is already committed (5f5b435); this record and its integration are committed with the acceptance.
  • The CHANGELOG (0.2.10) records the acceptance as documentation-level, not compiler implementation.

Consequences of rejecting (recorded for the record; Blake accepted as written)

If D2 were rejected, == on Decimal would remain undefined: Map/Set key behavior would stay undefined, the Bank Account postconditions would stay unstatable as written, and D3, D6, and D7 would each have to carry the equality question as an open variable.

10. Acceptance Criteria

  1. The decision record exists and states the decision (§1), the equality rule (§2), the ordering operators (§3), the Map/Set semantics (§4), the no-normalization rule (§5), the minimum commitments (§6), and what D2 does not decide (§7).
  2. TECHSPEC-21 §6 is amended with the equality rule and the display-similarity reading; TECHSPEC-24 §§4–5, 8 are amended with the Map/Set consequences; TECHSPEC-39 §8 records that Decimal's equality rule is now specified and that the special-values sentence awaits D3.
  3. The Unresolved Questions register records D2 as decided (alongside D1) while OQ-002 remains Open; the open count stays 28.
  4. The Language Guide Types page teaches the accepted equality rule without resolving D3–D7, OQ-003, or OQ-004.
  5. The glossary and cross-reference index gain D2 entries (Decimal equality; compare-total).
  6. No D3–D7, OQ-003, or OQ-004 semantics are introduced by the integration.
  7. No normative source file (source-original/) is modified.
  8. No IEEE 754 conformance claim of any version appears in the integration.
  9. NaN/Infinity comparison is left to D3 (not pre-decided); compare-total is not exposed as a language operation.
  10. The CHANGELOG (0.2.10) records the acceptance as documentation-level, not compiler implementation.

11. Implementation Notes

Acceptance is documentation-level: it makes D2 an authoritative language-design decision in the specification. It does not imply that the compiler or runtime already implements or validates these rules. Compiler implementation, the Kyllo decimal runtime module, and conformance testing remain separate requirements.

Implementation complexity for the first Python backend is low for D2 itself: Python's ==, dict, and set already implement value equality with a consistent hash. Two backend requirements in the same class as D1's commitment 6 are added to the OQ-011 list: (a) Kyllo's == on Decimal must be implemented as a same-type Decimal comparison (Python's == on finite Decimals, or an explicit compare call) — never by delegating to Python's cross-type ==; (b) if D3 admits NaN, the comparison path must route NaN operands through D3's defined behavior, not through Python's default NaN behavior (which raises InvalidOperation for ordering comparisons under the default traps and returns False for ==). These are requirements on OQ-011, not a decided runtime architecture.


Evidence: OQ-002 investigation report (reports/oq-002-numeric-representation-investigation.md); OQ-002 D1 report (reports/oq-002-d1-gda-conformance-foundation.md, §7.1 D2 statement); OQ-002 D2 report (reports/oq-002-d2-decimal-equality.md, §9 decision statement, §6 minimum commitments, §2 GDA comparison definitions, §3 Python probes); General Decimal Arithmetic Specification Version 1.70 (25 March 2009, Cowlishaw), pp. 27, 42–43; GDA dectest suite (compare.decTest, comparetotal.decTest); Python 3.12 decimal documentation and empirical checks (D2 report Appendix B).

Design principle: Explicit boundaries. Predictable behavior. No silent arithmetic.

12. Acceptance

Accepted on 2026-11-08, as written — Blake said "Accept" on the decision brief in the docs side chat. No refinement rounds: the §9 decision statement and the §6 minimum commitments were accepted verbatim. The acceptance criteria in §10 were verified during documentation integration:

  1. The decision record exists and states the decision (§1), the equality rule (§2), the ordering operators (§3), the Map/Set semantics (§4), the no-normalization rule (§5), the minimum commitments (§6), and what D2 does not decide (§7).
  2. TECHSPEC-21 §6 is amended with the equality rule and the display-similarity reading; TECHSPEC-24 §§4–5, 8 are amended with the Map/Set consequences; TECHSPEC-39 §8 records that Decimal's equality rule is now specified and that the special-values sentence awaits D3.
  3. The Unresolved Questions register records D2 as decided (alongside D1) while OQ-002 remains Open; the open count stays 28.
  4. The Language Guide Types page teaches the accepted equality rule without resolving D3–D7, OQ-003, or OQ-004.
  5. The glossary and cross-reference index gain D2 entries (Decimal equality; compare-total).
  6. No D3–D7, OQ-003, or OQ-004 semantics are introduced by the integration — cross-document consistency audit completed 2026-11-08 (read-only audit of the record and all five amendments against the D2 report: no factual errors, no scope leaks, no IEEE conformance claim, no implementation claims).
  7. No normative source file (source-original/) is modified — verified by git status: only the D2 decision record (new), amendments (new), builder, CHANGELOG, and generated docs/ pages changed.
  8. No IEEE 754 conformance claim of any version appears in the integration — the only IEEE mention in the record is §8 rationale 3, a factual statement about IEEE 754's compareQuiet operation, not a claim that Kyllo conforms to IEEE 754.
  9. NaN/Infinity comparison is left to D3 (not pre-decided); compare-total is not exposed as a language operation.
  10. The CHANGELOG (0.2.10) records the acceptance as documentation-level, not compiler implementation.

Interpretations recorded at acceptance

  • The display-similarity sentence is not an equality rule. TECHSPEC-21 §6's "must not treat two different numeric representations as equal merely because they appear similar when displayed" forbids display-based equality; it does not decide whether 1.50 == 1.5. D2 decides that: they are equal, because they have the same defined decimal value.
  • No program's meaning changes. No program's Decimal equality behavior was defined before D2; D2 defines it for the first time. Integer is untouched.
  • Integer equality was not stated by D2. The §7 bullet on Integer equality is a follow-up, not an authorization: no equality rule for Integer is stated anywhere by this decision, and Blake's acceptance of D2 — whose §9 decision statement never mentions Integer equality — is not agreement that it is exact. It remains to be stated explicitly, as a documentation completion or as its own small decision.
  • The GDA total-order list is not adopted. The GDA specification's own illustrative total-order list (p. 42) contradicts its rules for negative cohorts; the record and amendments do not repeat the list. If Kyllo ever adopts compare-total, the rule must be stated from the specification's rules, the dectest suite, and the reference implementation — not copied from the list.

Remaining open (not resolved by D2)

  • D3 (failure outcomes, failure representation, special values), D4 (literal typing), D5 (literal format), D6 (conversions), D7 (canonical external form), OQ-003, OQ-004, and the trap configuration. OQ-002 remains Open until D3–D7 are resolved.
  • Whether a cohort-sensitive comparison operation exists (deferred); compareTo's sign convention; duplicate-key replacement vs. rejection; Integer equality.

Scope of acceptance

Acceptance is documentation-level: it makes D2 an authoritative language-design decision in the specification. It does not imply that the compiler or runtime already implements or validates these rules. Compiler implementation, the Kyllo decimal runtime module, and conformance testing remain separate requirements.

Affected sections: TECHSPEC-21 §6, TECHSPEC-24 §§4–5, 8, TECHSPEC-39 §8, TECHSPEC-57 (register note), Language Guide (Types), Glossary, Cross-Reference Index, Unresolved Questions register, Decision Records index, CHANGELOG.md (0.2.10).