Files
AILang/design/contracts/prelude-classes.md
T
Brummel 4d45bc6c56 audit + close: operator-routing-eq-ord — drift_found → post-audit tidy applied inline; CLEAN
Single-iter milestone closing Gitea #1. Architect surfaced 4
honesty-rule drift items (2 high, 2 medium) — all post-Task-13
documentation drift, no codegen behaviour shift. Bench all three
scripts exit 0. Inline tidy applied under the CLAUDE.md trivial-
mechanical carve-out (4 documentation/comment edits on architect-
pre-named sites; no code surface change). No separate tidy iter.

Architect: drift_found → CLEAN-after-tidy.

The architect walked design/INDEX.md, the contracts touched by
the milestone (float-semantics, prelude-classes, str-abi,
scope-boundaries, authoring-surface, typeclasses, method-dispatch),
`git log 86dd7d7..HEAD --format=full` for the three milestone
commits (spec a68d7b6, plan 400ad7c, iter 5170b6a), and the
diff. The 4 drift items + per-item resolution:

  - [high] `design/contracts/prelude-classes.md` — Task-13
    direct-icmp intercept for `lt__Int`/`le__Int`/`gt__Int`/
    `ge__Int`/`ne__Int` was invisible. The Ord-class free-helper
    paragraph described them as routing via `compare` + `match`
    over Ordering, with no statement that at Int they short-
    circuit to direct `icmp`. The IR shape is actively pinned by
    `ord_int_intercept_ir_pin.rs` and load-bearing for
    `bench_closure_chain` perf. Tidy: new paragraph added naming
    the Int-direct family explicitly, the bench_closure_chain
    rationale, and the deliberate Int-only asymmetry (Bool/Str
    still go through compare→match because no current fixture
    exercises Ord-helpers there at hot-loop frequency).

  - [high] `crates/ailang-codegen/src/lib.rs:2777-2789` —
    rustdoc on `try_emit_primitive_instance_body` was three
    iterations stale: claimed "currently inhabited arms: `eq__Str`
    and `compare__Int/Bool/Str`" (= 4 arms, was correct as of
    iter 23.3), and claimed `eq__Int`/`eq__Bool` "ride the natural
    `lower_eq` dispatch via their lambda body `(== x y)`". Both
    claims are factually false post-milestone (the arm set is now
    14: 4 Eq + 3 compare + 5 lt/le/gt/ge/ne + 6 float_*; lower_eq
    is deleted; `==` is no longer a surface name). Tidy: rustdoc
    rewritten to enumerate the actual 14 arms, explain the
    alwaysinline-pairing, name the Int-direct optimization
    asymmetry, and cross-reference prelude-classes.md.

  - [medium] `crates/ail/tests/prelude_eq_alwaysinline_ir_pin.rs:19-21`
    — comment asserted the test could not use `eq_demo.ail`
    because "`eq_demo.ail` calls `(app == …)` directly which today
    uses the `lower_eq` direct-emit short-circuit and never
    references the prelude eq__Int symbol". Both factual claims
    became false the moment Task 4's fixture migration landed
    (eq_demo.ail now calls `(app eq …)`) and Task 7 deleted
    lower_eq. The test functions correctly but the rationale-
    comment lied about why the fixture choice mattered. Tidy:
    comment rewritten to describe the actual test path —
    `eq_ord_user_adt.ail` invokes `(app eq …)` on IntBox whose
    user-instance body internally calls `(app eq ai bi)` on Int,
    monomorphising `prelude.eq__Int` into the emitted IR where
    the alwaysinline attribute on the `define` line is the
    assertion target.

  - [medium] `design/contracts/typeclasses.md` — implicit-prelude-
    import was load-bearing at codegen for the new bare-prelude-
    fn surface (`(app float_eq …)` etc. without explicit
    `prelude.float_eq` qualifier) but no contract named it. The
    codegen extension (`lower_app::resolve_callee`,
    `is_static_callee`, `resolve_top_level_fn` each gained a
    parallel prelude-fallback during iter Task 2/3) was a
    silent dependency on a workspace invariant that lived only
    in typechecker-side comments. Tidy: invariant (4) added as a
    sibling to the existing three invariants on cross-module
    typeclass references, naming the source-level codegen
    fallback as the symmetric partner to invariant (2)'s post-
    mono fallback; the summary paragraph extended from "three
    invariants are lockstep partners" to four, with
    `float_compare_smoke_e2e.rs` named as the invariant-(4)
    ratifier.

The four fixes are pure-docs/comment work (no `.rs` code path
changed, no IR shape changed); the workspace passing-test count
is unchanged (640 still GREEN, 0 FAILED). No code regression
risk; therefore inline-apply rather than dispatching a separate
planner+implement cycle was the right cost-benefit call.

Bench: 0/0/0.

  - bench/check.py exit 0 — 36 metrics, 0 regressed (full
    bench_closure_chain healed by Task 13's direct-icmp
    intercept arms — bump_s -6.05% rc_s -2.67% bump_rss_kb
    -0.77% rc_rss_kb +0.46%, all four previously-regressed
    metrics back inside tolerance).
  - bench/compile_check.py exit 0 — 24 metrics, 0 regressed.
    Largest delta build_O0_ms.bench_list_sum_explicit +11.04%
    (within 20% tolerance — operator-routing adds two extra
    intercept-arm matches on the compile hot-path, marginal).
  - bench/cross_lang.py exit 0 — 25 metrics, 0 regressed.

