iter 23.3.2: codegen — three compare__T intercept arms + emit_ordering_arm helper
This commit is contained in:
@@ -2458,12 +2458,7 @@ impl<'a> Emitter<'a> {
|
||||
|
||||
/// Iter 23.2: hand-rolled body for monomorphiser-synthesised
|
||||
/// primitive instance methods whose natural lambda-lowering would
|
||||
/// not produce the spec-mandated IR shape. Currently the only
|
||||
/// inhabitant is `eq__Str`, which must call `@ail_str_eq` (the
|
||||
/// stable AILang-namespaced Str-equality ABI in `runtime/str.c`)
|
||||
/// rather than the `@strcmp + icmp eq i32 0` shape that
|
||||
/// `lower_eq`'s Str arm would emit if we let the lambda body
|
||||
/// `(== x y)` go through the normal path. Returns `Ok(true)` if
|
||||
/// not produce the spec-mandated IR shape. Returns `Ok(true)` if
|
||||
/// the body was emitted (including the closing `}` and a final
|
||||
/// `\n\n`); `emit_fn` skips its normal body-lowering branch but
|
||||
/// MUST still run its post-body steps (deferred-thunk flush and
|
||||
@@ -2471,10 +2466,19 @@ impl<'a> Emitter<'a> {
|
||||
/// top-level fn gets so it is reachable as a `Term::Var` value).
|
||||
/// `Ok(false)` lets `emit_fn` continue with normal body lowering.
|
||||
///
|
||||
/// Future iters add `compare__Str` here (23.3); int/bool primitive
|
||||
/// methods (`eq__Int`, `eq__Bool`, `compare__Int`, `compare__Bool`)
|
||||
/// ride the natural `lower_eq` / `lower_compare` dispatch and do
|
||||
/// not need a hand-rolled body.
|
||||
/// Currently inhabited arms: `eq__Str` (ships in 23.2.2) and
|
||||
/// `compare__Int`, `compare__Bool`, `compare__Str` (this iter
|
||||
/// 23.3). The two eq Int/Bool primitives (`eq__Int`, `eq__Bool`)
|
||||
/// ride the natural `lower_eq` dispatch via their lambda body
|
||||
/// `(== x y)` and do not need a hand-rolled body. The three
|
||||
/// `compare` arms must be hand-rolled because there is no
|
||||
/// type-polymorphic primitive that returns an Ordering ADT
|
||||
/// value (Decision: `builtin_binop_typed` covers Int and Float
|
||||
/// only — Bool is missing — so a surface-level
|
||||
/// `if x < y { LT } else if x == y { EQ } else { GT }` body
|
||||
/// would fail at codegen for `compare__Bool`). Hand-rolling all
|
||||
/// three keeps the family consistent and the IR shape
|
||||
/// predictable.
|
||||
fn try_emit_primitive_instance_body(
|
||||
&mut self,
|
||||
fn_name: &str,
|
||||
@@ -2505,10 +2509,150 @@ impl<'a> Emitter<'a> {
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"compare__Int" => {
|
||||
if param_tys != ["i64", "i64"] || ret_ty != "ptr" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"compare__Int body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (i64, i64) -> ptr)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let lt_test = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {lt_test} = icmp slt i64 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
let id = self.fresh_id();
|
||||
let lt_label = format!("cmp_lt_{id}");
|
||||
let after_lt_label = format!("cmp_after_lt_{id}");
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n"
|
||||
));
|
||||
self.emit_ordering_arm(<_label, "LT")?;
|
||||
self.start_block(&after_lt_label);
|
||||
let eq_test = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {eq_test} = icmp eq i64 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
let eq_label = format!("cmp_eq_{id}");
|
||||
let gt_label = format!("cmp_gt_{id}");
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n"
|
||||
));
|
||||
self.emit_ordering_arm(&eq_label, "EQ")?;
|
||||
self.emit_ordering_arm(>_label, "GT")?;
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"compare__Bool" => {
|
||||
if param_tys != ["i1", "i1"] || ret_ty != "ptr" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"compare__Bool body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (i1, i1) -> ptr)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let lt_test = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {lt_test} = icmp ult i1 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
let id = self.fresh_id();
|
||||
let lt_label = format!("cmp_lt_{id}");
|
||||
let after_lt_label = format!("cmp_after_lt_{id}");
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n"
|
||||
));
|
||||
self.emit_ordering_arm(<_label, "LT")?;
|
||||
self.start_block(&after_lt_label);
|
||||
let eq_test = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {eq_test} = icmp eq i1 {a_ssa}, {b_ssa}\n"
|
||||
));
|
||||
let eq_label = format!("cmp_eq_{id}");
|
||||
let gt_label = format!("cmp_gt_{id}");
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n"
|
||||
));
|
||||
self.emit_ordering_arm(&eq_label, "EQ")?;
|
||||
self.emit_ordering_arm(>_label, "GT")?;
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
"compare__Str" => {
|
||||
if param_tys != ["ptr", "ptr"] || ret_ty != "ptr" {
|
||||
return Err(CodegenError::Internal(format!(
|
||||
"compare__Str body intercept: unexpected signature \
|
||||
({param_tys:?}) -> {ret_ty} (want (ptr, ptr) -> ptr)"
|
||||
)));
|
||||
}
|
||||
let n = self.locals.len();
|
||||
let a_ssa = self.locals[n - 2].1.clone();
|
||||
let b_ssa = self.locals[n - 1].1.clone();
|
||||
let cmp_res = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {cmp_res} = call i32 @ail_str_compare(ptr {a_ssa}, ptr {b_ssa})\n"
|
||||
));
|
||||
let lt_test = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {lt_test} = icmp slt i32 {cmp_res}, 0\n"
|
||||
));
|
||||
let id = self.fresh_id();
|
||||
let lt_label = format!("cmp_lt_{id}");
|
||||
let after_lt_label = format!("cmp_after_lt_{id}");
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {lt_test}, label %{lt_label}, label %{after_lt_label}\n"
|
||||
));
|
||||
self.emit_ordering_arm(<_label, "LT")?;
|
||||
self.start_block(&after_lt_label);
|
||||
let eq_test = self.fresh_ssa();
|
||||
self.body.push_str(&format!(
|
||||
" {eq_test} = icmp eq i32 {cmp_res}, 0\n"
|
||||
));
|
||||
let eq_label = format!("cmp_eq_{id}");
|
||||
let gt_label = format!("cmp_gt_{id}");
|
||||
self.body.push_str(&format!(
|
||||
" br i1 {eq_test}, label %{eq_label}, label %{gt_label}\n"
|
||||
));
|
||||
self.emit_ordering_arm(&eq_label, "EQ")?;
|
||||
self.emit_ordering_arm(>_label, "GT")?;
|
||||
self.body.push_str("}\n\n");
|
||||
self.block_terminated = true;
|
||||
Ok(true)
|
||||
}
|
||||
_ => Ok(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Iter 23.3: 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
|
||||
/// emits a `ret ptr <ssa>`. Used three times per `compare__T`
|
||||
/// arm in `try_emit_primitive_instance_body`. The ctor is a
|
||||
/// zero-field allocation; `lower_ctor` handles alloc-strategy
|
||||
/// variance (Gc / Bump / Rc) without the intercept duplicating
|
||||
/// the per-strategy logic.
|
||||
fn emit_ordering_arm(&mut self, label: &str, ctor: &str) -> Result<()> {
|
||||
self.start_block(label);
|
||||
// term_ptr 0: synthetic call site, not present in the
|
||||
// escape-analysis result; falls back to the conservative
|
||||
// "escapes → heap allocate" default. Safe for the Ordering
|
||||
// return value (the caller owns it after `ret`).
|
||||
let (ssa, _llvm_ty) = self.lower_ctor(
|
||||
"Ordering",
|
||||
ctor,
|
||||
&[],
|
||||
0,
|
||||
)?;
|
||||
self.body.push_str(&format!(" ret ptr {ssa}\n"));
|
||||
self.block_terminated = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Iter 16e: 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`
|
||||
@@ -3696,4 +3840,116 @@ mod tests {
|
||||
"header missing @ail_str_compare declaration; ir was:\n{ir}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 23.3: codegen intercepts a fn named `compare__Int` and
|
||||
/// emits a three-way branch ladder: `icmp slt i64 a, b` →
|
||||
/// LT-block; else `icmp eq i64 a, b` → EQ-block; else GT-block.
|
||||
/// Each block constructs the matching Ordering ctor via the
|
||||
/// existing `lower_ctor` path. Pinned with a synthetic FnDef so
|
||||
/// the test does not depend on prelude auto-injection.
|
||||
#[test]
|
||||
fn compare_int_mono_symbol_emits_branch_ladder() {
|
||||
let m = synth_compare_module("compare__Int", Type::int(), "i64");
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("icmp slt i64"),
|
||||
"compare__Int body must contain `icmp slt i64`; ir:\n{ir}"
|
||||
);
|
||||
assert!(
|
||||
ir.contains("icmp eq i64"),
|
||||
"compare__Int body must contain `icmp eq i64`; ir:\n{ir}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 23.3: same shape for `compare__Bool`. The LT-test uses
|
||||
/// `icmp ult i1` (unsigned: false=0 ult true=1 gives the natural
|
||||
/// Bool ordering false < true); the EQ-test uses `icmp eq i1`.
|
||||
#[test]
|
||||
fn compare_bool_mono_symbol_emits_branch_ladder() {
|
||||
let m = synth_compare_module("compare__Bool", Type::bool_(), "i1");
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("icmp ult i1"),
|
||||
"compare__Bool body must contain `icmp ult i1`; ir:\n{ir}"
|
||||
);
|
||||
assert!(
|
||||
ir.contains("icmp eq i1"),
|
||||
"compare__Bool body must contain `icmp eq i1`; ir:\n{ir}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Iter 23.3: `compare__Str` calls `@ail_str_compare` to get the
|
||||
/// normalised {-1, 0, +1}, then branches: slt 0 → LT, eq 0 → EQ,
|
||||
/// else GT.
|
||||
#[test]
|
||||
fn compare_str_mono_symbol_emits_ail_str_compare_call() {
|
||||
let m = synth_compare_module("compare__Str", Type::str_(), "ptr");
|
||||
let ir = emit_ir(&m).unwrap();
|
||||
assert!(
|
||||
ir.contains("call i32 @ail_str_compare("),
|
||||
"compare__Str body must call @ail_str_compare; ir:\n{ir}"
|
||||
);
|
||||
assert!(
|
||||
ir.contains("icmp slt i32"),
|
||||
"compare__Str body must compare result `slt i32` against 0; ir:\n{ir}"
|
||||
);
|
||||
assert!(
|
||||
ir.contains("icmp eq i32"),
|
||||
"compare__Str body must compare result `eq i32` against 0; ir:\n{ir}"
|
||||
);
|
||||
}
|
||||
|
||||
/// Test helper: minimal two-module workspace where the "prelude"
|
||||
/// module declares `data Ordering = LT | EQ | GT` and an
|
||||
/// instance-fn shell (the intercept overrides the body), and the
|
||||
/// entry module's `main` is a Unit no-op so `emit_ir` returns a
|
||||
/// well-formed program. Used by the three `compare_*` tests above.
|
||||
fn synth_compare_module(fn_name: &str, param_ail_ty: Type, _llvm_param_ty: &str) -> Module {
|
||||
let ordering = Def::Type(TypeDef {
|
||||
name: "Ordering".into(),
|
||||
vars: vec![],
|
||||
ctors: vec![
|
||||
Ctor { name: "LT".into(), fields: vec![] },
|
||||
Ctor { name: "EQ".into(), fields: vec![] },
|
||||
Ctor { name: "GT".into(), fields: vec![] },
|
||||
],
|
||||
doc: None,
|
||||
drop_iterative: false,
|
||||
});
|
||||
// Ordering is `ptr` at the LLVM level (boxed ADT).
|
||||
let compare = Def::Fn(FnDef {
|
||||
name: fn_name.into(),
|
||||
ty: Type::Fn {
|
||||
params: vec![param_ail_ty.clone(), param_ail_ty.clone()],
|
||||
ret: Box::new(Type::Con { name: "Ordering".into(), args: vec![] }),
|
||||
effects: vec![],
|
||||
param_modes: vec![ParamMode::Borrow, ParamMode::Borrow],
|
||||
ret_mode: ParamMode::Implicit,
|
||||
},
|
||||
params: vec!["x".into(), "y".into()],
|
||||
body: Term::Lit { lit: Literal::Unit }, // placeholder; intercept overrides
|
||||
suppress: vec![],
|
||||
doc: None,
|
||||
});
|
||||
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,
|
||||
});
|
||||
Module {
|
||||
schema: SCHEMA.into(),
|
||||
name: "prelude".into(),
|
||||
imports: vec![],
|
||||
defs: vec![ordering, compare, main_def],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user