//! IR-shape pin for milestone 24.3's `print` polymorphic free fn. //! //! Property protected: the post-mono `print__Int` `Def::Fn.body` is //! structurally `Term::Let { name: "s", value: App(show__Int, [x]), //! body: Term::Do { op: "io/print_str", args: [s] } }`. The explicit //! let-binder around `show__Int x` is load-bearing for the heap-Str RC //! discipline (the heap-Str Str carve-out at //! `drop_symbol_for_binder` requires //! a let-binder to attach the rc-dec to). If a future codegen / mono //! refactor inlines the let-binder away, this pin fires and surfaces //! the regression BEFORE the E2E runtime stats produce a confusing //! "memory leak" diagnostic. //! //! Note: `FnDef.body: Term` is the *inner* function body — top-level //! fns carry their parameter list separately on `FnDef.params`, so no //! outer `Term::Lam` wraps the body. This contrasts with instance //! method bodies (which carry a `Term::Lam` because they are anonymous //! function values inside `InstanceMethod.body`). use ailang_core::ast::{Def, Term}; use ailang_surface::load_workspace; use std::path::PathBuf; fn fixture_path() -> PathBuf { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.pop(); d.pop(); d.join("examples").join("show_print_smoke.ail") } #[test] fn print_int_body_preserves_explicit_let_binder() { let ws = load_workspace(&fixture_path()).expect("workspace loads"); let diags = ailang_check::check_workspace(&ws); assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}"); let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green"); let prelude_mod = post_mono .modules .get("prelude") .expect("prelude module present"); let print_int = prelude_mod .defs .iter() .find_map(|d| match d { Def::Fn(f) if f.name == "print__Int" => Some(f), _ => None, }) .expect("print__Int mono symbol not found in prelude module"); // Body shape: Term::Let { name: "s", // value: Term::App(show__Int, [x]), // body: Term::Do { op: "io/print_str", args: [s] } } let (let_value, let_body) = match &print_int.body { Term::Let { name, value, body } => { assert_eq!(name, "s", "let-binder name expected 's', got {name:?}"); (value.as_ref(), body.as_ref()) } other => panic!( "print__Int.body is not Term::Let — let-binder was optimised away or never inserted. Got: {other:?}" ), }; // Inner App should reference show__Int (post-mono symbol). let app_fn = match let_value { Term::App { callee, .. } => callee.as_ref(), other => panic!("print__Int let-value is not Term::App, got: {other:?}"), }; let app_fn_name = match app_fn { Term::Var { name } => name, other => panic!("let-value's fn position is not Term::Var, got: {other:?}"), }; assert_eq!( app_fn_name, "show__Int", "post-mono let-value should call show__Int, got: {app_fn_name}" ); // Inner body should be a Do invoking io/print_str. match let_body { Term::Do { op, args, .. } => { assert_eq!(op, "io/print_str", "let-body Do op expected 'io/print_str', got {op:?}"); assert_eq!(args.len(), 1, "io/print_str expects 1 arg, got {}", args.len()); } other => panic!("print__Int let-body is not Term::Do, got: {other:?}"), } }