Skip to main content

aver/codegen/lean/universal_lane/
mod.rs

1//! When-universal quarantine lane — genuine universal proofs for
2//! scalar-sign `when`-laws, emitted OUTSIDE the counted build.
3//!
4//! Today a `when`-law's manifest theorem is domain-bounded by
5//! construction (`law_theorem_prop` prepends the sampled-domain
6//! disjunctions), so even a tactic-proved `when`-law only ever earns
7//! `universal:false`. This module emits, for a narrow validated
8//! family, a TWIN theorem in the TRUE universal form
9//! `∀ givens, <when> = true -> claim` into a quarantined, NON-DEFAULT
10//! `lean_lib` (one lib + one hashed module per law, srcDir
11//! `universal_lane/`, importing the manifest entry root).
12//!
13//! IRON GUARD — budget inflation is mechanically impossible:
14//! - the manifest pipeline is untouched: the law keeps its
15//!   `BackendDispatch` strategy and its bounded guarded-domain
16//!   theorem byte-for-byte (this module only ADDS files);
17//! - lane scripts contain ZERO `sorry` tokens (enforced by a debug
18//!   assertion here and a grep test in `proof_spec`) — there is
19//!   nothing for the sorry budget to count even in principle;
20//! - the counted `lake build` only builds the `@[default_target]`
21//!   lib; lane libs are built by SEPARATE, failure-TOLERATED
22//!   invocations at per-law granularity (`aver proof --check`), so a
23//!   hard failure (elaboration error, maxHeartbeats) costs exactly
24//!   "that law stays bounded" — budgets, `passed` and neighbors are
25//!   out of reach by construction;
26//! - credit keys on PER-DECLARATION evidence (`#print axioms` ⊆
27//!   {propext, Classical.choice, Quot.sound} against the built lane
28//!   module), never on an invocation exit code; module names are
29//!   content-hashed so a stale `.olean` can never masquerade as a
30//!   fresh success.
31//!
32//! The prover content comes in two recognized families:
33//!
34//! FAMILY 1 — scalar-sign: the law's `when` is a sign guard (`n > 0` /
35//! `n < 0` / `n >= 0`) on a single Int given, and the LHS enters the
36//! SAME canonical decimal parser pipeline an `IntDecimalRoundtrip`
37//! pin (#470) already certified for the file — the lane proof is a
38//! segment/polarity parameterization of that skeleton
39//! (`law_auto/decimal.rs`), with the premise consumed ONCE at the
40//! existing `rcases` sign split. Exactly the five empirically
41//! validated (kernel-clean on the emitted json project, Lean 4.15)
42//! entry-segment × polarity combinations are recognized; everything
43//! else declines at zero cost:
44//! - digit-dispatch wrapper entry, `when n >= 0` (dispatchNumberOrErr)
45//! - `pos_fn` entry, `when n > 0` (startNumberDigits)
46//! - `neg_fn` entry, `when n < 0` (parseNumberSign)
47//! - `sign_fn` entry, `when n < 0` (startSignDigit)
48//! - scanner entry with pinned `n == 0` Bool arg, `when n >= 0`
49//!   (scanIntTail)
50//!
51//! FAMILY 2 — bridge-shaped premise: the `when` is `boolRel(a, b) =
52//! true` where `boolRel` is a recursive Bool fn mirroring a Prop
53//! relation — concretely a canonical Peano structural equality
54//! (`natEq`-shape, lifted to builtin `Nat`), possibly negated through
55//! `Bool.not` or a 2-arm not-wrapper fn. The probe-proven mechanics
56//! (TIP prop_85 hand proof, kernel-genuine `[propext, Quot.sound]`):
57//! (1) a use-side Bool→Prop inversion bridge `(natEq a b = true) → a
58//! = b`; (2) a REINTRODUCTION bridge `natEq a a = true` (without it
59//! the emitter cannot instantiate its own induction hypothesis);
60//! (3) per-step premise stepping (`simp only [measure-fns] at h` +
61//! `omega`, re-bridged for the structurally smaller call); (4) for
62//! the zip-rev figure, a snoc-distribution aux lemma (zip over
63//! append-singleton under length equality) emitted as a lane-local
64//! lemma from the validated template — never as a sorry. Exactly the
65//! hand-validated figures are recognized (see [`BridgePlan`]);
66//! everything else declines. An ACL2-style free-variables gate
67//! additionally requires vars(when) ⊆ vars(lhs) — a premise variable
68//! unbound by the conclusion's match side would make conditional
69//! rewriting guess, so such laws decline instead.
70//!
71//! Paid-for landmines from the hand-proof probe, baked into the
72//! rendered tactic text:
73//! - the rendered premise is decide-coerced (`(n > 0) = true`) —
74//!   normalized via `of_decide_eq_true h_when` with a `simpa`
75//!   fallback;
76//! - Lean 4.15 `omega` ATOMIZES `Int.ofNat m` / `Int.negSucc m` —
77//!   the zero-case discharge goes `intro h0; subst h0; exact absurd
78//!   hn (by decide)`, and vacuous branches pair the core
79//!   `Int.negSucc_lt_zero` / `Int.ofNat_nonneg` facts with `omega`
80//!   over the shared atom;
81//! - the one genuinely new lemma vs the #470 skeleton is the
82//!   slice-head bridge `String.slice s 0 1 = Char.toString
83//!   (digitChar d)` (and its `1 2` negative twin), closing by
84//!   `rw [hmk]; rfl`.
85//!
86//! TEST HOOKS (env vars read in `cmd_proof_lean`, fail-safe direction
87//! only): `AVER_PROOF_LANE_SABOTAGE=<label>` breaks one lane module's
88//! proof (the tolerated per-law build fails, that law stays bounded,
89//! the counted build is untouched — the iron-guard test);
90//! `AVER_PROOF_NO_UNIVERSAL_LANE=1` disables the lane entirely and
91//! retires a stale index. Neither can grant credit, only withhold it.
92
93use crate::ast::TopLevel;
94use crate::codegen::CodegenContext;
95
96mod bridge;
97mod shared;
98mod sign;
99
100use bridge::{classify_bridge_law, render_bridge_law};
101use sign::{classify_lane_law, collect_pins, render_lane_law};
102
103/// Subdirectory (relative to the proof output dir) holding lane
104/// modules. Deliberately NOT the dir root: `lean_universal_proof`'s
105/// non-recursive scan of root `.lean` files must never see lane
106/// theorems, so the file-level `universal` flag keeps its
107/// counted-build semantics.
108pub const LANE_SUBDIR: &str = "universal_lane";
109
110/// Machine-readable index of emitted lane laws, written to the proof
111/// output dir. `aver proof --check` consumes it to run the
112/// failure-tolerated per-law builds and the `#print axioms` crediting
113/// probes; its absence simply means `when_universal: 0`.
114pub const LANE_MANIFEST_FILE: &str = "_aver_universal_lane.json";
115
116/// One emitted lane law: a single hashed Lean module hosting the
117/// universal twin theorem, exposed as its own non-default `lean_lib`.
118pub struct LaneLawFile {
119    /// `fn.law` label for surfacing (`dispatchNumberOrErr.fromIntRoundtrip`).
120    pub label: String,
121    /// Twin theorem name (`<fn>_law_<law>_universal`).
122    pub theorem: String,
123    /// Module = file stem = lib name, content-hashed so a stale
124    /// `.olean` under an old name is unreachable.
125    pub module: String,
126    /// Full `.lean` source. Contains NO `sorry` token.
127    pub content: String,
128}
129
130/// Generate the lane files for every recognized when-law in the
131/// entry scope. `entry_content` (the emitted manifest entry `.lean`)
132/// is folded into each module's content hash so any manifest change
133/// retires the old module names. `sabotage` is a TEST-ONLY hook
134/// (`AVER_PROOF_LANE_SABOTAGE`): a label substring whose matching
135/// law gets a deliberately failing tactic injected — the executable
136/// proof that one broken lane proof cannot touch budgets or
137/// neighbors.
138pub fn generate(
139    ctx: &CodegenContext,
140    entry_content: &str,
141    sabotage: Option<&str>,
142) -> Vec<LaneLawFile> {
143    let pins = collect_pins(ctx);
144    let entry_root = crate::codegen::common::entry_basename(ctx);
145    let mut out = Vec::new();
146    for item in &ctx.items {
147        let TopLevel::Verify(vb) = item else { continue };
148        let crate::ast::VerifyKind::Law(law) = &vb.kind else {
149            continue;
150        };
151        let sabotage_this = sabotage
152            .is_some_and(|s| format!("{}.{}", vb.fn_name, law.name).contains(s) && !s.is_empty());
153        // Family 1: scalar-sign over an IntDecimalRoundtrip pin.
154        let mut rendered = false;
155        for pin in &pins {
156            let Some(plan) = classify_lane_law(vb, law, ctx, pin) else {
157                continue;
158            };
159            if let Some(file) = render_lane_law(
160                vb,
161                law,
162                ctx,
163                pin,
164                &plan,
165                &entry_root,
166                entry_content,
167                sabotage_this,
168            ) {
169                out.push(file);
170                rendered = true;
171            }
172            break; // first matching pin wins
173        }
174        if rendered {
175            continue;
176        }
177        // Family 2: bridge-shaped premise (recursive Bool equality
178        // bridge over a canonical Peano type).
179        if let Some(plan) = classify_bridge_law(vb, law, ctx)
180            && let Some(file) = render_bridge_law(
181                vb,
182                law,
183                ctx,
184                &plan,
185                &entry_root,
186                entry_content,
187                sabotage_this,
188            )
189        {
190            out.push(file);
191        }
192    }
193    out
194}
195
196/// Append one non-default `lean_lib` per lane law to the generated
197/// lakefile. Appended AFTER the `@[default_target]` lib: the counted
198/// `lake build` never builds these, and `lean_lakefile_roots` (which
199/// keys the `#print axioms` prober for the counted build) reads only
200/// the FIRST `roots :=` line, so the file-level `universal` flag
201/// semantics are untouched.
202pub fn lakefile_with_lane_libs(lakefile: &str, lane: &[LaneLawFile]) -> String {
203    let mut out = lakefile.to_string();
204    for law in lane {
205        out.push_str(&format!(
206            "\nlean_lib «{}» where\n  srcDir := \"{}\"\n  roots := #[`{}]\n",
207            law.module, LANE_SUBDIR, law.module
208        ));
209    }
210    out
211}
212
213/// The machine-readable lane index (`LANE_MANIFEST_FILE` content).
214pub fn lane_manifest_json(lane: &[LaneLawFile]) -> String {
215    let laws: Vec<serde_json::Value> = lane
216        .iter()
217        .map(|l| {
218            serde_json::json!({
219                "law": l.label,
220                "theorem": l.theorem,
221                "module": l.module,
222            })
223        })
224        .collect();
225    serde_json::to_string_pretty(&serde_json::json!({
226        "version": 1,
227        "laws": laws,
228    }))
229    .unwrap_or_else(|_| "{}".to_string())
230}
231
232/// FNV-1a 64-bit — content hash for module names.
233fn fnv1a64(bytes: &[u8]) -> u64 {
234    let mut h: u64 = 0xcbf29ce484222325;
235    for b in bytes {
236        h ^= *b as u64;
237        h = h.wrapping_mul(0x100000001b3);
238    }
239    h
240}
241
242/// Content-hashed lane module name (`U_<theorem-base>_<hash>`): the
243/// hash covers the module's own content AND the manifest entry file,
244/// so a stale `.olean` under an old name is unreachable after any
245/// change to either. Shared by both lane families.
246fn lane_module_id(theorem_base: &str, content: &str, entry_content: &str) -> String {
247    let hash = fnv1a64(content.as_bytes()) ^ fnv1a64(entry_content.as_bytes()).rotate_left(1);
248    let sanitized: String = theorem_base
249        .chars()
250        .map(|c| {
251            if c.is_ascii_alphanumeric() || c == '_' {
252                c
253            } else {
254                '_'
255            }
256        })
257        .collect();
258    format!("U_{sanitized}_{:08x}", (hash & 0xffff_ffff) as u32)
259}