//! RED-pin for leg (C) of the drop-soundness bug family the //! Implicit-cutover (#55) surfaced: a polymorphic ctor's per-type //! drop fn rc_dec's a type-parameter field even when the field is //! monomorphised at a value type (Int/Bool/Float/Unit). //! //! Property protected: under `--alloc=rc`, dropping a value of a //! polymorphic ADT whose ctor field is monomorphised at a value type //! must NOT emit `ailang_rc_dec` on that field. The field is stored //! inline as a raw scalar (an i64 `7`, not a heap pointer), so an //! `rc_dec` dereferences the scalar-as-pointer and crashes. The //! binary must run to completion, print the inner value, and the //! runtime RC stats line must report `live == 0` (the single ADT //! slab is freed exactly once, with no spurious dec of the inline //! value field). //! //! Root cause: `crates/ailang-codegen/src/drop.rs` //! `emit_drop_fn_for_type` (mirrored in `emit_iterative_drop_fn_for_type` //! and `emit_partial_drop_fn_for_type`) emits the per-type drop fn ONCE //! from the polymorphic `Def::Type` declaration — never per-monomorph. //! For each ctor field it computes `llvm_type(fty)` where `fty` is the //! declared field type. For a type-parameter field that type is a //! `Type::Var`, which `llvm_type` rejects (synth.rs returns `Err`); the //! `.unwrap_or_else(|_| "ptr".into())` at the field loop then silently //! treats the var-typed field as a boxed pointer and emits `rc_dec`. //! At the `Box Int` monomorph the field is an inline `i64`, so the //! emitted `rc_dec` dereferences the integer value -> SIGSEGV. //! //! As of HEAD (the in-flight Implicit cutover) this test fails: the //! binary exits 139 (SIGSEGV) before printing anything. Pre-cutover //! the bug was latent — Implicit scrutinees skipped the drop path //! entirely, so the mistyped drop never ran (it leaked instead of //! crashing). //! //! A single-field `Box a` at `Int` is the minimal trigger. The fix //! must still dec genuinely-heap fields (e.g. a `Box Str` field must //! free its slab exactly once) and must hold for the partial-drop and //! nested-monomorph variants — see the debugger handoff for the fix //! locus and soundness reasoning. use std::path::Path; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } #[test] fn alloc_rc_value_type_field_is_not_rc_dec_dropped() { let manifest_dir = env!("CARGO_MANIFEST_DIR"); let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); let src = workspace .join("examples") .join("drop_value_field_no_segfault_pin.ail"); let tmp = std::env::temp_dir().join(format!( "ailang_drop_value_field_no_segfault_pin_{}", std::process::id() )); std::fs::create_dir_all(&tmp).unwrap(); let out = tmp.join("bin"); let status = Command::new(ail_bin()) .args(["build", src.to_str().unwrap(), "--alloc=rc", "-o"]) .arg(&out) .status() .expect("ail build failed to run"); assert!( status.success(), "ail build --alloc=rc failed for drop_value_field_no_segfault_pin.ail" ); let output = Command::new(&out) .env("AILANG_RC_STATS", "1") .output() .expect("execute binary"); assert!( output.status.success(), "binary exited non-zero (status {:?}): the polymorphic drop fn \ drop__Box rc_dec's the type-parameter field, which at the \ Box Int monomorph is the inline i64 `7` (not a heap pointer), \ so rc_dec(7) dereferences the scalar-as-pointer and SIGSEGVs. \ Fix in crates/ailang-codegen/src/drop.rs: do not dec a field \ whose monomorphised type is a value type.", output.status ); let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); assert_eq!( stdout.trim_end(), "7", "unboxing MkBox 7 at Int must print 7; got {stdout:?}" ); let stderr = String::from_utf8(output.stderr).expect("stderr utf8"); let stats_line = stderr .lines() .find(|l| l.starts_with("ailang_rc_stats:")) .unwrap_or_else(|| { panic!("missing ailang_rc_stats line in stderr; stderr was:\n{stderr}") }); let mut allocs: Option = None; let mut frees: Option = None; let mut live: Option = None; for tok in stats_line.split_whitespace() { if let Some(v) = tok.strip_prefix("allocs=") { allocs = v.parse().ok(); } else if let Some(v) = tok.strip_prefix("frees=") { frees = v.parse().ok(); } else if let Some(v) = tok.strip_prefix("live=") { live = v.parse().ok(); } } let allocs = allocs.expect("missing allocs= field"); let frees = frees.expect("missing frees= field"); let live = live.expect("missing live= field"); assert_eq!( live, 0, "RC imbalance after dropping a value-type-field ADT \ (allocs={allocs} frees={frees} live={live}); the Box slab \ must be freed exactly once and the inline Int field must \ never be dec'd." ); assert_eq!( allocs, frees, "alloc/free mismatch (allocs={allocs} frees={frees} live={live})" ); }