From c6168ad5d19dc4c77308ac0144f370978b6b6630 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 10 May 2026 22:01:00 +0200 Subject: [PATCH] iter 23.2.2-fixup: emit closure adapter for primitive-instance-bodied fns --- crates/ailang-codegen/src/lib.rs | 77 +++++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 6 deletions(-) diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 9df5754..6431c64 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -1060,13 +1060,20 @@ impl<'a> Emitter<'a> { self.start_block("entry"); // Iter 23.2: primitive-instance body intercept. Returns true if - // the body was emitted in full; we then skip the normal body - // lowering for this fn. - if self.try_emit_primitive_instance_body(&f.name, &llvm_param_tys, &llvm_ret)? { - return Ok(()); - } + // the body was emitted in full (including the closing `}\n\n`). + // When that fires we skip the normal body-lowering block below + // but still fall through to deferred-thunk flush + closure-pair + // emission, so the intercepted fn participates in the same + // post-body machinery as every other top-level fn (iter 23.2.2 + // fixup: previously this short-circuited past + // `emit_adapter_and_static_closure`, leaving the fn without a + // closure-pair symbol — a footgun the moment any caller + // referenced it by value). + let body_was_intercepted = + self.try_emit_primitive_instance_body(&f.name, &llvm_param_tys, &llvm_ret)?; - let (val, val_ty) = self.lower_term(&f.body)?; + if !body_was_intercepted { + let (val, val_ty) = self.lower_term(&f.body)?; if !self.block_terminated { if val_ty != llvm_ret { return Err(CodegenError::Internal(format!( @@ -1185,6 +1192,7 @@ impl<'a> Emitter<'a> { // the function body — no fall-through ret. self.body.push_str("}\n\n"); } + } // close `if !body_was_intercepted` // Iter 8b: flush lambda thunks collected while lowering this fn's // body. They go after the closing `}` of the parent fn, before @@ -3481,4 +3489,61 @@ mod tests { "eq__Str body must call @ail_str_eq; ir was:\n{ir}" ); } + + /// Iter 23.2.2-fixup: every top-level fn — including primitive- + /// instance-bodied ones like `eq__Str` — must flow through + /// `emit_adapter_and_static_closure` so that referencing the fn + /// as a `Term::Var` value (dictionary entry, by-value pass to + /// higher-order code) finds the expected closure-pair symbol. + /// The intercept path used to short-circuit past adapter emission, + /// leaving `eq__Str` as the only top-level fn in the module + /// without an `@ail___adapter` / `@ail___clos` pair. + /// This test pins the invariant. + #[test] + fn eq_str_mono_symbol_emits_closure_adapter_pair() { + let m = Module { + schema: SCHEMA.into(), + name: "prelude".into(), + imports: vec![], + defs: vec![ + Def::Fn(FnDef { + name: "eq__Str".into(), + ty: Type::Fn { + params: vec![Type::str_(), Type::str_()], + ret: Box::new(Type::bool_()), + effects: vec![], + param_modes: vec![ParamMode::Borrow, ParamMode::Borrow], + ret_mode: ParamMode::Implicit, + }, + params: vec!["x".into(), "y".into()], + body: Term::Lit { lit: Literal::Bool { value: false } }, + suppress: vec![], + doc: 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, + }), + ], + }; + let ir = emit_ir(&m).unwrap(); + assert!( + ir.contains("@ail_prelude_eq__Str_adapter"), + "eq__Str must emit a closure adapter; ir was:\n{ir}" + ); + assert!( + ir.contains("@ail_prelude_eq__Str_clos"), + "eq__Str must emit a static closure pair; ir was:\n{ir}" + ); + } }