//! mir.1a: `lower_to_mir` fills `ty` on every node from the canonical //! `synth`, and routes string literals to `MTerm::Str { rep: Static }`. //! Loads the boundary witnesses as workspace fixtures (prelude + //! kernel injected by `load_workspace`) and elaborates the whole //! workspace, so the walk is exercised over the full prelude. These //! pins protect the producer; codegen consumption arrives in mir.1b. use ailang_check::elaborate_workspace; use ailang_mir::{Callee, MTerm, MirWorkspace, Mode, StrRep}; use ailang_surface::load_workspace; use ailang_test_support::examples_dir; /// Load a witness fixture (prelude injected) and elaborate it to MIR. fn elaborate_fixture(module: &str) -> MirWorkspace { let entry = examples_dir().join(format!("{module}.ail")); let ws = load_workspace(&entry).expect("fixture loads (prelude injected)"); elaborate_workspace(&ws).expect("witness elaborates to MIR") } /// A def's lowered body in the elaborated workspace. fn body<'a>(mir: &'a MirWorkspace, module: &str, def: &str) -> &'a MTerm { &mir.modules[module] .defs .iter() .find(|d| d.name == def) .expect("def present") .body } #[test] fn loop_seed_str_literal_lowers_to_heap_str_node() { // #49 witness: the loop seed "x" is a Str literal → MTerm::Str. // mir.4: a loop-carried seed is promoted to rep = Heap so codegen // emits the str_clone heap promotion and the recur superseded-value // dec is sound (the prior alloca value is always an owned slab). let m = "loop_recur_str_binder_no_leak_pin"; let mir = elaborate_fixture(m); assert_eq!( loop_seed_rep(body(&mir, m, "main")), Some(StrRep::Heap), "loop seed \"x\" must lower to MTerm::Str {{ rep: Heap }}" ); } #[test] fn loop_recur_literal_arg_lowers_to_heap_str_node() { // mir.4: a literal recur arg at a Str-binder position is promoted // to rep = Heap, mirroring the seed promotion — `(recur "reset" …)`. let m = "loop_str_recur_literal_no_leak_pin"; let mir = elaborate_fixture(m); assert!( find_recur_str_arg_with_rep(body(&mir, m, "main"), StrRep::Heap), "the recur arg literal \"reset\" must lower to MTerm::Str {{ rep: Heap }}" ); } #[test] fn non_loop_str_literal_stays_static() { // Control: a Str literal NOT in a loop-carried position keeps // rep = Static (no spurious heap promotion). `hello` prints the // literal "Hello, AILang." via io/print_str. let m = "hello"; let mir = elaborate_fixture(m); assert!( find_str_node_with_rep(body(&mir, m, "main"), StrRep::Static), "a non-loop Str literal must stay MTerm::Str {{ rep: Static }}" ); } #[test] fn loop_exit_arm_str_literal_lowers_to_heap_str_node() { // mir.4 loop-result leg: a Str literal in a loop exit (tail) arm is // promoted to rep = Heap, so the loop returns an owned heap slab the // caller's scope-close can safely free. `loop_str_static_exit` exits // with the literal "result"; it is the only Str literal in the body. let m = "loop_str_static_exit_no_leak_pin"; let mir = elaborate_fixture(m); assert!( find_str_node_with_rep(body(&mir, m, "main"), StrRep::Heap), "the loop exit-arm literal \"result\" must lower to MTerm::Str {{ rep: Heap }}" ); assert!( !find_str_node_with_rep(body(&mir, m, "main"), StrRep::Static), "no Str literal in the static-exit fixture should remain Static" ); } #[test] fn new_over_user_adt_carries_node_types() { // #53 witness: every node has a filled `ty`; the lowered // `(new Counter 42)` node's type is the user ADT `Counter`. // // A *monomorphic* `new` (no leading type-arg) is desugared to a // type-scoped call `(app Counter.new 42)` before lower_to_mir runs // (desugar.rs:1083-1098) — so the let-init lowers to `MTerm::App`, // not `MTerm::New`. `MTerm::New` survives only for a *polymorphic* // `new` carrying a written `NewArg::Type` (the #51 RawBuf case). // What this pin protects is the ty-fill: the lowered init node // carries the checker-proved result type `Counter`. let m = "new_counter_user_adt"; let mir = elaborate_fixture(m); let MTerm::Let { init, .. } = body(&mir, m, "main") else { panic!("main body is a let"); }; let ty_str = ailang_core::pretty::type_to_string(&init.ty()); assert!( ty_str.contains("Counter"), "lowered (new Counter 42) init node ty is Counter, got {ty_str}" ); // mir.2: the `(new Counter 42)` desugars to `App{callee: // Var{"Counter.new"}}`; lower_to_mir must resolve it to a // `Callee::Static` whose module is `Counter`'s home (its own // module, spelled bare per the own-module-types-stay-bare rule), // NOT leave it `Indirect` for codegen's deleted ladder to resolve. let MTerm::App { callee, .. } = init.as_ref() else { panic!("let init is the (new Counter 42) App node"); }; match callee { ailang_mir::Callee::Static { module, fn_name, .. } => { assert_eq!(module, "new_counter_user_adt", "Counter.new home module"); assert_eq!(fn_name, "new", "Counter.new fn_name"); } other => panic!("expected Callee::Static for Counter.new, got {other:?}"), } } #[test] fn rawbuf_size_only_elaborates() { // #51 witness: a RawBuf read only for size — must elaborate clean // (the element type carried only by the author's annotation does // not block lowering). let mir = elaborate_fixture("new_rawbuf_size_only"); assert!(mir.modules.contains_key("new_rawbuf_size_only")); } #[test] fn poly_free_fn_accumulating_into_own_adt_elaborates() { // Producer pin (mono ↔ lower_to_mir boundary): a polymorphic free // fn whose accumulator type-arg instantiates to the *defining // module's own* ADT must elaborate. `std_list.fold_left` is // `forall a b. ((b, a) -> b, b, List) -> b`; `List_reverse` // calls it with `b := List` — the accumulator is std_list's // OWN `List`. mono normalises the observed type-arg to the // registry-canonical qualified form (`std_list.List`) for dedup + // symbol naming, but that qualified form must be localised back to // the module's bare convention before it is substituted into the // (own-bare) polymorphic signature and body. Without the // localisation the synthesised specialisation is self-inconsistent // — a qualified-`std_list.List` accumulator parameter against a // bare-`List` list argument and Lam body — and the post-mono // `synth` re-entry in `lower_to_mir` rejects it with // `expected std_list.List, got List`. let mir = elaborate_fixture("std_list_demo"); assert!(mir.modules.contains_key("std_list")); } #[test] fn callee_classification_builtin_and_static() { // mir.2: `lower_to_mir::classify_callee` resolves each App callee // against check's own synth ladder, so codegen reads the resolved // identity off MIR. This pins two of the three legs from the // `classify_pin` witness: an arithmetic op (`+`) is a `Builtin` // (no owning module), a bare same-module user fn (`bump`) is a // `Static` carrying its own module name. The `Indirect` leg (a // fn-typed local) is covered by the existing closure e2e tests. let m = "classify_pin"; let mir = elaborate_fixture(m); // `bump`'s body is `(app + n 1)` → the `+` callee is a Builtin. match body(&mir, m, "bump") { MTerm::App { callee: Callee::Builtin { name, .. }, .. } => { assert_eq!(name, "+", "arithmetic op classified as Builtin"); } other => panic!("expected Builtin `+`, got {other:?}"), } // `main`'s body is `(app bump 41)` → `bump` is a same-module user // fn → Static{module: "classify_pin", fn_name: "bump"}. match body(&mir, m, "main") { MTerm::App { callee: Callee::Static { module, fn_name, .. }, .. } => { assert_eq!(module, "classify_pin", "own-module callee module"); assert_eq!(fn_name, "bump", "own-module callee fn_name"); } other => panic!("expected Static `bump`, got {other:?}"), } } #[test] fn mirdef_consume_is_populated_for_let_binder() { // mir.3a: lower_to_mir runs the uniqueness pass and attaches the // per-binder consume_count to MirDef.consume. `main`'s `(let c …)` // binder must appear in the map (the exact count is validated // end-to-end by the RC-stats leak pins; this pin guards that the // producer fills the field at all, so codegen has it to read). let ws = elaborate_fixture("new_counter_user_adt"); let m = ws.modules.get("new_counter_user_adt").expect("module"); let main = m.defs.iter().find(|d| d.name == "main").expect("main"); assert!( main.consume.contains_key("c"), "MirDef.consume must carry the `c` let-binder, got {:?}", main.consume, ); } #[test] fn app_arg_carries_callee_borrow_mode() { // mir.3b: lower_to_mir fills MArg.mode from the callee's param_modes. // In `(app RawBuf.get 0)`, RawBuf.get's param 0 is a // borrow receiver, so the outer App's arg 0 must be Mode::Borrow — // the value codegen's anon-temp drop gate now reads off MIR. let mir = elaborate_fixture("raw_buf_drop_min"); // main is `(app print (let buf (new RawBuf …) // (app RawBuf.get (app RawBuf.set …) 0)))`, so the RawBuf.get App // is nested under the print App's arg and the let body. Mono mangles // `RawBuf.get` to `RawBuf_get__Int`. let outer = find_app_with_fn(body(&mir, "raw_buf_drop_min", "main"), "RawBuf_get") .expect("RawBuf.get App node"); let MTerm::App { args, .. } = outer else { unreachable!("find_app_with_fn returns an App"); }; assert!( matches!(args[0].mode, Mode::Borrow), "RawBuf.get arg 0 (borrow receiver) must be Mode::Borrow, got {:?}", args[0].mode, ); } /// First `MTerm::App` whose resolved callee (`Static`/`Builtin`) names a /// fn equal to (or a monomorphised specialisation of) `fn_name`, /// searching App args and Let init/body. The fixture's `RawBuf.get` /// callee may be mangled by mono (e.g. `get__Int`), so the match is on /// the fn-name stem. fn find_app_with_fn<'a>(t: &'a MTerm, fn_name: &str) -> Option<&'a MTerm> { let names = match t { MTerm::App { callee: Callee::Static { fn_name: n, .. }, .. } => Some(n.as_str()), MTerm::App { callee: Callee::Builtin { name: n, .. }, .. } => Some(n.as_str()), _ => None, }; if let Some(n) = names { if n == fn_name || n.starts_with(&format!("{fn_name}__")) { return Some(t); } } match t { MTerm::App { args, .. } => args.iter().find_map(|a| find_app_with_fn(&a.term, fn_name)), MTerm::Let { init, body, .. } => { find_app_with_fn(init, fn_name).or_else(|| find_app_with_fn(body, fn_name)) } _ => None, } } /// The rep of the first loop binder seed that is an `MTerm::Str`, /// searching `Let`-init / `Let`-body / the loop's own binders. fn loop_seed_rep(t: &MTerm) -> Option { match t { MTerm::Loop { binders, body, .. } => binders .iter() .find_map(|b| match &b.init { MTerm::Str { rep, .. } => Some(*rep), _ => None, }) .or_else(|| loop_seed_rep(body)), MTerm::Let { init, body, .. } => { loop_seed_rep(init).or_else(|| loop_seed_rep(body)) } _ => None, } } /// True if any `Recur` arg in `t` is an `MTerm::Str` with the given rep. fn find_recur_str_arg_with_rep(t: &MTerm, want: StrRep) -> bool { match t { MTerm::Recur { args, .. } => args.iter().any(|a| matches!( &a.term, MTerm::Str { rep, .. } if *rep == want )), MTerm::Loop { binders, body, .. } => { binders.iter().any(|b| find_recur_str_arg_with_rep(&b.init, want)) || find_recur_str_arg_with_rep(body, want) } MTerm::Let { init, body, .. } => { find_recur_str_arg_with_rep(init, want) || find_recur_str_arg_with_rep(body, want) } MTerm::If { cond, then, else_, .. } => { find_recur_str_arg_with_rep(cond, want) || find_recur_str_arg_with_rep(then, want) || find_recur_str_arg_with_rep(else_, want) } _ => false, } } /// True if any `MTerm::Str` reachable in `t` carries the given rep /// (used by the non-loop control: a plain literal stays Static). fn find_str_node_with_rep(t: &MTerm, want: StrRep) -> bool { match t { MTerm::Str { rep, .. } => *rep == want, MTerm::Let { init, body, .. } => { find_str_node_with_rep(init, want) || find_str_node_with_rep(body, want) } MTerm::App { args, .. } => { args.iter().any(|a| find_str_node_with_rep(&a.term, want)) } MTerm::Do { args, .. } => { args.iter().any(|a| find_str_node_with_rep(&a.term, want)) } MTerm::If { cond, then, else_, .. } => { find_str_node_with_rep(cond, want) || find_str_node_with_rep(then, want) || find_str_node_with_rep(else_, want) } MTerm::Loop { binders, body, .. } => { binders.iter().any(|b| find_str_node_with_rep(&b.init, want)) || find_str_node_with_rep(body, want) } _ => false, } }