//! 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}" ); } } } }