//! Embedding-ABI M3 two-record-param round-trip — coverage backfill //! for an already-shipped capability (NO language/checker/codegen //! change; these tests pass on HEAD by design — that is the proof). //! //! Named property protected: the M3 export gate accepts a //! single-constructor all-scalar record *independently per parameter* //! (`crates/ailang-check/src/lib.rs:1953` — `is_c_abi_type` in a //! `for p in ¶m_tys` loop), and the staticlib forwarder maps every //! non-scalar `Type::Con` to a bare `ptr` (M3-frozen). Therefore a //! kernel `(State, Tick) -> State` where BOTH params are single-ctor //! all-scalar records is callable from a C host today — a record //! `Tick` pushed per call, not just a scalar sample. Every shipped M3 //! fixture pushes a *scalar* `Float`; none pinned the two-record-param //! per-tick shape (the actual minimal data-server binding). This does. //! //! `own` (no -DBORROW): the kernel consumes BOTH `(own (con State))` //! and `(own (con Tick))` inputs (drop at return); the host's //! `make_state`/`make_tick` + final `dec` land on `g_rc_*`. //! `borrow` (-DBORROW): `Tick` is `(borrow (con Tick))` — the kernel //! does NOT consume it; the host retains and dec's each Tick itself. //! Both prove "ownership follows the declared mode" — pinned for the //! *Tick* record param both ways, mirroring the M3 `_borrow.ail` //! precedent. //! //! The build/link incantation + dual-stat-line global proof mirrors //! `embed_record_e2e.rs` verbatim (M2 TLS-ctx binds the ctx only for //! the synchronous forwarder call, so host-side allocs/decs land on //! `g_rc_*`, not ctx; the invariant is GLOBAL Σallocs == Σfrees //! across *all* stat lines, not per-ctx-line balance). use ailang_test_support::workspace_root; use std::path::PathBuf; use std::process::Command; fn ail_bin() -> &'static str { env!("CARGO_BIN_EXE_ail") } fn manifest() -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) } #[derive(Debug)] struct RcStats { allocs: u64, frees: u64, exit_code: i32, } /// Build `` as a staticlib, link `` (under /// `crates/ail/tests/`) with `extra_cc` flags, run under /// `AILANG_RC_STATS=1`, and return the summed RC-stats readback. fn build_link_run_embed(fixture: &str, host_rel: &str, extra_cc: &[&str]) -> RcStats { let module = fixture.strip_suffix(".ail").expect("fixture is a .ail file"); let fixture_path = workspace_root().join("examples").join(fixture); let host_c = manifest().join("tests").join(host_rel); let outdir = std::env::temp_dir().join(format!( "ail-embed-tick-e2e-{}-{}", module, std::process::id() )); std::fs::create_dir_all(&outdir).unwrap(); let build = Command::new(ail_bin()) .args(["build", fixture_path.to_str().unwrap(), "--emit=staticlib", "-o", outdir.to_str().unwrap()]) .output().expect("ail build --emit=staticlib"); assert!(build.status.success(), "staticlib build failed: {}", String::from_utf8_lossy(&build.stderr)); let host_bin = outdir.join("host"); let mut cc = Command::new("cc"); cc.arg(&host_c); for f in extra_cc { cc.arg(f); } cc.arg(outdir.join(format!("lib{module}.a"))) .arg(outdir.join("libailang_rt.a")) .arg("-o").arg(&host_bin); let cc_out = cc.output().expect("cc host link"); assert!(cc_out.status.success(), "cc link failed: {}", String::from_utf8_lossy(&cc_out.stderr)); let run = Command::new(&host_bin) .env("AILANG_RC_STATS", "1") .output().expect("run host"); let stderr = String::from_utf8_lossy(&run.stderr); // GLOBAL leak-freedom (mirrors embed_record_e2e.rs): the run emits // TWO `ailang_rc_stats:` lines — the `ailang_ctx_free` ctx readback // AND the `g_rc_*` atexit line. M2's TLS-ctx binds the ctx only for // the synchronous forwarder call, so the host's `make_state` / // `make_tick` / per-iter `dec` / final `dec` land on `g_rc_*`. The // invariant is Σallocs == Σfrees across *all* stat lines // (equivalently Σ`live` = 0) — not per-ctx-line balance. let mut allocs: u64 = 0; let mut frees: u64 = 0; let mut seen = 0usize; for line in stderr.lines().filter(|l| l.starts_with("ailang_rc_stats:")) { seen += 1; for tok in line.split_whitespace() { if let Some(v) = tok.strip_prefix("allocs=") { allocs += v.parse::().expect("allocs= u64"); } else if let Some(v) = tok.strip_prefix("frees=") { frees += v.parse::().expect("frees= u64"); } } } assert!(seen > 0, "missing ailang_rc_stats line; stderr was:\n{stderr}"); RcStats { allocs, frees, exit_code: run.status.code().unwrap_or(-1), } } /// own two-record-param: a `(own (con State), own (con Tick)) -> /// State` kernel is callable from C today — the export gate accepts /// the second single-ctor all-scalar record param independently, and /// the forwarder lowers BOTH to `ptr`. Globally leak-free: every /// State and Tick box freed exactly once (kernel-consumed inputs on /// ctx; host `make_state`/`make_tick`/final `dec` on `g_rc_*`), /// Σallocs == Σfrees across both stat lines, and the C host's /// `assert(n == 1000000) && assert(acc == Σ prices)` held (exit 0). #[test] fn tick_roundtrip_own_alloc_eq_free() { let stats = build_link_run_embed( "embed_backtest_step_tick.ail", "embed/tick_roundtrip.c", &[], ); assert_eq!(stats.allocs, stats.frees, "own two-record-param: globally leak-free — Σallocs == Σfrees \ across the ctx readback + g_rc atexit lines (kernel-consumed \ State+Tick on ctx; host make_state/make_tick/final dec on \ g_rc); {stats:?}"); assert_eq!(stats.exit_code, 0, "C host assert(n==1000000 && acc==Σ prices) held"); } /// borrow Tick param: with `Tick` declared `(borrow (con Tick))` the /// kernel does NOT consume it — ownership follows the declared mode /// for a *record* param, the second direction of the M3 `_borrow` /// precedent. The host retains and dec's each Tick itself. Globally /// leak-free: ctx line shows kernel return allocs (live=+N), g_rc the /// host input/Tick/return decs (live=−N), summing to 0 — correct /// M2-TLS cross-attribution, not a leak. Σallocs == Σfrees across /// both lines. Same host, `-DBORROW`. #[test] fn tick_roundtrip_borrow_alloc_eq_free() { let stats = build_link_run_embed( "embed_backtest_step_tick_borrow.ail", "embed/tick_roundtrip.c", &["-DBORROW"], ); assert_eq!(stats.allocs, stats.frees, "borrow Tick: globally leak-free — Σallocs == Σfrees across the \ ctx readback (kernel return allocs, live=+N) + g_rc atexit \ (host State/Tick/return decs, live=−N); summed they balance; \ {stats:?}"); assert_eq!(stats.exit_code, 0, "C host assert(n==1000000 && acc==Σ prices) held"); }