4d45bc6c56
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 (speca68d7b6, plan400ad7c, iter5170b6a), 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
53 lines
2.3 KiB
Rust
53 lines
2.3 KiB
Rust
//! Pin: the codegen mechanism that emits primitive Eq/Ord
|
|
//! instance bodies (try_emit_primitive_instance_body) MUST
|
|
//! attach an `alwaysinline` attribute to the generated LLVM
|
|
//! function definition so the `-O0` build path inlines the
|
|
//! single-instruction body at every use site, matching the IR
|
|
//! shape the deleted `lower_eq` direct-emit produced. Without
|
|
//! `alwaysinline`, `ail run` (which defaults to -O0) pays a
|
|
//! function-call overhead per comparison.
|
|
|
|
use std::path::PathBuf;
|
|
use std::process::Command;
|
|
|
|
#[test]
|
|
fn prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir() {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let repo_root = PathBuf::from(manifest_dir).join("../..");
|
|
// Use a fixture that invokes the class-method `eq` path so
|
|
// `prelude.eq__Int` gets monomorphised into the emitted IR.
|
|
// `eq_ord_user_adt.ail` calls `(app eq …)` on an IntBox whose
|
|
// user instance body internally calls `(app eq ai bi)` on Int —
|
|
// class-dispatch resolves both the outer (Eq IntBox) and inner
|
|
// (Eq Int) calls. The inner Eq Int path is the one we pin here:
|
|
// its `define i1 @ail_prelude_eq__Int(...) alwaysinline { ... }`
|
|
// must carry `alwaysinline` so the call folds to a single
|
|
// `icmp eq i64` at every use site under -O0.
|
|
let fixture = repo_root.join("examples/eq_ord_user_adt.ail");
|
|
|
|
let out = Command::new(env!("CARGO_BIN_EXE_ail"))
|
|
.arg("emit-ir")
|
|
.arg(&fixture)
|
|
.output()
|
|
.expect("ail emit-ir failed to spawn");
|
|
assert!(
|
|
out.status.success(),
|
|
"ail emit-ir failed: stderr={}",
|
|
String::from_utf8_lossy(&out.stderr)
|
|
);
|
|
|
|
let ir = String::from_utf8_lossy(&out.stdout);
|
|
// Either form is acceptable:
|
|
// define i1 @ail_prelude_eq__Int(i64, i64) alwaysinline { ... }
|
|
// (attribute appended to the define line). The grep checks for
|
|
// both the symbol presence and the alwaysinline annotation in
|
|
// the same line as the define.
|
|
let saw_define_with_alwaysinline = ir
|
|
.lines()
|
|
.any(|line| line.contains("define") && line.contains("eq__Int") && line.contains("alwaysinline"));
|
|
assert!(
|
|
saw_define_with_alwaysinline,
|
|
"expected `define … eq__Int … alwaysinline` in emitted IR; full IR follows:\n{ir}"
|
|
);
|
|
}
|