iter operator-routing-eq-ord.1 (DONE 13/13): drop comparator builtins, route through Eq/Ord
Closes Gitea #1. Realises the "P2 follow-up" called out in examples/prelude.ail:9 — removes the surface comparator names `==` / `!=` / `<` / `<=` / `>` / `>=` from the language entirely, routes the LLM-author's `(app eq …)` / `(app compare …)` / `(app ne|lt|le|gt|ge …)` through prelude.Eq / prelude.Ord class- dispatch, ships six named Float-comparison fns (`float_eq`/`float_ne`/`float_lt`/`float_le`/`float_gt`/`float_ge`) so Float keeps comparability without an Eq/Ord instance, and emits primitive instance bodies with `alwaysinline` so the -O0 IR shape stays at one instruction per comparison. Plan-task journal (13 tasks, single atomic iter per Approach A): Task 1 — Bootstrap: 4 new fixtures (eq_user_adt_smoke.ail, eq_float_must_fail.ail, float_compare_smoke.ail, operator_unbound_check.ail) + 5 new E2E/pin tests as RED starting state. All 5 confirmed RED at start: north-star fixed `NoInstance Eq Unit` (preserved by Unit-eq opening line intentionally added in plan-self-review); float_compare unknown `float_eq`; operator-name typechecked (== still polymorphic); must-fail diagnostic lacked `float_eq`; alwaysinline absent from IR. Task 2 — Codegen alwaysinline + intercept arms: introduced `intercept_emit_wants_alwaysinline` allowlist + appended ` alwaysinline` between `)` and `{` of the `define` line in `emit_fn`; added 9 new intercept arms to `try_emit_primitive_instance_body` — `eq__Int`/`eq__Bool`/ `eq__Unit` + the six `float_*` arms. Task 3 — Prelude reshape: `instance Eq Unit` added; Eq Int/Bool bodies become placeholder-`false` (intercept overrides); six `float_*` free fns added; line-9 P2-follow-up comment removed. Lockstep hash re-pins: prelude_module_hash_pin (3abe0d3fa3c11c99 → new), mono_hash_stability six body-hash literals (eq__Int/Bool/Str + compare__Int/Bool/Str all shifted because placeholder body changes the canonical hash). IR-snapshot regen (hello/list/max3/sum/ws_main) rolled forward into this task at orchestrator's pragmatic call — prelude shift forces the snapshots immediately. Task 4 — Fixture migration: 58 .ail fixtures rewritten (`(app == …)` → `(app eq …)`, etc.; Float-typed operand sites to `(app float_eq …)` / `(app float_lt …)`). eq_ord_user_adt.ail:21 inner `==` → `eq` migration with lockstep eq_ord_e2e.rs:134 body-hash re-pin (3c4cf040cb4e8bb2 → new). Two prose snapshots accepted; deps test + 5 hash_pin literals updated as cascading consequences. Task 5 — Test-scaffold migration: all 8 in-source `#[cfg(test)] mod tests` AST-literal sites threaded (desugar.rs:2414, check/lib.rs:5656/6092/6220, codegen/lib.rs sites). 7 obsolete in-source tests deleted (5 eq_typechecks + 2 lower_eq ADT/Fn rejection — coverage moves to E2E eq_user_adt_smoke + eq_float_must_fail). 2 additional letrec tests deleted (single-module check env can't resolve eq/ge without prelude auto-import; covered by workspace E2E). Task 6 — Lit-pattern desugar: build_eq emits `Term::Var { name: "eq" }` instead of `"=="` at desugar.rs:1109; doc-comment rewritten to describe class-dispatch. Task 7 — Dead-machinery deletion sweep (compile-gated): typchecker builtins (install + list comparator entries deleted); codegen builtin_binop_typed (10 comparator arms deleted from synth.rs, table reduced to arithmetic core); lower_eq fn entirely deleted; `==` short-circuit in lower_app deleted; `is_static_callee` `==` clause deleted; `is_arithmetic_or_comparison_op` renamed to `is_arithmetic_op` + caller-update at codegen/lib.rs:2152 + :2529. Dead `poly_a_a_to_bool` helpers also removed. Workspace build green after compile gate — every caller of the deleted surface was migrated by Tasks 4-6. Task 8 — Float-aware NoInstance diagnostic: check/lib.rs:856-880 addendum extended to name `float_eq` / `float_lt` as the explicit alternative for Eq/Ord at Float; eq_float_noinstance.rs:32-44 assertion extended. Task 9 — IR snapshot regen: subsumed by Task 3 (prelude shift forced immediate snapshot regen; deferring to Task 9 would have left the workspace red between tasks). Task 10 — Prose-projection cleanup: 6 comparator arms deleted from binop_info; 3 in-source mod-tests updated/deleted (comparator-infix rendering would be dishonest now that the operators are no longer language identifiers). Task 11 — Contract updates: 5 design files rewritten to the class-dispatch present — float-semantics.md (arithmetic guarantees retained on +/-/*/; comparison guarantees transferred from ==/!=/< to float_eq/float_ne/float_lt/etc.); prelude-classes.md (Eq Unit added to instance list; new paragraph on six float_* fns; Float-no-Eq/Ord clause gains `→ use float_eq` cross-reference); str-abi.md (clause "REMAIN primitive operators" rewritten to describe class-method dispatch); scope-boundaries.md (multiple operator-name and Pattern::Lit-desugar clauses rewritten); authoring-surface.md (==, <= dropped from operator-example list). Task 12 — Initial acceptance gate: workspace 638/0 GREEN; bench/compile_check + cross_lang 0 regressed; bench/check.py flagged 4 regressions on bench_closure_chain (+29% bump_s, +47% rc_s, +29% bump_rss_kb, +48% rc_rss_kb). Orchestrator initially classified as DONE-with-concerns; Boss reclassified as PARTIAL-via-acceptance-#8-fail after independent re-run, extended iter scope to Task 13. Task 13 — Direct icmp intercept arms (Boss-extension): try_emit_primitive_instance_body gains direct-icmp arms for lt__Int / le__Int / gt__Int / ge__Int / ne__Int, bypassing the compare__Int → Ordering → match indirection that allocated one Ordering ctor per call in tight loops. Each new arm inherits `alwaysinline` via the existing allowlist (extended accordingly). New IR pin test `ord_int_intercept_ir_pin.rs` + smoke fixture `ord_int_intercept_smoke.ail` ratify the optimization — opt -O2 -S confirms zero `call @ail_prelude_lt__Int` in optimized IR; the icmp folds directly at every use site. Bool variants (lt__Bool/etc.) deliberately NOT added — no bench/example calls Ord at Bool; spec-extension permits skipping if unreachable at bench level. Family can be extended symmetrically when first Bool-ordered perf workload appears. Bench-gate post-Task-13: bench_closure_chain 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. Full bench corpus: 36 metrics, 0 regressed, 0 improved beyond tolerance, 36 stable. bench/check.py exit 0 ✓; bench/compile_check.py exit 0 ✓; bench/cross_lang.py exit 0 ✓. Workspace: 640 passed, 0 failed across all binaries (delta vs. milestone start: +5 new E2E/pin tests added in Task 1 + 1 IR pin added in Task 13 − 9 in-source mod tests deleted as obsolete net ≈ −3). The eq_user_adt_smoke fixture's Unit-opening line was the deliberate RED-first device added in planner self-review so the north-star wouldn't accidentally GREEN at start (user-ADT-Eq on Point with hand-written instance was already operable today; the milestone's actual delivery is operator-name death + Eq Unit + Float-named-fns + cleanup of the two-pathy primitive comparator machinery). Net delta: 96 files changed, 1760 insertions, 1101 deletions; 12 net-new files (6 new fixtures, 5 new E2E/pin tests, 1 stats file). main passes-test-count: 640 (was 633 pre-iter, accounting for the deletions). Spec-vs-acceptance addendum: spec §Testing strategy anticipated the bench-gate-regression case with two recovery paths (`alwaysinline` investigation or "spec needs revisiting toward α"). Task 13 is the third path — direct intercept arms for `lt`/etc. that bypass the compare→match path entirely. The spec's contingency clause was thus generous enough to absorb Task 13 without spec revision, but a future iter that hits a class-method primitive where the body shape introduces a similar codegen cost (Ordering allocation, RC tax, deferred-init) should expect a parallel extension. The pattern is "primitive instance whose canonical body indirects through other class methods that allocate" → add a direct-emit intercept arm + alwaysinline + IR-shape pin. Concerns absorbed: - Codegen lower_app / resolve_top_level_fn gained a prelude-fallback lookup so bare monomorphic prelude fns (`float_eq` etc.) resolve from non-prelude modules without explicit `prelude.float_eq` qualifier. Mirrors the typechecker's implicit prelude import — not in plan but necessary infra for the named-fn surface to be callable. - Recon's "≈10 fixtures" estimate was 6× too low — 58 actual. Mechanical migration; no design impact. - Recon caught 4 in-source AST-literal sites the spec missed (lib.rs:5656 `>=` site + three codegen/lib.rs sites); Task 5 covered all. - Recon caught 2 contract files outside the spec's update set that directly contradicted the milestone (str-abi.md + scope-boundaries.md "REMAIN primitive operators" / "Pattern::Lit desugar to ==" clauses); Task 11 covered all 5. Stats file: `bench/orchestrator-stats/2026-05-21-iter-operator-routing-eq-ord.1.json`. closes #1
This commit is contained in:
+417
-390
@@ -57,18 +57,17 @@ use synth::{
|
||||
fn_sig_from_type, ll_string_literal, llvm_type,
|
||||
};
|
||||
|
||||
/// Floats iter 4.2 fixup: classify a builtin name as a polymorphic
|
||||
/// arithmetic or comparison op (the set `+`, `-`, `*`, `/`, `%`,
|
||||
/// `!=`, `<`, `<=`, `>`, `>=`). `==` is NOT in this set — it goes
|
||||
/// through `lower_eq` separately. Used by `lower_app` to decide
|
||||
/// whether to call `builtin_binop_typed`, and by `is_static_callee`
|
||||
/// (in combination with `==` and `not`) to recognise built-in
|
||||
/// callees during the global-resolution pass.
|
||||
fn is_arithmetic_or_comparison_op(name: &str) -> bool {
|
||||
matches!(
|
||||
name,
|
||||
"+" | "-" | "*" | "/" | "%" | "!=" | "<" | "<=" | ">" | ">="
|
||||
)
|
||||
/// Classifies a builtin name as a polymorphic arithmetic op (the
|
||||
/// set `+`, `-`, `*`, `/`, `%`). After iter operator-routing-eq-ord.1
|
||||
/// the comparator names `==` / `!=` / `<` / `<=` / `>` / `>=` are
|
||||
/// no longer surface-level identifiers (comparison goes through the
|
||||
/// `eq` / `compare` class methods and the `float_*` named fns), so
|
||||
/// only the five arithmetic ops live here. Used by `lower_app` to
|
||||
/// decide whether to call `builtin_binop_typed`, and by
|
||||
/// `is_static_callee` (in combination with `not`) to recognise
|
||||
/// built-in callees during the global-resolution pass.
|
||||
fn is_arithmetic_op(name: &str) -> bool {
|
||||
matches!(name, "+" | "-" | "*" | "/" | "%")
|
||||
}
|
||||
|
||||
/// Failure modes of [`emit_ir`] / [`lower_workspace`].
|
||||
@@ -1150,6 +1149,51 @@ impl<'a> Emitter<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns `true` when the codegen should emit the LLVM
|
||||
/// `alwaysinline` attribute on this fn's `define` line. The
|
||||
/// allowlist is the set of primitive-instance bodies emitted by
|
||||
/// `try_emit_primitive_instance_body` whose IR is so trivial
|
||||
/// (one `icmp` / `fcmp` / `call` plus a `ret`) that `-O0` build
|
||||
/// paths pay an unacceptable per-call overhead without inlining,
|
||||
/// while `-O2` would inline these unconditionally anyway. The
|
||||
/// list mirrors the intercept arm set in
|
||||
/// `try_emit_primitive_instance_body` for the equality /
|
||||
/// ordering / Float-comparison primitives. Checked against the
|
||||
/// unmangled fn name; these symbols only live in the prelude
|
||||
/// module so cross-module collisions are not a concern.
|
||||
fn intercept_emit_wants_alwaysinline(symbol: &str) -> bool {
|
||||
if matches!(
|
||||
symbol,
|
||||
"eq__Int" | "eq__Bool" | "eq__Str" | "eq__Unit"
|
||||
| "compare__Int" | "compare__Bool" | "compare__Str"
|
||||
| "float_eq" | "float_ne"
|
||||
| "float_lt" | "float_le"
|
||||
| "float_gt" | "float_ge"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// The polymorphic free helpers `ne` / `lt` / `le` / `gt` /
|
||||
// `ge` mono into per-type bodies. At `_Int` (iter
|
||||
// operator-routing-eq-ord.1 Task 13) they are intercepted as
|
||||
// single direct `icmp` instructions in
|
||||
// `try_emit_primitive_instance_body` — same shape as
|
||||
// `eq__Int`, ie. one `icmp` + one `ret`. At other types
|
||||
// (`_Bool`, `_Str`, user ADTs) they keep their polymorphic
|
||||
// body — a one-arm-deep `match (compare x y) (case LT ... )
|
||||
// (case _ ... )` — which still folds at `-O2` provided the
|
||||
// mono symbol carries `alwaysinline`. Both shapes benefit
|
||||
// from the attribute; the stem-prefix match covers all T the
|
||||
// mono pass produces uniformly so the bench-perf parity with
|
||||
// the pre-milestone arithmetic-comparator emission is
|
||||
// preserved end-to-end.
|
||||
if let Some(stem) = symbol.split("__").next() {
|
||||
if matches!(stem, "ne" | "lt" | "le" | "gt" | "ge") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn emit_fn(&mut self, f: &FnDef) -> Result<()> {
|
||||
// also lift `param_modes` out of the fn type. The
|
||||
// fn-return Own-param dec emission below consults it to decide
|
||||
@@ -1241,7 +1285,17 @@ impl<'a> Emitter<'a> {
|
||||
self.ssa_fn_sigs.insert(pssa, fs);
|
||||
}
|
||||
}
|
||||
sig.push_str(") {\n");
|
||||
if Self::intercept_emit_wants_alwaysinline(&f.name) {
|
||||
// Append the `alwaysinline` attribute between the close-paren
|
||||
// of the parameter list and the `{` opening the body. Symbols
|
||||
// matched here are emitted by `try_emit_primitive_instance_body`;
|
||||
// forcing inlining keeps the single `icmp` / `fcmp` body folded
|
||||
// at every use site under `-O0` (matching the IR shape the
|
||||
// deleted `lower_eq` direct-emit path produced).
|
||||
sig.push_str(") alwaysinline {\n");
|
||||
} else {
|
||||
sig.push_str(") {\n");
|
||||
}
|
||||
self.body.push_str(&sig);
|
||||
self.start_block("entry");
|
||||
|
||||
@@ -2124,32 +2178,19 @@ impl<'a> Emitter<'a> {
|
||||
}
|
||||
|
||||
fn lower_app(&mut self, name: &str, args: &[Term], tail: bool) -> Result<(String, String)> {
|
||||
// `==` is polymorphic (`forall a. (a, a) -> Bool`).
|
||||
// Dispatch on the resolved AIL arg type — the LLVM `ptr` shape
|
||||
// aliases multiple AIL types (Str vs ADT vs Fn), so we cannot
|
||||
// dispatch on the LLVM type alone. ADT/Fn equality is rejected
|
||||
// here with a clear error; `Unit` evaluates both sides for
|
||||
// their side effects then returns constant `i1 1`.
|
||||
if name == "==" {
|
||||
if args.len() != 2 {
|
||||
return Err(CodegenError::Internal(
|
||||
"builtin `==` expected 2 args".into(),
|
||||
));
|
||||
}
|
||||
let arg_ty = self.synth_arg_type(&args[0])?;
|
||||
let (a, a_ll) = self.lower_term(&args[0])?;
|
||||
let (b, _b_ll) = self.lower_term(&args[1])?;
|
||||
let _ = tail;
|
||||
return self.lower_eq(&arg_ty, &a, &b, &a_ll);
|
||||
}
|
||||
// 2026-05-21 operator-routing-eq-ord: the `if name == "=="`
|
||||
// short-circuit that routed through `lower_eq` is gone. `==`
|
||||
// is no longer a surface identifier; the typechecker
|
||||
// intercepts `(app == …)` as `unknown variable` long before
|
||||
// codegen. Equality dispatch lives in
|
||||
// `try_emit_primitive_instance_body` for the primitive Eq
|
||||
// instance bodies (Int/Bool/Str/Unit), called via the
|
||||
// class-method `eq` from prelude.Eq.
|
||||
|
||||
// Floats iter 4.2: arithmetic / comparison are now polymorphic
|
||||
// over `{Int, Float}`. Resolve the arg type, then dispatch via
|
||||
// `builtin_binop_typed`. Same shape as the `==` dispatch above.
|
||||
// Comparison ops are matched here in iter 4.2 with Int-only
|
||||
// dispatch (preserving pre-iter-4 behaviour) so the workspace
|
||||
// stays green; iter 4.3 adds the Float comparison arms.
|
||||
if is_arithmetic_or_comparison_op(name) {
|
||||
// Arithmetic ops `+`/`-`/`*`/`/`/`%` are still polymorphic
|
||||
// over `{Int, Float}`. Resolve the arg type, then dispatch
|
||||
// via `builtin_binop_typed`.
|
||||
if is_arithmetic_op(name) {
|
||||
if args.len() != 2 {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"builtin `{name}` expected 2 args"
|
||||
@@ -2389,6 +2430,21 @@ impl<'a> Emitter<'a> {
|
||||
return self.emit_call(self.module_name, name, &sig, args, tail);
|
||||
}
|
||||
|
||||
// operator-routing-eq-ord.1: prelude fallback for monomorphic
|
||||
// prelude fns called bare from a non-prelude module (e.g.
|
||||
// `(app float_eq …)`). Mirrors the fallback already in
|
||||
// `is_static_callee` and `resolve_top_level_fn`.
|
||||
if self.module_name != "prelude" {
|
||||
if let Some(sig) = self
|
||||
.module_user_fns
|
||||
.get("prelude")
|
||||
.and_then(|m| m.get(name))
|
||||
.cloned()
|
||||
{
|
||||
return self.emit_call("prelude", name, &sig, args, tail);
|
||||
}
|
||||
}
|
||||
|
||||
Err(CodegenError::Internal(format!(
|
||||
"unknown callee: `{name}`"
|
||||
)))
|
||||
@@ -2526,7 +2582,7 @@ impl<'a> Emitter<'a> {
|
||||
/// `prefix.def`. Locals shadow this — the caller checks for
|
||||
/// that first.
|
||||
fn is_static_callee(&self, name: &str) -> bool {
|
||||
if is_arithmetic_or_comparison_op(name) || name == "==" || name == "not" {
|
||||
if is_arithmetic_op(name) || name == "not" {
|
||||
return true;
|
||||
}
|
||||
// Floats iter 4.4: new fn-builtins (`neg`, `int_to_float`,
|
||||
@@ -2554,9 +2610,35 @@ impl<'a> Emitter<'a> {
|
||||
// iter 23.4: poly-def arm dropped — post-mono there are no
|
||||
// `Type::Forall` defs in `module_user_fns` to begin with, and
|
||||
// `module_polymorphic_fns` no longer exists.
|
||||
self.module_user_fns
|
||||
if self
|
||||
.module_user_fns
|
||||
.get(self.module_name)
|
||||
.is_some_and(|m| m.contains_key(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// operator-routing-eq-ord.1: the typechecker auto-imports
|
||||
// prelude into every non-prelude module so a bare reference
|
||||
// like `(app float_eq …)` typechecks against the prelude's
|
||||
// monomorphic `float_eq` fn. The codegen needs the matching
|
||||
// resolution: if the name is not in the current module but
|
||||
// IS in prelude's monomorphic fn table, treat it as a static
|
||||
// callee — the call lowers to `@ail_prelude_<name>` directly.
|
||||
// Without this fallback, post-mono polymorphic-prelude calls
|
||||
// (e.g. `print`) work because the mono pass synthesises a
|
||||
// mono arm `print__T` into the user's module, but monomorphic
|
||||
// prelude fns (no Forall to instantiate) never get a mirror
|
||||
// arm in user modules and would otherwise fail at codegen
|
||||
// with `unknown variable`.
|
||||
if self.module_name != "prelude"
|
||||
&& self
|
||||
.module_user_fns
|
||||
.get("prelude")
|
||||
.is_some_and(|m| m.contains_key(name))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// resolve `name` to a top-level fn-value, i.e. the address
|
||||
@@ -2587,15 +2669,32 @@ impl<'a> Emitter<'a> {
|
||||
let sig = self.module_user_fns.get(target)?.get(suffix)?.clone();
|
||||
return Some((format!("@ail_{target}_{suffix}_clos"), sig));
|
||||
}
|
||||
let sig = self
|
||||
if let Some(sig) = self
|
||||
.module_user_fns
|
||||
.get(self.module_name)?
|
||||
.get(name)?
|
||||
.clone();
|
||||
Some((
|
||||
format!("@ail_{module}_{name}_clos", module = self.module_name),
|
||||
sig,
|
||||
))
|
||||
.get(self.module_name)
|
||||
.and_then(|m| m.get(name))
|
||||
.cloned()
|
||||
{
|
||||
return Some((
|
||||
format!("@ail_{module}_{name}_clos", module = self.module_name),
|
||||
sig,
|
||||
));
|
||||
}
|
||||
// operator-routing-eq-ord.1: fall back to prelude (mirrors
|
||||
// the auto-import the typechecker performs for non-prelude
|
||||
// modules). Needed for monomorphic prelude fns (e.g. `float_eq`)
|
||||
// which have no mono mirror in the user's module.
|
||||
if self.module_name != "prelude" {
|
||||
if let Some(sig) = self
|
||||
.module_user_fns
|
||||
.get("prelude")
|
||||
.and_then(|m| m.get(name))
|
||||
.cloned()
|
||||
{
|
||||
return Some((format!("@ail_prelude_{name}_clos"), sig));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// resolve a `Term::Var` reference to a const def. Returns
|
||||
@@ -2794,10 +2893,241 @@ impl<'a> Emitter<'a> {
|
||||
)?;
|
||||
Ok(true)
|
||||
}
|
||||
"eq__Int" => {
|
||||
if param_tys != ["i64", "i64"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"eq__Int body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (i64, i64) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = icmp eq i64 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"eq__Bool" => {
|
||||
if param_tys != ["i1", "i1"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"eq__Bool body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (i1, i1) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = icmp eq i1 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"eq__Unit" => {
|
||||
// Unit is single-inhabitant: all values compare equal.
|
||||
// The fn signature still carries the operand slots; we
|
||||
// discard them and return true unconditionally.
|
||||
if ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"eq__Unit body intercept: unexpected return type \
|
||||
({param_tys:?}) -> {ret_ty} (want -> i1)"
|
||||
)));
|
||||
}
|
||||
self.body.push_str(" ret i1 1\n");
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"float_eq" => {
|
||||
if param_tys != ["double", "double"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"float_eq body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = fcmp oeq double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"float_ne" => {
|
||||
// `fcmp une` ("unordered or not-equal"), not `one`,
|
||||
// mirroring float-semantics.md: NaN != NaN must be true.
|
||||
if param_tys != ["double", "double"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"float_ne body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = fcmp une double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"float_lt" => {
|
||||
if param_tys != ["double", "double"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"float_lt body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = fcmp olt double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"float_le" => {
|
||||
if param_tys != ["double", "double"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"float_le body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = fcmp ole double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"float_gt" => {
|
||||
if param_tys != ["double", "double"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"float_gt body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = fcmp ogt double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"float_ge" => {
|
||||
if param_tys != ["double", "double"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"float_ge body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (double, double) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = fcmp oge double {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
// Ord/Eq Int direct-icmp arms (iter operator-routing-eq-ord.1
|
||||
// Task 13). The polymorphic prelude bodies are
|
||||
// `(match (app compare x y) ...)`; without these intercept
|
||||
// arms, each `lt`-at-Int call site goes through
|
||||
// `compare__Int` → allocate an `Ordering` ctor → match.
|
||||
// At `-O2` even with `alwaysinline` on the helpers, clang
|
||||
// does not always collapse the Ordering allocation across
|
||||
// the helper boundary (observed: +29% bump_s, +47% rc_s
|
||||
// bench_closure_chain regression before this arm shipped).
|
||||
// Direct `icmp slt i64` at the helper body lets clang fold
|
||||
// the comparison to the use site reliably.
|
||||
//
|
||||
// `ne__Int` uses `icmp ne i64` directly, NOT the
|
||||
// `not (eq x y)` indirection — that would re-introduce one
|
||||
// call boundary and one `xor i1 %r, true` per comparison
|
||||
// that contributes no semantic value beyond the direct
|
||||
// `icmp ne`.
|
||||
//
|
||||
// Bool variants (`lt__Bool`, `le__Bool`, etc.) are not
|
||||
// emitted as intercept arms here because no current
|
||||
// bench fixture or example reaches them; the polymorphic
|
||||
// body via `compare__Bool` remains correct, just slightly
|
||||
// slower per call. The next fixture that needs Bool-ordered
|
||||
// perf can extend this family symmetrically.
|
||||
"lt__Int" => self.emit_direct_int_icmp_intercept("lt__Int", "icmp slt i64", param_tys, ret_ty),
|
||||
"le__Int" => self.emit_direct_int_icmp_intercept("le__Int", "icmp sle i64", param_tys, ret_ty),
|
||||
"gt__Int" => self.emit_direct_int_icmp_intercept("gt__Int", "icmp sgt i64", param_tys, ret_ty),
|
||||
"ge__Int" => self.emit_direct_int_icmp_intercept("ge__Int", "icmp sge i64", param_tys, ret_ty),
|
||||
"ne__Int" => self.emit_direct_int_icmp_intercept("ne__Int", "icmp ne i64", param_tys, ret_ty),
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a direct `<icmp_op> i64` body for the `lt__Int` /
|
||||
/// `le__Int` / `gt__Int` / `ge__Int` / `ne__Int` intercept arms.
|
||||
/// All five share the same shape: pull the two operand SSAs from
|
||||
/// `self.locals` (populated by `emit_fn` before dispatch), emit
|
||||
/// `%r = <icmp_op> %a, %b` and `ret i1 %r`, close the body.
|
||||
/// `icmp_op` is the full instruction-and-operand-type prefix
|
||||
/// (e.g. `"icmp slt i64"`); the operand SSAs are appended.
|
||||
fn emit_direct_int_icmp_intercept(
|
||||
&mut self,
|
||||
fn_name: &str,
|
||||
icmp_op: &str,
|
||||
param_tys: &[String],
|
||||
ret_ty: &str,
|
||||
) -> Result<bool> {
|
||||
if param_tys != ["i64", "i64"] || ret_ty != "i1" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"{fn_name} body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (i64, i64) -> i1)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let r = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {r} = {icmp_op} {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
self.body.push_str(&format!(" ret i1 {r}\n"));
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// emit one arm of the `compare__T` branch ladder.
|
||||
/// Starts a fresh basic block labelled `label`, constructs the
|
||||
/// matching `Ordering` ctor (LT / EQ / GT) via `lower_ctor`, and
|
||||
@@ -2869,99 +3199,16 @@ impl<'a> Emitter<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// lower a `==` call after the two operands have been
|
||||
/// emitted. Dispatches on the resolved AIL type of the arg side
|
||||
/// (both sides have the same type after typecheck). The `_a_ll`
|
||||
/// hint is the LLVM type the lowering produced for `a`; we use
|
||||
/// it as a sanity check against `arg_ty`'s expected LLVM shape.
|
||||
///
|
||||
/// Supported:
|
||||
/// - `Int` → `icmp eq i64`
|
||||
/// - `Bool` → `icmp eq i1`
|
||||
/// - `Str` → `@strcmp` then `icmp eq i32 0`
|
||||
/// - `Unit` → constant `i1 true` (both sides already evaluated
|
||||
/// for any side effects; Unit has a single inhabitant).
|
||||
///
|
||||
/// Rejected with `CodegenError::Internal` for ADT, `Fn`, and any
|
||||
/// other type — those would need either a structural-equality
|
||||
/// scheme (ADT) or a fn-pointer compare (Fn) that the language
|
||||
/// does not yet specify.
|
||||
fn lower_eq(
|
||||
&mut self,
|
||||
arg_ty: &Type,
|
||||
a: &str,
|
||||
b: &str,
|
||||
_a_ll: &str,
|
||||
) -> Result<(String, String)> {
|
||||
match arg_ty {
|
||||
Type::Con { name, .. } => match name.as_str() {
|
||||
"Int" => {
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = icmp eq i64 {a}, {b}\n"
|
||||
));
|
||||
Ok((dst, "i1".into()))
|
||||
}
|
||||
"Bool" => {
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = icmp eq i1 {a}, {b}\n"
|
||||
));
|
||||
Ok((dst, "i1".into()))
|
||||
}
|
||||
"Str" => {
|
||||
// IR-Str pointers now land on the
|
||||
// `len`-field of the packed-struct slab; @strcmp
|
||||
// needs the bytes pointer 8 bytes further on.
|
||||
// Parallel to the `eq__Str` and `compare__Str`
|
||||
// intercepts in `try_emit_primitive_instance_body`.
|
||||
let a_bytes = self.fresh_ssa();
|
||||
let b_bytes = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {a_bytes} = getelementptr inbounds i8, ptr {a}, i64 8\n"
|
||||
));
|
||||
self.body.push_str(&format!(
|
||||
" {b_bytes} = getelementptr inbounds i8, ptr {b}, i64 8\n"
|
||||
));
|
||||
let cmp = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {cmp} = call i32 @strcmp(ptr {a_bytes}, ptr {b_bytes})\n"
|
||||
));
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = icmp eq i32 {cmp}, 0\n"
|
||||
));
|
||||
Ok((dst, "i1".into()))
|
||||
}
|
||||
"Unit" => {
|
||||
// Both sides have already been evaluated above for
|
||||
// any side effects; Unit has a single inhabitant,
|
||||
// so equality is `true` by definition.
|
||||
let _ = a;
|
||||
let _ = b;
|
||||
Ok(("true".into(), "i1".into()))
|
||||
}
|
||||
"Float" => {
|
||||
let dst = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {dst} = fcmp oeq double {a}, {b}\n"
|
||||
));
|
||||
Ok((dst, "i1".into()))
|
||||
}
|
||||
other => Err(CodegenError::Internal(format!(
|
||||
"`==` not supported for type `{other}` \
|
||||
(ADT and user-defined types lack a structural-equality scheme)"
|
||||
))),
|
||||
},
|
||||
Type::Fn { .. } => Err(CodegenError::Internal(
|
||||
"`==` not supported for function types (no canonical fn-pointer equality)".into(),
|
||||
)),
|
||||
other => Err(CodegenError::Internal(format!(
|
||||
"`==` not supported for type `{}`",
|
||||
ailang_core::pretty::type_to_string(other)
|
||||
))),
|
||||
}
|
||||
}
|
||||
// 2026-05-21 operator-routing-eq-ord: `lower_eq` is deleted.
|
||||
// Equality is no longer a direct-emit codegen path; it flows
|
||||
// through the class method `eq` (prelude.Eq) whose primitive
|
||||
// instance bodies are emitted by
|
||||
// `try_emit_primitive_instance_body` (Eq Int/Bool/Str/Unit
|
||||
// arms, each lowering to a single `icmp` / call /
|
||||
// constant-`i1`-`1` plus the alwaysinline attribute on the
|
||||
// generated fn definition). The deleted fn handled Int / Bool /
|
||||
// Str / Unit / Float branches plus the ADT/Fn rejection arms;
|
||||
// none of those code paths are now reachable.
|
||||
|
||||
pub(crate) fn fresh_ssa(&mut self) -> String {
|
||||
self.counter += 1;
|
||||
@@ -3327,119 +3574,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// codegen rejects `==` on ADT-typed args with a clear
|
||||
/// error. The typechecker accepts the call (the rigid var of
|
||||
/// `forall a. (a, a) -> Bool` unifies with the ADT type), so the
|
||||
/// rejection has to happen here. The diagnostic must mention the
|
||||
/// `==` symbol and the ADT type name.
|
||||
#[test]
|
||||
fn eq_on_adt_rejected_at_codegen() {
|
||||
// Tiny ADT `data K = Mk` (nullary).
|
||||
let mk = Term::Ctor {
|
||||
type_name: "K".into(),
|
||||
ctor: "Mk".into(),
|
||||
args: vec![],
|
||||
};
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![
|
||||
Def::Type(TypeDef {
|
||||
name: "K".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![Ctor {
|
||||
name: "Mk".into(),
|
||||
fields: vec![],
|
||||
}],
|
||||
doc: None,
|
||||
drop_iterative: false,
|
||||
}),
|
||||
Def::Fn(FnDef {
|
||||
name: "main".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::unit()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec![],
|
||||
body: Term::Let {
|
||||
name: "_b".into(),
|
||||
value: Box::new(Term::App {
|
||||
callee: Box::new(Term::Var { name: "==".into() }),
|
||||
args: vec![mk.clone(), mk],
|
||||
tail: false,
|
||||
}),
|
||||
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
export: None,
|
||||
}),
|
||||
],
|
||||
};
|
||||
let err = emit_ir(&m).expect_err(
|
||||
"`==` on ADT must be rejected at codegen; emit_ir succeeded",
|
||||
);
|
||||
let msg = format!("{err:?}");
|
||||
assert!(
|
||||
msg.contains("==") && msg.contains("not supported"),
|
||||
"expected error mentioning `==` not supported; got: {msg}"
|
||||
);
|
||||
}
|
||||
|
||||
/// same negative-path guard for function-typed args.
|
||||
/// `==` on `Fn` is rejected with a "not supported for function
|
||||
/// types" message.
|
||||
#[test]
|
||||
fn eq_on_fn_rejected_at_codegen() {
|
||||
// `let f = main in (== f f)` — `main` is in scope as a fn-value.
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![Def::Fn(FnDef {
|
||||
name: "main".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::unit()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec![],
|
||||
body: Term::Let {
|
||||
name: "f".into(),
|
||||
value: Box::new(Term::Var { name: "main".into() }),
|
||||
body: Box::new(Term::Let {
|
||||
name: "_b".into(),
|
||||
value: Box::new(Term::App {
|
||||
callee: Box::new(Term::Var { name: "==".into() }),
|
||||
args: vec![
|
||||
Term::Var { name: "f".into() },
|
||||
Term::Var { name: "f".into() },
|
||||
],
|
||||
tail: false,
|
||||
}),
|
||||
body: Box::new(Term::Lit { lit: Literal::Unit }),
|
||||
}),
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
export: None,
|
||||
})],
|
||||
};
|
||||
let err = emit_ir(&m).expect_err(
|
||||
"`==` on Fn must be rejected at codegen; emit_ir succeeded",
|
||||
);
|
||||
let msg = format!("{err:?}");
|
||||
assert!(
|
||||
msg.contains("==") && msg.contains("function"),
|
||||
"expected error mentioning `==` and function types; got: {msg}"
|
||||
);
|
||||
}
|
||||
// 2026-05-21 operator-routing-eq-ord: the two
|
||||
// `eq_on_adt_rejected_at_codegen` / `eq_on_fn_rejected_at_codegen`
|
||||
// tests pinned the `lower_eq` direct-emit rejection path for ADT /
|
||||
// Fn args. With `lower_eq` deleted in Task 7 and `==` no longer a
|
||||
// builtin, the rejection path is gone by structure: `(app eq ADT
|
||||
// ADT)` is rejected at typecheck as `NoInstance Eq <ADT>` (the
|
||||
// user did not provide an instance), and `(app eq Fn Fn)` is
|
||||
// similarly rejected. Coverage of the ADT no-instance case lives
|
||||
// in `eq_ord_e2e.rs` (negative path on a missing user instance).
|
||||
|
||||
#[test]
|
||||
fn missing_entry_main_is_error() {
|
||||
@@ -3676,73 +3819,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Floats iter 4.3 RED: `(< 1.5 2.5)` lowers as `fcmp olt double`;
|
||||
/// `(!= 1.5 1.5)` lowers as `fcmp UNE double` (NOT `one`); `(== 1.5
|
||||
/// 1.5)` lowers as `fcmp oeq double`. Int regressions still emit
|
||||
/// `icmp slt i64` / `icmp ne i64` / `icmp eq i64`.
|
||||
#[test]
|
||||
fn lowers_float_comparison_dispatched() {
|
||||
use ailang_core::ast::*;
|
||||
fn fn_def(name: &str, body: Term) -> Def {
|
||||
Def::Fn(FnDef {
|
||||
name: name.into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::bool_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec![],
|
||||
body,
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
export: None,
|
||||
})
|
||||
}
|
||||
fn cmp(op: &str, a: Term, b: Term) -> Term {
|
||||
Term::App {
|
||||
callee: Box::new(Term::Var { name: op.into() }),
|
||||
args: vec![a, b],
|
||||
tail: false,
|
||||
}
|
||||
}
|
||||
let f1 = Term::Lit { lit: Literal::Float { bits: 0x3ff8_0000_0000_0000u64 } };
|
||||
let f2 = Term::Lit { lit: Literal::Float { bits: 0x4004_0000_0000_0000u64 } };
|
||||
let i1 = Term::Lit { lit: Literal::Int { value: 1 } };
|
||||
let i2 = Term::Lit { lit: Literal::Int { value: 2 } };
|
||||
let main_def = Def::Fn(FnDef {
|
||||
name: "main".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![], ret: Box::new(Type::unit()), effects: vec![],
|
||||
param_modes: vec![], ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec![], body: Term::Lit { lit: Literal::Unit },
|
||||
suppress: vec![], doc: None,
|
||||
export: None,
|
||||
});
|
||||
let m = Module {
|
||||
schema: ailang_core::SCHEMA.to_string(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![
|
||||
fn_def("flt_f", cmp("<", f1.clone(), f2.clone())),
|
||||
fn_def("fne_f", cmp("!=", f1.clone(), f1.clone())),
|
||||
fn_def("feq_f", cmp("==", f1.clone(), f1.clone())),
|
||||
fn_def("flt_i", cmp("<", i1.clone(), i2.clone())),
|
||||
fn_def("fne_i", cmp("!=", i1.clone(), i2.clone())),
|
||||
fn_def("feq_i", cmp("==", i1.clone(), i2.clone())),
|
||||
main_def,
|
||||
],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(ir.contains("fcmp olt double"), "missing `fcmp olt double`: {ir}");
|
||||
assert!(ir.contains("fcmp une double"), "missing `fcmp une double` (note: `une` not `one`): {ir}");
|
||||
assert!(ir.contains("fcmp oeq double"), "missing `fcmp oeq double`: {ir}");
|
||||
assert!(ir.contains("icmp slt i64"), "Int `<` regressed: {ir}");
|
||||
assert!(ir.contains("icmp ne i64"), "Int `!=` regressed: {ir}");
|
||||
assert!(ir.contains("icmp eq i64"), "Int `==` regressed: {ir}");
|
||||
}
|
||||
// 2026-05-21 operator-routing-eq-ord: the
|
||||
// `lowers_float_comparison_dispatched` test pinned the direct-emit
|
||||
// `builtin_binop_typed` comparator arms for `==`/`!=`/`<` over
|
||||
// Int and Float. Those arms are deleted in Task 7 (comparison is
|
||||
// now class-method dispatch for Int/Bool/Str via prelude.Eq/Ord
|
||||
// and named-fn dispatch for Float via float_eq / float_lt / etc.);
|
||||
// the test's premise is gone. Replacement coverage of the new
|
||||
// IR shape lives in the intercept-arm machinery exercised by
|
||||
// `prelude_eq_int_carries_alwaysinline_attribute_in_emitted_ir`
|
||||
// (Eq Int → icmp eq + alwaysinline) and indirectly via the
|
||||
// workspace e2e tests for the Float-named-fn surface.
|
||||
|
||||
/// Floats iter 4.4 RED: four new fn-builtins lower to the spec'd
|
||||
/// LLVM ops. `neg` polymorphic dispatches to `sub i64 0, %x` for
|
||||
@@ -4325,75 +4412,15 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// the `==` operator on `Str` lowers via an inline
|
||||
/// `@strcmp` call (separate from the `eq__Str` instance-method
|
||||
/// intercept used by the dictionary path). Both operand pointers
|
||||
/// must be `+8` GEP'd to land on the bytes pointer before
|
||||
/// `@strcmp`, symmetric to the `eq__Str` and `compare__Str`
|
||||
/// fixes. Without this, `==` on Str literals reads the 8-byte
|
||||
/// `len`-field as bytes and never finds the NUL terminator
|
||||
/// within the expected range, breaking string equality.
|
||||
#[test]
|
||||
fn lower_eq_str_calls_strcmp_with_bytes_pointer() {
|
||||
let m = Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "t".into(),
|
||||
imports: vec![],
|
||||
defs: vec![
|
||||
Def::Fn(FnDef {
|
||||
name: "test".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::bool_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec![],
|
||||
body: Term::App {
|
||||
callee: Box::new(Term::Var { name: "==".into() }),
|
||||
args: vec![
|
||||
Term::Lit { lit: Literal::Str { value: "a".into() } },
|
||||
Term::Lit { lit: Literal::Str { value: "b".into() } },
|
||||
],
|
||||
tail: false,
|
||||
},
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
export: None,
|
||||
}),
|
||||
Def::Fn(FnDef {
|
||||
name: "main".into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![],
|
||||
ret: Box::new(Type::unit()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec![],
|
||||
body: Term::Lit { lit: Literal::Unit },
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
export: None,
|
||||
}),
|
||||
],
|
||||
};
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
let body_idx = ir.find("define i1 @ail_t_test").expect("test body");
|
||||
let body = &ir[body_idx..];
|
||||
let cmp_idx = body.find("@strcmp(").expect("@strcmp call present");
|
||||
let before_cmp = &body[..cmp_idx];
|
||||
let gep_count = before_cmp.matches("getelementptr inbounds i8, ptr ").count();
|
||||
assert_eq!(
|
||||
gep_count, 2,
|
||||
"expected 2 +8 GEPs before @strcmp (one per operand); ir body was:\n{body}"
|
||||
);
|
||||
assert!(
|
||||
before_cmp.matches(", i64 8").count() >= 2,
|
||||
"expected both GEPs to be `, i64 8`; ir body was:\n{body}"
|
||||
);
|
||||
}
|
||||
// 2026-05-21 operator-routing-eq-ord: the
|
||||
// `lower_eq_str_calls_strcmp_with_bytes_pointer` test pinned the
|
||||
// `lower_eq` direct-emit Str path that used an inline `@strcmp`
|
||||
// call. With `lower_eq` deleted in Task 7 and `==` no longer a
|
||||
// builtin, the only Str-equality path is class-method dispatch
|
||||
// via prelude.Eq.eq, which the intercept lowers via the existing
|
||||
// `eq__Str` arm (call to `@ail_str_eq`, not `@strcmp`). That
|
||||
// path's GEP-+8 / bytes-pointer correctness is pinned by
|
||||
// `eq_str_mono_symbol_emits_ail_str_eq_call` below.
|
||||
|
||||
/// a `Term::App` calling `int_to_str` lowers to
|
||||
/// `call ptr @ailang_int_to_str(i64 %a)`. Pins the new builtin's
|
||||
|
||||
@@ -79,20 +79,9 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
|
||||
ret_mode: ParamMode::Implicit,
|
||||
}),
|
||||
};
|
||||
let poly_a_a_to_bool = || Type::Forall {
|
||||
vars: vec!["a".into()],
|
||||
constraints: vec![],
|
||||
body: Box::new(Type::Fn {
|
||||
params: vec![
|
||||
Type::Var { name: "a".into() },
|
||||
Type::Var { name: "a".into() },
|
||||
],
|
||||
ret: Box::new(Type::bool_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
}),
|
||||
};
|
||||
// 2026-05-21 operator-routing-eq-ord: `poly_a_a_to_bool` was
|
||||
// shared by the six comparator builtins. With those names
|
||||
// deleted from the surface, the helper is dead.
|
||||
let int_int_int = || Type::Fn {
|
||||
params: vec![Type::int(), Type::int()],
|
||||
ret: Box::new(Type::int()),
|
||||
@@ -103,26 +92,13 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
|
||||
Some(match name {
|
||||
"+" | "-" | "*" | "/" => poly_a_a_to_a(),
|
||||
"%" => int_int_int(),
|
||||
"!=" | "<" | "<=" | ">" | ">=" => poly_a_a_to_bool(),
|
||||
// `==` is polymorphic — `forall a. (a, a) -> Bool`.
|
||||
// The mono pipeline asks `synth_arg_type` for the actual arg
|
||||
// types at the call site; `lower_app` then dispatches to the
|
||||
// right LLVM instruction (icmp eq i64 / i1, @strcmp, or
|
||||
// constant i1 1) on those resolved types.
|
||||
"==" => Type::Forall {
|
||||
vars: vec!["a".into()],
|
||||
constraints: vec![],
|
||||
body: Box::new(Type::Fn {
|
||||
params: vec![
|
||||
Type::Var { name: "a".into() },
|
||||
Type::Var { name: "a".into() },
|
||||
],
|
||||
ret: Box::new(Type::bool_()),
|
||||
effects: vec![],
|
||||
param_modes: vec![],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
}),
|
||||
},
|
||||
// 2026-05-21 operator-routing-eq-ord: the six comparator
|
||||
// names (`==` / `!=` / `<` / `<=` / `>` / `>=`) are no
|
||||
// longer surface identifiers. Equality / ordering route
|
||||
// through the class methods `eq` / `compare` (prelude.Eq /
|
||||
// Ord); Float comparison routes through the named fns
|
||||
// `float_eq` / `float_lt` / etc. Neither path needs an
|
||||
// entry in this codegen-side mirror table.
|
||||
"not" => Type::Fn {
|
||||
params: vec![Type::bool_()],
|
||||
ret: Box::new(Type::bool_()),
|
||||
@@ -237,10 +213,14 @@ pub(crate) fn builtin_effect_op_ret(op: &str) -> Option<Type> {
|
||||
/// longer has to second-guess which arm fired.
|
||||
///
|
||||
/// Comparison-op Int arms are kept here in iter 4.2 to preserve
|
||||
/// the no-regression invariant — pre-iter-4 codegen routed
|
||||
/// `<`/`<=`/`>`/`>=`/`!=` through the same Int-only `builtin_binop`
|
||||
/// table. Iter 4.3 adds the Float arms (`("fcmp olt", "double", "i1")`
|
||||
/// and friends).
|
||||
/// `builtin_binop_typed` is now arithmetic-only. Comparators
|
||||
/// (`==` / `!=` / `<` / `<=` / `>` / `>=`) were removed in iter
|
||||
/// operator-routing-eq-ord.1 — surface comparison routes through
|
||||
/// the prelude.Eq / Ord class-method dispatch, with primitive
|
||||
/// instance bodies emitted via `try_emit_primitive_instance_body`
|
||||
/// in lib.rs (Eq Int / Bool / Str / Unit → `icmp`; Ord Int / Bool
|
||||
/// / Str → `icmp` + Ordering ctor; float_eq / float_ne / float_lt
|
||||
/// / float_le / float_gt / float_ge → `fcmp`).
|
||||
pub(crate) fn builtin_binop_typed(
|
||||
name: &str,
|
||||
arg_ty: &Type,
|
||||
@@ -257,23 +237,6 @@ pub(crate) fn builtin_binop_typed(
|
||||
("/", true, _) => Some(("sdiv", "i64", "i64")),
|
||||
("/", _, true) => Some(("fdiv", "double", "double")),
|
||||
("%", true, _) => Some(("srem", "i64", "i64")),
|
||||
// Comparison ops: operand types differ, result is always `i1`.
|
||||
// Int-arm comparisons preserved from pre-iter-4 `builtin_binop`;
|
||||
// Float arms land in iter 4.3.
|
||||
("!=", true, _) => Some(("icmp ne", "i64", "i1")),
|
||||
("<", true, _) => Some(("icmp slt", "i64", "i1")),
|
||||
("<=", true, _) => Some(("icmp sle", "i64", "i1")),
|
||||
(">", true, _) => Some(("icmp sgt", "i64", "i1")),
|
||||
(">=", true, _) => Some(("icmp sge", "i64", "i1")),
|
||||
// Float comparison arms (Floats iter 4.3). Note: `!=` Float
|
||||
// uses `fcmp une` ("unordered or not equal") — NOT `one`
|
||||
// ("ordered and not equal"), which would return `false` for
|
||||
// `nan != nan` and violate IEEE / spec A5.
|
||||
("!=", _, true) => Some(("fcmp une", "double", "i1")),
|
||||
("<", _, true) => Some(("fcmp olt", "double", "i1")),
|
||||
("<=", _, true) => Some(("fcmp ole", "double", "i1")),
|
||||
(">", _, true) => Some(("fcmp ogt", "double", "i1")),
|
||||
(">=", _, true) => Some(("fcmp oge", "double", "i1")),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user