//! M2 item 1: direct rc-accounting de-globalisation proof. Compiles //! runtime/rc.c + runtime/str.c into libailang_rt.a, links the //! rc_accounting_tsan.c harness against it under -fsanitize=thread. //! Per-thread-ctx build: tsan-clean + exit 0. RED until rc.c gains //! ailang_ctx_* + __ail_tls_ctx (link error: undefined symbols). use std::process::Command; fn cc(args: &[&str], ctx: &str) { let st = Command::new("clang").args(args).status().expect("spawn clang"); assert!(st.success(), "clang failed: {ctx}"); } #[test] fn rc_accounting_per_ctx_is_tsan_clean() { let root = ailang_test_support::canonical_workspace_root(); let tmp = std::env::temp_dir().join("ail_m2_rcacct"); std::fs::create_dir_all(&tmp).unwrap(); let rc_o = tmp.join("rc.o"); let str_o = tmp.join("str.o"); let rt_a = tmp.join("libailang_rt.a"); cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", rc_o.to_str().unwrap(), root.join("runtime/rc.c").to_str().unwrap()], "rc.c"); cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", str_o.to_str().unwrap(), root.join("runtime/str.c").to_str().unwrap()], "str.c"); let st = Command::new("ar").args(["rcs", rt_a.to_str().unwrap(), rc_o.to_str().unwrap(), str_o.to_str().unwrap()]).status().unwrap(); assert!(st.success(), "ar failed"); let bin = tmp.join("rcacct"); cc(&["-O1", "-g", "-fsanitize=thread", "-pthread", "-o", bin.to_str().unwrap(), root.join("crates/ail/tests/embed/rc_accounting_tsan.c").to_str().unwrap(), rt_a.to_str().unwrap()], "link rc_accounting_tsan"); let out = Command::new(&bin).env("TSAN_OPTIONS", "halt_on_error=1") .output().expect("run harness"); assert!(out.status.success(), "per-ctx rc-accounting harness must exit 0 tsan-clean; stderr:\n{}", String::from_utf8_lossy(&out.stderr)); } #[test] fn rc_accounting_shared_ctx_is_flagged_by_tsan() { // Negative control: all N threads share one ailang_ctx_t* while // calling ailang_rc_alloc/ailang_rc_dec → concurrent // ctx->alloc_count++ is a genuine data race tsan MUST report. // Proves the de-globalisation property is not vacuous. let root = ailang_test_support::canonical_workspace_root(); let tmp = std::env::temp_dir().join("ail_m2_rcacct_neg"); std::fs::create_dir_all(&tmp).unwrap(); let rc_o = tmp.join("rc.o"); let str_o = tmp.join("str.o"); let rt_a = tmp.join("libailang_rt.a"); cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", rc_o.to_str().unwrap(), root.join("runtime/rc.c").to_str().unwrap()], "rc.c"); cc(&["-O1", "-g", "-fsanitize=thread", "-c", "-o", str_o.to_str().unwrap(), root.join("runtime/str.c").to_str().unwrap()], "str.c"); let st = Command::new("ar").args(["rcs", rt_a.to_str().unwrap(), rc_o.to_str().unwrap(), str_o.to_str().unwrap()]).status().unwrap(); assert!(st.success(), "ar failed"); let bin = tmp.join("rcacct_neg"); cc(&["-O1", "-g", "-fsanitize=thread", "-pthread", "-DSHARED_CTX", "-o", bin.to_str().unwrap(), root.join("crates/ail/tests/embed/rc_accounting_tsan.c").to_str().unwrap(), rt_a.to_str().unwrap()], "link rc_accounting_tsan -DSHARED_CTX"); let out = Command::new(&bin).env("TSAN_OPTIONS", "halt_on_error=1") .output().expect("run rcacct neg"); let stderr = String::from_utf8_lossy(&out.stderr); assert!( !out.status.success() || stderr.contains("WARNING: ThreadSanitizer"), "shared-ctx rc-accounting negative control MUST be tsan-flagged \ (the de-globalisation teeth); exit={:?} stderr:\n{stderr}", out.status.code() ); }