//! Tests for the unified mono pass over polymorphic free fns (iter 23.4). //! //! These tests assert workspace-level invariants AFTER //! `monomorphise_workspace` has run: synthesised mono `Def::Fn`s for //! both class-method residuals AND polymorphic free-fn calls live in //! the post-mono workspace. use ailang_check::mono::{mono_target_key, MonoTarget}; use ailang_core::ast::{Def, Type}; use ailang_core::workspace::load_workspace; use std::path::PathBuf; fn fixture(name: &str) -> PathBuf { let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); d.pop(); d.pop(); d.join("examples").join(name) } #[test] fn cmp_max_smoke_produces_both_mono_symbols_in_one_fixpoint() { let ws = load_workspace(&fixture("cmp_max_smoke.ail.json")) .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"); // Free-fn mono symbols are appended to their owner module // (where the polymorphic Def::Fn lives) — for cmp_max, that is // `cmp_max_smoke`. Class-method mono symbols (compare__Int) are // appended to the instance's defining module (`prelude`). let main_mod = post_mono .modules .get("cmp_max_smoke") .expect("main module present"); let prelude_mod = post_mono .modules .get("prelude") .expect("prelude module present"); let main_names: Vec<&str> = main_mod .defs .iter() .filter_map(|d| if let Def::Fn(f) = d { Some(f.name.as_str()) } else { None }) .collect(); let prelude_names: Vec<&str> = prelude_mod .defs .iter() .filter_map(|d| if let Def::Fn(f) = d { Some(f.name.as_str()) } else { None }) .collect(); assert!( main_names.contains(&"cmp_max__Int"), "post-mono workspace must contain free-fn mono symbol cmp_max__Int in module cmp_max_smoke; got {main_names:?}" ); assert!( prelude_names.contains(&"compare__Int"), "post-mono workspace must contain class-method mono symbol compare__Int in prelude (closed transitively from cmp_max__Int body); got {prelude_names:?}" ); } #[test] fn collect_mono_targets_emits_free_fn_target_for_cmp_max_at_int() { use ailang_check::mono::collect_mono_targets; let ws = load_workspace(&fixture("cmp_max_smoke.ail.json")).expect("workspace loads"); let diags = ailang_check::check_workspace(&ws); assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}"); let env = ailang_check::build_check_env(&ws); let main_mod = ws.modules.get("cmp_max_smoke").expect("main module"); let main_fn = main_mod.defs.iter().find_map(|d| { if let Def::Fn(f) = d { (f.name == "main").then_some(f) } else { None } }).expect("main fn"); let targets = collect_mono_targets(main_fn, "cmp_max_smoke", &env) .expect("collector runs"); let has_free_fn_target = targets.iter().any(|t| matches!( t, MonoTarget::FreeFn { name, type_args, .. } if name == "cmp_max" && type_args.len() == 1 && matches!(&type_args[0], Type::Con { name, args } if name == "Int" && args.is_empty()) )); assert!( has_free_fn_target, "expected MonoTarget::FreeFn {{ name: \"cmp_max\", type_args: [Int] }}; got {targets:?}" ); } #[test] fn cmp_max_smoke_runs_end_to_end() { // Compile + run the cmp_max_smoke fixture, prove it prints 7 // (cmp_max(3, 7) at Int via Ord Int). use std::process::Command; let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let workspace = manifest_dir.parent().unwrap().parent().unwrap(); let src = workspace.join("examples").join("cmp_max_smoke.ail.json"); let tmp = std::env::temp_dir().join(format!( "ailang_cmp_max_smoke_{}", std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let out = tmp.join("bin"); let status = Command::new(env!("CARGO_BIN_EXE_ail")) .args(["build", src.to_str().unwrap(), "-o"]) .arg(&out) .status() .expect("ail build failed to run"); assert!(status.success(), "ail build failed for cmp_max_smoke"); let output = Command::new(&out).output().expect("run cmp_max_smoke binary"); assert!(output.status.success(), "binary exited non-zero"); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); assert_eq!(stdout.trim(), "7", "cmp_max(3, 7) at Int prints 7"); } #[test] fn workspace_with_only_poly_free_fn_and_no_classes_still_monomorphises() { // poly_id has a Type::Forall free fn and NO class/instance defs // in the user module. The prelude (auto-injected) has classes, // but for early-out purposes we test the user-module gate. // Today the workspace-has-typeclasses early-out checks the // WHOLE workspace including the prelude, so it actually fires // on every user workspace. The Task-7 generalisation widens // the predicate to also include `Def::Fn` with `Type::Forall` // as a specialisable target — which makes the predicate true // for any workspace with poly free fns (regardless of classes). let ws = load_workspace(&fixture("poly_id.ail.json")).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"); // After the unified pass, the workspace contains synthesised // mono fn defs for `id` at Int and at Bool — both invoked from // `main`. let names: Vec = post_mono.modules.values() .flat_map(|m| m.defs.iter().filter_map(|d| { if let Def::Fn(f) = d { Some(f.name.clone()) } else { None } })) .collect(); assert!(names.iter().any(|n| n == "id__Int"), "poly_id must produce id__Int via unified mono; got {names:?}"); assert!(names.iter().any(|n| n == "id__Bool"), "poly_id must produce id__Bool via unified mono; got {names:?}"); } #[test] fn rewrite_mono_calls_rewrites_bare_poly_free_fn_to_mono_symbol() { let ws = load_workspace(&fixture("cmp_max_smoke.ail.json")).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 main_mod = post_mono.modules.get("cmp_max_smoke").expect("main module"); let main_fn = main_mod.defs.iter().find_map(|d| { if let Def::Fn(f) = d { (f.name == "main").then_some(f) } else { None } }).expect("main fn"); let body_str = format!("{:?}", main_fn.body); assert!( body_str.contains("cmp_max__Int"), "main body must call cmp_max__Int after rewrite; got: {body_str}" ); // The bare-name `cmp_max` (as a Term::Var.name) must NOT appear // anywhere in main's body post-rewrite. assert!( !body_str.contains("Var { name: \"cmp_max\""), "main body must NOT still call bare cmp_max after rewrite; got: {body_str}" ); } #[test] fn synthesise_mono_fn_free_fn_arm_substitutes_rigid_vars() { use ailang_check::mono::synthesise_mono_fn_for_free_fn; let ws = load_workspace(&fixture("cmp_max_smoke.ail.json")).expect("workspace loads"); let diags = ailang_check::check_workspace(&ws); assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}"); let int_ty = Type::Con { name: "Int".into(), args: vec![] }; let target = MonoTarget::FreeFn { name: "cmp_max".into(), type_args: vec![int_ty.clone()], defining_module: "cmp_max_smoke".into(), }; let synth = synthesise_mono_fn_for_free_fn(&target, &ws) .expect("synthesis succeeds"); assert_eq!(synth.name, "cmp_max__Int"); // Body must still reference `compare` (pre-rewrite); Task 6 // rewrites it to `compare__Int` during the walker pass. let body_str = format!("{:?}", synth.body); assert!(body_str.contains("compare"), "body still references compare bare-name pre-rewrite: {body_str}"); // Param types must be Int after rigid substitution. assert!(matches!( &synth.ty, Type::Fn { params, ret, .. } if params.len() == 2 && matches!(¶ms[0], Type::Con { name, args } if name == "Int" && args.is_empty()) && matches!(¶ms[1], Type::Con { name, args } if name == "Int" && args.is_empty()) && matches!(ret.as_ref(), Type::Con { name, args } if name == "Int" && args.is_empty()) ), "synth.ty = {:?}", synth.ty); } #[test] fn mono_target_free_fn_variant_keys_distinctly_from_class_method() { let int_ty = Type::Con { name: "Int".into(), args: vec![] }; let class_tgt = MonoTarget::ClassMethod { class: "Eq".into(), method: "eq".into(), type_: int_ty.clone(), defining_module: "prelude".into(), }; let free_tgt = MonoTarget::FreeFn { name: "cmp_max".into(), type_args: vec![int_ty.clone()], defining_module: "prelude".into(), }; assert_ne!( mono_target_key(&class_tgt), mono_target_key(&free_tgt), "class-method and free-fn targets must have distinct dedup keys" ); }