Files
AILang/crates/ail/tests/embed_tick_e2e.rs
T
Brummel 7ae92d3e60 refactor(test): hoist duplicated test helpers into ailang-test-support
closes #60

The fixture-corpus filter (NON_PARSEABLE_FIXTURES + list_ail_fixtures)
was copy-pasted across three test crates and drifted out of sync as
new reject fixtures landed; the examples_dir / workspace_root path
walk was reimplemented ~30 more times across the suite, under two
spellings (workspace_root / ws_root).

Introduce crates/ailang-test-support — a zero-dependency, dev-only
leaf crate — as the single home for these helpers:
- NON_PARSEABLE_FIXTURES, list_ail_fixtures
- workspace_root, examples_dir
- canonical_workspace_root (symlink-resolved variant, for the tsan/
  race tests that feed the root into native build steps)

All integration-test copies across ailang-core, ailang-surface,
ailang-prose, ailang-check, and ail now import from the shared crate;
the local definitions and their now-orphaned path imports are removed.
Path helpers resolve via the support crate's own CARGO_MANIFEST_DIR,
so they return the correct absolute paths from any caller.

ail_bin helpers are intentionally left in place: they depend on
env!("CARGO_BIN_EXE_ail"), which Cargo only defines when compiling the
ail crate's own integration tests, so they cannot move to a shared
crate.

Behaviour-identical: same paths, same fixture lists; full workspace
test suite green. Net -177 LOC.
2026-06-02 01:54:37 +02:00

157 lines
6.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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 &param_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 `<fixture>` as a staticlib, link `<host_rel>` (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::<u64>().expect("allocs= u64");
} else if let Some(v) = tok.strip_prefix("frees=") {
frees += v.parse::<u64>().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");
}