e25580e3a3
Wires the #55-cutover fieldtest into a durable regression net. The
fieldtest exercised the post-cutover ParamMode={Own,Borrow} surface as a
downstream LLM author would and produced 12 fixtures + a report; left as
loose files they would be dead fixtures that drift. This commits them as
a protected property.
crates/ail/tests/cut55_cutover_surface.rs — one table-driven test running
each fixture through `ail check` and asserting accept (exit 0) or reject
at the right pipeline stage. Reject rows key on the bracketed diagnostic
CODE (consume-while-borrowed, borrow-over-value, borrow-return-not-permitted,
surface-parse-error), NOT message text, so the test is decoupled from the
diagnostic-wording improvement tracked separately.
examples/fieldtest/cut55_*.ail — the corpus. cut55_2c is the #58 repro
(now correctly rejected). cut55_2b was misanalysed by the fieldtest as a
consume-while-borrowed reject; in fact bump's recursive param is
(borrow List) so the tail is only borrowed, never consumed into an own
slot — `ail check` correctly accepts it. Renamed to
cut55_2b_borrow_traversal_clean and recast as the #58 false-positive
guard (a borrow-position use of a heap sub-binder must stay accepted).
cut55_1b similarly does not fire over-strict-mode (its recursive call
consumes the tail, so the lint's consume_count==0 premise fails) — comment
corrected; it is a plain accept. cut55_1c is the genuine over-strict-mode
example (accepts exit 0, emits the warning).
docs/specs/0065-eliminate-implicit-mode fieldtest report — moved 2c to
the reject set, added the 2b false-positive-guard section, marked the
double-free spec_gap RESOLVED (it shipped as the #58 fix).
Relates to #55.
167 lines
7.2 KiB
Rust
167 lines
7.2 KiB
Rust
//! Regression net for the #55 ParamMode={Own,Borrow} totality cutover
|
|
//! (commit `76b21c0`, spec 0062 / plan 0121) and its #58 follow-up
|
|
//! (`b129af1`, sub-binder borrow inheritance).
|
|
//!
|
|
//! Property protected: the post-#55-cutover ownership surface accepts the
|
|
//! legitimate authoring forms and rejects the unsound / over-strict ones,
|
|
//! AT THE RIGHT PIPELINE STAGE. Concretely, for each fixture under
|
|
//! examples/fieldtest/cut55_*.ail, `ail check` must:
|
|
//! - exit 0 for the legitimate forms (read-only borrow, consume own,
|
|
//! value-typed own, clone-to-return escape hatch, and the
|
|
//! borrow-traverse-and-rebuild that must NOT trip the #58 check);
|
|
//! - reject the unsound / over-strict forms with the EXACT diagnostic
|
|
//! code, at the stage that owns the rule:
|
|
//! * bare fn-type slot -> `surface-parse-error` (parser stage),
|
|
//! * borrow over value type -> `borrow-over-value` (signature),
|
|
//! * borrowed return -> `borrow-return-not-permitted` (sig),
|
|
//! * consume while borrowed -> `consume-while-borrowed` (linearity),
|
|
//! and emit the `over-strict-mode` advisory (a warning, exit 0) on a
|
|
//! true read-only `(own)` heap param.
|
|
//!
|
|
//! Coupling discipline: the assertions key on the bracketed diagnostic
|
|
//! CODE (e.g. `[consume-while-borrowed]`) appearing in stderr, never on
|
|
//! the surrounding message wording — that wording is slated to change
|
|
//! (the borrow-return and consume-while-borrowed friction tidy-ups noted
|
|
//! in the spec-0065 findings) and the regression net must survive it.
|
|
//! Each row carries the fixture stem so a failure names which authoring
|
|
//! form regressed.
|
|
//!
|
|
//! Without this test the twelve cut55_* fixtures are dead files that the
|
|
//! build never executes; a regression in the totality check (a relaxed
|
|
//! reject, a moved stage, a re-introduced default) would land silently.
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
|
|
fn ail_bin() -> &'static str {
|
|
env!("CARGO_BIN_EXE_ail")
|
|
}
|
|
|
|
fn fixture(stem: &str) -> PathBuf {
|
|
let manifest_dir = env!("CARGO_MANIFEST_DIR");
|
|
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
|
|
workspace
|
|
.join("examples")
|
|
.join("fieldtest")
|
|
.join(format!("{stem}.ail"))
|
|
}
|
|
|
|
/// What `ail check` must do with a fixture. Codes name the bracketed
|
|
/// diagnostic tag, never the message prose.
|
|
enum Expect {
|
|
/// exit 0, no warning expected (legitimate form).
|
|
Accept,
|
|
/// exit 0, but the `over-strict-mode` advisory warning must appear.
|
|
AcceptWithWarning(&'static str),
|
|
/// exit non-zero, and `[code]` must appear in stderr.
|
|
Reject(&'static str),
|
|
}
|
|
|
|
fn check(stem: &str) -> (bool, String) {
|
|
let out = Command::new(ail_bin())
|
|
.args(["check", fixture(stem).to_str().unwrap()])
|
|
.output()
|
|
.expect("ail check failed to run");
|
|
let stderr = String::from_utf8(out.stderr).expect("stderr utf8");
|
|
(out.status.success(), stderr)
|
|
}
|
|
|
|
#[test]
|
|
fn cut55_cutover_surface_accepts_and_rejects_at_the_right_stage() {
|
|
// (fixture stem, expectation). The stem is in every failure message
|
|
// so a red row names the regressed authoring form.
|
|
let table: &[(&str, Expect)] = &[
|
|
// --- decision (a): borrow vs own on a heap value -----------------
|
|
("cut55_1_readonly_length", Expect::Accept),
|
|
// over-strict own that does NOT trip the lint: the recursive call
|
|
// consumes the heap tail into an own self-param, so the lint's
|
|
// consume_count==0 premise fails. Still a sound (if strict)
|
|
// annotation -> accepted, silent.
|
|
("cut55_1b_overstrict_own", Expect::Accept),
|
|
// true read-only own: nothing consumed -> the advisory fires.
|
|
(
|
|
"cut55_1c_overstrict_isempty",
|
|
Expect::AcceptWithWarning("over-strict-mode"),
|
|
),
|
|
// --- decision (b): consume vs borrow -----------------------------
|
|
("cut55_2_consume_map", Expect::Accept),
|
|
// false-positive guard for #58: borrow-traverse-and-rebuild, the
|
|
// recursive param is (borrow) so the tail is never consumed.
|
|
("cut55_2b_borrow_traversal_clean", Expect::Accept),
|
|
// #58 fix: tail sub-binder of a borrowed scrutinee moved into an
|
|
// own sink -> reject at the linearity check.
|
|
(
|
|
"cut55_2c_consume_borrow_reject",
|
|
Expect::Reject("consume-while-borrowed"),
|
|
),
|
|
// whole borrowed param moved into an own sink -> reject.
|
|
(
|
|
"cut55_2d_consume_borrow_whole",
|
|
Expect::Reject("consume-while-borrowed"),
|
|
),
|
|
// --- decision (c): value-typed params ----------------------------
|
|
("cut55_3_value_param", Expect::Accept),
|
|
// borrow over an unboxed value type -> reject at the signature.
|
|
(
|
|
"cut55_3b_borrow_over_value",
|
|
Expect::Reject("borrow-over-value"),
|
|
),
|
|
// --- decision (d): borrowed return -------------------------------
|
|
// a (borrow) return is refcount-invisible -> reject at the signature.
|
|
(
|
|
"cut55_4_borrow_return_reject",
|
|
Expect::Reject("borrow-return-not-permitted"),
|
|
),
|
|
// the clone escape hatch lifts a borrowed field to an owned return.
|
|
("cut55_4b_clone_to_return", Expect::Accept),
|
|
// --- the cutover's core promise: no defaulted mode ---------------
|
|
// a bare fn-type slot is rejected at the PARSER stage, not linearity.
|
|
(
|
|
"cut55_5_bare_slot_reject",
|
|
Expect::Reject("surface-parse-error"),
|
|
),
|
|
];
|
|
|
|
for (stem, expect) in table {
|
|
let (ok, stderr) = check(stem);
|
|
match expect {
|
|
Expect::Accept => {
|
|
assert!(
|
|
ok,
|
|
"fixture `{stem}` must be ACCEPTED (exit 0) by the \
|
|
post-#55 ownership surface, but `ail check` rejected \
|
|
it. stderr:\n{stderr}"
|
|
);
|
|
}
|
|
Expect::AcceptWithWarning(code) => {
|
|
assert!(
|
|
ok,
|
|
"fixture `{stem}` must exit 0 (the `{code}` advisory \
|
|
is a warning, not an error), but `ail check` exited \
|
|
non-zero. stderr:\n{stderr}"
|
|
);
|
|
assert!(
|
|
stderr.contains(&format!("[{code}]")),
|
|
"fixture `{stem}` must emit the `[{code}]` advisory \
|
|
warning, but it did not appear in stderr. stderr:\n{stderr}"
|
|
);
|
|
}
|
|
Expect::Reject(code) => {
|
|
assert!(
|
|
!ok,
|
|
"fixture `{stem}` must be REJECTED (non-zero exit) by \
|
|
the post-#55 ownership surface, but `ail check` \
|
|
accepted it (exit 0). stderr:\n{stderr}"
|
|
);
|
|
assert!(
|
|
stderr.contains(&format!("[{code}]")),
|
|
"fixture `{stem}` must be rejected with diagnostic code \
|
|
`[{code}]`, but that code did not appear in stderr. \
|
|
The reject may have moved to a different stage or code. \
|
|
stderr:\n{stderr}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|