No baseline updates needed; no `--update-baseline` invoked. The
intercept-arm extension in Task 13 explicitly aimed at restoring
the pre-milestone baseline; bench data ratifies that the aim was
met.

Pipeline this milestone followed:

  brainstorm (one-question-at-a-time Q&A, three design forks
              resolved: operator-names die from surface; Float
              keeps named fns; codegen via always-call +
              alwaysinline pre-emptive mitigation; grounding-
              check PASS over 11 load-bearing assumptions, after
              one BLOCK → spec defect about desugar.rs:2414
              mischaracterisation fixed)
  -> planner (recon DONE_WITH_CONCERNS, 4 substantive spec gaps
              absorbed inline: 60 fixtures not ≈10, 8 in-source
              AST-literal sites not 3, 5 contract files not 3,
              alwaysinline mechanism new to crate; plus 2 self-
              review items absorbed inline: north-star RED-first
              via Unit-eq line; lockstep edits at codegen/lib.rs:2529)
  -> implement (DONE 12/12 from orchestrator; Boss reclassified
                as PARTIAL via acceptance-#8-fail after independent
                bench re-run; iter scope extended to Task 13 via
                SendMessage to same orchestrator-context; final
                DONE 13/13)
  -> audit (drift_found, 4 items inline-tidy applied: prelude-
            classes.md paragraph + codegen rustdoc refresh + test
            comment rewrite + typeclasses.md invariant-(4)
            addition; bench 0/0/0; CLEAN-after-tidy)

Closing Gitea #1. The `closes #1` trailer lived on the iter
commit `5170b6a` and fired on push.

Spec: docs/specs/2026-05-20-operator-routing-eq-ord.md
Plan: docs/plans/operator-routing-eq-ord.1.md
2026-05-21 01:26:13 +02:00

4.0 KiB

Prelude (built-in) classes

Prelude (built-in) classes

The prelude ships the Ordering ADT, the Eq and Ord classes, primitive Eq Int/Bool/Str/Unit and Ord Int/Bool/Str instances, and the five polymorphic free-fn helpers ne/lt/le/gt/ge. The primitive Eq / Ord instance bodies are placeholder lambdas in examples/prelude.ail; the codegen intercept try_emit_primitive_instance_body overrides them with single-instruction bodies (icmp eq i64 for eq__Int, icmp eq i1 for eq__Bool, @ail_str_eq for eq__Str, ret i1 1 for eq__Unit; a three-way icmp ladder constructing LT/EQ/GT for compare__T) and attaches alwaysinline so the call folds to the single instruction at every use site.

The five Ord-class free helpers (ne/lt/le/gt/ge) have source-level bodies that route through eq / compare plus match over Ordering — written that way so the surface form is LLM-natural and so user-ADT instances of Eq/Ord automatically pick up working helpers without any per-type opt-in. At Int the codegen intercept short-circuits this indirection: lt__Int, le__Int, gt__Int, ge__Int, and ne__Int are intercept- overridden to emit icmp slt i64 / sle i64 / sgt i64 / sge i64 / ne i64 directly, bypassing the compare__IntOrdering-ctor → match path. Without this short-circuit the Ordering ctor allocates per call inside hot loops — a real cost surfaced by the bench_closure_chain regression at the operator-routing-eq-ord milestone and pinned by crates/ail/tests/ord_int_intercept_ir_pin.rs. The short-circuit is Int-only by deliberate asymmetry: no current bench or example fixture exercises Ord-lt/le/gt/ge at Bool or Str at sufficient call frequency to justify the intercept-arm maintenance; the symmetric extensions are mechanical when the first such workload appears.

Float has neither Eq nor Ord instance per Float semantics; a polymorphic helper invoked at Float fires NoInstance at typecheck with a Float-aware diagnostic that names the explicit float_eq / float_lt alternatives and cross-references this section. The lookup machinery that turns a show x call site into the right monomorphic instance is documented in method dispatch.

Float comparison is a separate surface: the six monomorphic prelude fns float_eq / float_ne / float_lt / float_le / float_gt / float_ge each carry the type (Float, Float) -> Bool without a class constraint. They lower via the same intercept machinery as the primitive Eq instances (single fcmp instruction with the matching predicate oeq / une / olt / ole / ogt / oge, plus alwaysinline). They replace what would otherwise be a polymorphic == / < on Float — Float gets full comparability via explicit named fns, not via an implicit class instance.

The prelude ships class Show a where show : (a borrow) -> Str and primitive Show Int, Show Bool, Show Str, Show Float instances. Float is included in Show (unlike Eq/Ord) — IEEE-754 makes structural equality and total ordering semantically dubious, but textual representation of a Float is well-defined modulo the NaN-spelling caveat in Float semantics. Each Show <T> instance body is a single-application lambda invoking the corresponding runtime primitive (int_to_str, bool_to_str, str_clone, float_to_str; see Str ABI for the heap-Str primitives); no codegen intercept is required. The polymorphic helper print : forall a. Show a => a -> () !IO has body \x -> let s = show x in do io/print_str s (explicit let-binder for heap-Str RC discipline per the Str carve-out in Str ABI). The let-binder is structurally pinned by crates/ail/tests/print_mono_body_shape.rs. Routing through print is the path for non-Str primitives; io/print_str is the only built-in direct-output effect-op.

Ratified by: crates/ail/tests/show_no_instance_e2e.rs.