aver/codegen/proof_lower/mod.rs
1//! Build `ProofIR` from a `CodegenContext`.
2//!
3//! The lowering producer: types live in `src/ir/proof_ir.rs`, this
4//! file fills them in from a typechecked + analysed codegen
5//! context. Output lands in `CodegenContext.proof_ir`; both proof
6//! backends read from the same field, so any classifier-side
7//! decision flows consistently to Lean and Dafny without each
8//! backend re-running shape detection.
9//!
10//! Populates three IR sections: `refined_types` (refinement-via-
11//! opaque records → Lean Subtype / Dafny subset type),
12//! `fn_contracts` (per-pure-fn recursion shape: native /
13//! sized-fuel / linear recurrence), and `law_theorems` (per-verify-
14//! law strategy + quantifier decomposition + claim shape, with
15//! Oracle-Lift'd impl-spec calls for effectful equivalence).
16//!
17//! `tests/proof_ir_diff.rs` pins the producer's output for each
18//! canonical source pattern — divergence between the classifier and
19//! the IR populator surfaces there.
20//!
21//! # Epic #170 Phase 7 invariant — AST discovery + typed identity
22//!
23//! This module is the **last consumer** of raw `crate::ast::Expr`
24//! patterns in the codegen layer. That is intentional, not
25//! migration debt.
26//!
27//! ## What's AST-shaped (syntax-discovery-only)
28//!
29//! Detector helpers in this file (`detect_*`, `walk_for_*`,
30//! `callee_matches_name`, `call_named_args`, `binary_call_var_const`,
31//! `matches_ident_expr`) walk `ast::Expr` directly. They are
32//! **pattern matchers** over source shape — they look for things
33//! like `match n { 0 -> base; _ -> rec(n - 1) }` or
34//! `Map.has(outer(m, k), k)` to decide which `ProofStrategy` /
35//! `RecursionPlan` variant lowers a given fn or law. The pattern
36//! belongs in source-shape; rewriting them on `ResolvedExpr` would
37//! be the same logic spelled in a different enum, no extra safety.
38//!
39//! Every detector helper carries a `syntax-discovery-only` comment
40//! at its definition.
41//!
42//! ## What's identity-sensitive (typed IDs)
43//!
44//! Decisions that depend on **which fn / type / ctor** a name
45//! refers to (not just "does this name appear") MUST go through
46//! `SymbolTable` or `ProofIR.refined_types` (`TypeId`-keyed) /
47//! `ProofIR.fn_contracts` (`FnId`-keyed). Examples:
48//!
49//! - Refinement-carrier lookups go through `find_refined_type` /
50//! `resolve_refined_type_in_with_key`, both of which canonicalise
51//! the name through the symbol table before reaching the IR map.
52//! - Fn-contract lookups go through `find_fn_contract_for_fn` —
53//! pointer-eq scope on `&FnDef` resolves to the right `FnId`.
54//! - The Lean native-guarded rewriter pins target by `FnId` via
55//! `rewrite_native_guarded_calls_resolved_expr` (PR 169).
56//!
57//! ## What stays raw-AST as a documented identity exception
58//!
59//! Builtin matchers (`callee_is X for X ∈ {"Bool.and", "Map.set",
60//! …}`) compare against the canonical builtin namespace, which is
61//! global by spec — no per-scope identity to leak. Verify-law
62//! callsites all walk `vb.fn_name` (entry-only by parser grammar);
63//! the `EntryFnIndex` newtype in `verify_law.rs` pins the
64//! entry-only contract at the type level (PR 177).
65//!
66//! Full `ResolvedProofLowerView` + semantic matcher API
67//! (`callee_is_builtin`, `callee_is_fn(FnId)`, `ctor_is`,
68//! `ident_name`, `int_lit`) deferred per
69//! `project_phase_e_scope_b_deferred` memory until a real trigger
70//! lands (module-scoped verify, dotted law targets, LSP rename,
71//! cross-scope inliner).
72
73use std::collections::{HashMap, HashSet};
74
75use crate::ast::{Expr, FnDef, Literal, Spanned, TopLevel, TypeDef};
76use crate::codegen::common::expr_to_dotted_name;
77use crate::codegen::recursion::RecursionPlan;
78use crate::codegen::{CodegenContext, ModuleInfo};
79use crate::ir::proof_ir::{
80 DecreaseProof, FnContract, Measure, NativeIntCountdownBody, Predicate, PreservationProof,
81 ProofIR, QuantifierType, RecursionContract, RefinedTypeDecl,
82};
83
84/// Backend-neutral view of the data `proof_lower` needs. Built once
85/// per lowering call; lets the pipeline pass it through without
86/// requiring a fully-assembled `CodegenContext` (which only exists
87/// after `build_context` runs). Legacy callers still build the view
88/// from `&CodegenContext` via [`ProofLowerInputs::from_ctx`].
89///
90/// All fields are borrows — the struct never owns memory; the pipeline
91/// and `build_context` both already own the data and just lend it.
92///
93/// Post-Step-7c: every helper the lowerer touches
94/// (`refinement_info_for`, `analyze_plans`, the `detect.rs` shape
95/// checkers) reads its inputs through this view. No more
96/// `&CodegenContext` reach-through — the struct stands on its own.
97pub struct ProofLowerInputs<'a> {
98 /// Entry-file top-level items, post-pipeline (TCO etc. applied).
99 pub entry_items: &'a [TopLevel],
100 /// Dependent modules already split into type/fn defs.
101 pub dep_modules: &'a [ModuleInfo],
102 /// Set of dep module prefix strings (e.g. `"Models.User"`).
103 pub module_prefixes: &'a HashSet<String>,
104 /// Recursive fn ids from the `analyze` pipeline stage. Keyed
105 /// by opaque [`crate::ir::FnId`] so entry+module same-bare-name
106 /// fns don't merge. Per-scope helpers below project back to
107 /// `HashSet<String>` for consumers that operate on a single
108 /// scope (the DAG invariant keeps bare-name unambiguous within
109 /// a scope).
110 pub recursive_fns: &'a HashSet<crate::ir::FnId>,
111 /// Resolved-identity table (#138 phase E). When `Some`, the
112 /// populate-side resolves `FnKey` / `TypeKey` to `FnId` /
113 /// `TypeId` once at the IR boundary and keys `ProofIR.fn_contracts`
114 /// / `ProofIR.refined_types` / `LawTheorem.fn_id` by the opaque
115 /// IDs. Callers that haven't wired in the symbol-table stage
116 /// pass `None` and fall through to legacy key-typed maps
117 /// (transitional during phase E migration).
118 pub symbol_table: &'a crate::ir::SymbolTable,
119 /// Optional `ProgramShape` substrate (Stage 6b of #232). When
120 /// `Some`, `refinement_info_for` reads from the typed
121 /// `ModulePattern::RefinementSmartConstructor` entries instead of
122 /// re-walking the AST. `None` keeps the legacy walk path —
123 /// preserved for test fixtures that build `ProofLowerInputs` by
124 /// hand without going through the pipeline.
125 pub program_shape: Option<&'a crate::analysis::shape::ProgramShape>,
126}
127
128impl<'a> ProofLowerInputs<'a> {
129 /// Build a view from a fully-assembled `CodegenContext` — used
130 /// by `refresh_facts` (test helper) and by any caller that
131 /// already owns a built context. Reads only the fields the
132 /// lowerer actually needs.
133 pub fn from_ctx(ctx: &'a CodegenContext) -> Self {
134 Self {
135 entry_items: &ctx.items,
136 dep_modules: &ctx.modules,
137 module_prefixes: &ctx.module_prefixes,
138 recursive_fns: &ctx.recursive_fns,
139 symbol_table: &ctx.symbol_table,
140 program_shape: ctx.program_shape.as_ref(),
141 }
142 }
143
144 /// All pure fn defs across entry items and dep modules, in walk
145 /// order (entry first, then deps). `is_pure_fn` lives in the
146 /// Lean toplevel module today; pure_fns reaches there since the
147 /// pure-ness criterion is the same for every proof backend.
148 pub fn pure_fns(&self) -> Vec<&'a FnDef> {
149 // Order matches the legacy `lean::pure_fns(ctx)`: deps first,
150 // entry last. `call_graph::ordered_fn_components` is order-
151 // sensitive (SCC discovery order changes which member is
152 // chosen as the representative); flipping the order shifted
153 // some classifications between fuel and "outside subset".
154 self.dep_modules
155 .iter()
156 .flat_map(|m| m.fn_defs.iter())
157 .chain(self.entry_items.iter().filter_map(|item| match item {
158 TopLevel::FnDef(fd) => Some(fd),
159 _ => None,
160 }))
161 .filter(|fd| crate::codegen::common::is_pure_fn(fd))
162 .collect()
163 }
164
165 /// Recursive pure fn names. Filters `recursive_fns` by pure-ness.
166 /// Returns bare names (pure_fns view is the whole program here,
167 /// so any FnId in `recursive_fns` that maps back to a pure fn
168 /// gets its bare name surfaced for downstream classifiers).
169 pub fn recursive_pure_fn_names(&self) -> HashSet<String> {
170 let symbols = self.symbol_table;
171 let pure_ids: HashSet<crate::ir::FnId> = self
172 .pure_fns()
173 .into_iter()
174 .filter_map(|fd| {
175 let scope = self
176 .dep_modules
177 .iter()
178 .find(|m| m.fn_defs.iter().any(|d| std::ptr::eq(d, fd)))
179 .map(|m| m.prefix.as_str());
180 // **syntax-discovery-only** (epic #170 Phase 8
181 // guardrail): scope was just resolved via pointer-eq
182 // against dep modules — the `None` arm is the
183 // correct entry-scope key by construction (same
184 // shape as `fn_key_for_decl` in `codegen::common`).
185 let key = match scope {
186 Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &fd.name),
187 None => crate::ir::FnKey::entry(&fd.name),
188 };
189 symbols.fn_id_of(&key)
190 })
191 .collect();
192 self.recursive_fns
193 .intersection(&pure_ids)
194 .map(|id| symbols.fn_entry(*id).key.name.clone())
195 .collect()
196 }
197
198 /// Pure fns restricted to a single scope: `None` = entry only,
199 /// `Some(prefix)` = the dep module with that prefix only. Aver's
200 /// module DAG invariant rules out cross-module recursion SCCs,
201 /// so per-scope classification is the canonical view —
202 /// `populate_fn_contracts` walks this per scope to give each
203 /// `Module.fn` its own canonical key in `ir.fn_contracts`
204 /// instead of letting two same-bare-name fns silently merge.
205 pub fn pure_fns_in_scope(&self, scope: Option<&str>) -> Vec<&'a FnDef> {
206 match scope {
207 None => self
208 .entry_items
209 .iter()
210 .filter_map(|item| match item {
211 TopLevel::FnDef(fd) => Some(fd),
212 _ => None,
213 })
214 .filter(|fd| crate::codegen::common::is_pure_fn(fd))
215 .collect(),
216 Some(prefix) => self
217 .dep_modules
218 .iter()
219 .filter(|m| m.prefix == prefix)
220 .flat_map(|m| m.fn_defs.iter())
221 .filter(|fd| crate::codegen::common::is_pure_fn(fd))
222 .collect(),
223 }
224 }
225
226 /// Recursive pure fn names restricted to a single scope. Filters
227 /// the FnId-keyed `recursive_fns` to the ones whose canonical
228 /// scope matches `scope`, then projects back to bare names for
229 /// scope-local consumers (DAG invariant keeps bare-name
230 /// unambiguous within a single scope).
231 pub fn recursive_pure_fn_names_in_scope(&self, scope: Option<&str>) -> HashSet<String> {
232 let symbols = self.symbol_table;
233 let pure_ids: HashSet<crate::ir::FnId> = self
234 .pure_fns_in_scope(scope)
235 .into_iter()
236 .filter_map(|fd| {
237 // **syntax-discovery-only** (epic #170 Phase 8
238 // guardrail): scope is the caller's stated scope —
239 // `None` = entry, `Some(prefix)` = dep module. Both
240 // arms below are the correct key for the matching
241 // arm; bare-name keying is safe because the caller
242 // has already narrowed to a single scope.
243 let key = match scope {
244 Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &fd.name),
245 None => crate::ir::FnKey::entry(&fd.name),
246 };
247 symbols.fn_id_of(&key)
248 })
249 .collect();
250 self.recursive_fns
251 .intersection(&pure_ids)
252 .map(|id| symbols.fn_entry(*id).key.name.clone())
253 .collect()
254 }
255
256 /// Iterator over (`None` = entry, `Some(prefix)` = each dep
257 /// module) — drives `populate_fn_contracts`'s per-scope walk.
258 pub fn scopes(&self) -> Vec<Option<String>> {
259 let mut out = vec![None];
260 for m in self.dep_modules {
261 out.push(Some(m.prefix.clone()));
262 }
263 out
264 }
265
266 /// Scope of the dep module that owns `fd`, or `None` for entry
267 /// module fns. Pointer-eq match against `dep_modules`, mirroring
268 /// `crate::codegen::common::fn_owning_scope_for` but reading off
269 /// the lowering view (which doesn't carry a full `CodegenContext`).
270 pub fn fn_owning_scope(&self, fd: &FnDef) -> Option<&'a str> {
271 for m in self.dep_modules {
272 for f in &m.fn_defs {
273 if std::ptr::eq(f, fd) {
274 return Some(m.prefix.as_str());
275 }
276 }
277 }
278 None
279 }
280
281 /// Resolve a raw-AST expression to its `ResolvedExpr` form under
282 /// the given scope. ProofIR stores resolved expressions (Phase E
283 /// PR 12 Scope A), so this helper is called at every producer
284 /// site that lifts a `Spanned<crate::ast::Expr>` slice from the
285 /// source into an IR field. Mirrors
286 /// `CodegenContext::resolve_expr` but reads only the
287 /// `symbol_table` carried on this view — proof lowering runs
288 /// inside the pipeline, before a full `CodegenContext` exists.
289 pub fn resolve_expr(
290 &self,
291 expr: &crate::ast::Spanned<crate::ast::Expr>,
292 scope: Option<&str>,
293 ) -> crate::ast::Spanned<crate::ir::hir::ResolvedExpr> {
294 use crate::ir::hir::{ResolveCtx, ResolvedStmt};
295 let mut rctx = ResolveCtx::new(self.symbol_table);
296 rctx.current_module = scope.map(String::from);
297 let stmt = crate::ast::Stmt::Expr(expr.clone());
298 match crate::ir::hir::resolve::resolve_stmt_external(&rctx, &stmt) {
299 ResolvedStmt::Expr(s) => s,
300 ResolvedStmt::Binding { value, .. } => value,
301 }
302 }
303
304 /// Names of every recursive user-defined type across entry + deps.
305 pub fn recursive_type_names(&self) -> HashSet<String> {
306 self.entry_items
307 .iter()
308 .filter_map(|item| match item {
309 TopLevel::TypeDef(td) => Some(td),
310 _ => None,
311 })
312 .chain(self.dep_modules.iter().flat_map(|m| m.type_defs.iter()))
313 .filter(|td| crate::codegen::common::is_recursive_type_def(td))
314 .map(|td| crate::codegen::common::type_def_name(td).to_string())
315 .collect()
316 }
317
318 /// Find a fn def by name across entry + deps. Falls back to the
319 /// last segment of a dotted call (e.g. `Module.fn` resolves to
320 /// `fn` when no exact-match candidate exists).
321 pub fn find_fn_def_by_call_name(&self, call_name: &str) -> Option<&'a FnDef> {
322 let find_exact = |name: &str| -> Option<&'a FnDef> {
323 self.dep_modules
324 .iter()
325 .flat_map(|m| m.fn_defs.iter())
326 .chain(self.entry_items.iter().filter_map(|item| match item {
327 TopLevel::FnDef(fd) => Some(fd),
328 _ => None,
329 }))
330 .find(|fd| fd.name == name)
331 };
332 find_exact(call_name).or_else(|| {
333 let short = call_name.rsplit('.').next()?;
334 find_exact(short)
335 })
336 }
337
338 /// Find a type def by bare name across entry + deps. None on miss
339 /// or when the name resolves to a non-Product / non-Sum shape.
340 pub fn find_type_def(&self, type_name: &str) -> Option<&'a TypeDef> {
341 self.entry_items
342 .iter()
343 .filter_map(|item| match item {
344 TopLevel::TypeDef(td) => Some(td),
345 _ => None,
346 })
347 .chain(self.dep_modules.iter().flat_map(|m| m.type_defs.iter()))
348 .find(|td| crate::codegen::common::type_def_name(td) == type_name)
349 }
350}
351
352/// Run every proof-export lowering in one shot — convenience for
353/// callers that want a fully-populated ProofIR. The pipeline calls
354/// the three `populate_*` fns directly so it can run them as
355/// independent stages and short-circuit on typecheck failure.
356pub fn lower(inputs: &ProofLowerInputs) -> ProofIR {
357 let mut ir = ProofIR::default();
358 populate_refined_types(inputs, &mut ir);
359 populate_fn_contracts(inputs, &mut ir);
360 populate_law_theorems(inputs, &mut ir);
361 ir
362}
363
364/// Refinement-via-opaque lift. Walks every type definition (entry +
365/// dep modules), classifies the records that pair a single carrier
366/// field with a validating smart constructor, and emits
367/// `RefinedTypeDecl` entries into `ir.refined_types`. Backends
368/// (Lean → Subtype, Dafny → subset type) render these directly.
369pub fn populate_refined_types(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
370 // Walk entry items first, then dep modules. The map is keyed by
371 // opaque `TypeId` resolved through the symbol table — same
372 // collision-safe shape as `fn_contracts: HashMap<FnId, _>`. The
373 // typechecker explicitly permits two modules to expose distinct
374 // types of the same bare name (`A.Shape` vs `B.Shape`; see
375 // `tests/typechecker_spec::cross_module_same_named_types_do_not_
376 // merge`); opaque IDs make their predicates impossible to merge
377 // by construction. Producer resolves `TypeKey -> TypeId` once
378 // here; consumers (`find_refined_type_scoped`) resolve through
379 // the same symbol table at lookup time.
380 //
381 // SymbolTable is always present (`ProofLowerInputs.symbol_table`
382 // is `&SymbolTable`, not `Option<&_>` — the pipeline builds it
383 // unconditionally). Synthetic-ctx callers (test helpers) thread
384 // their own through `from_ctx` / direct construction.
385 let symbols = inputs.symbol_table;
386
387 let entry_typedefs = inputs.entry_items.iter().filter_map(|item| match item {
388 TopLevel::TypeDef(td) => Some((None::<&str>, td)),
389 _ => None,
390 });
391 let module_typedefs = inputs.dep_modules.iter().flat_map(|m| {
392 m.type_defs
393 .iter()
394 .map(move |td| (Some(m.prefix.as_str()), td))
395 });
396
397 for (module_prefix, td) in entry_typedefs.chain(module_typedefs) {
398 let TypeDef::Product { name, fields, .. } = td else {
399 continue;
400 };
401 if fields.len() != 1 {
402 continue;
403 }
404 let type_key = match module_prefix {
405 Some(prefix) => crate::ir::TypeKey::in_module(prefix.to_string(), name),
406 None => crate::ir::TypeKey::entry(name),
407 };
408 let Some(canonical_key) = symbols.type_id_of(&type_key) else {
409 // Type isn't in the symbol table — built-ins (Result.Ok
410 // etc.) are excluded by construction; for user types
411 // this is a wiring bug surfaced via the symbol-table
412 // builder, so just skip.
413 continue;
414 };
415 if ir.refined_types.contains_key(&canonical_key) {
416 // Same TypeId already populated — possible if a module
417 // is walked twice through dep aliasing. Skip so we don't
418 // overwrite a verified-witness entry with a predicate-
419 // eval fallback witness.
420 continue;
421 }
422 // Scope the smart-constructor lookup to the same module the
423 // record lives in. Refinement-via-opaque keeps the record
424 // opaque (`exposes opaque [X]`); a smart constructor in any
425 // other module couldn't reach the carrier field anyway.
426 // Without the scope, two modules each declaring a `Natural`
427 // with different predicates would both pick up whichever
428 // smart constructor walked first.
429 let Some(info) =
430 crate::codegen::common::refinement_info_for_in_scope(name, inputs, module_prefix)
431 else {
432 continue;
433 };
434 let invariant = Predicate {
435 free_vars: vec![(
436 info.param_name.to_string(),
437 crate::ir::proof_ir::QuantifierType::Plain(info.carrier_type.to_string()),
438 )],
439 expr: inputs.resolve_expr(info.predicate, module_prefix),
440 };
441 let witness = pick_witness(
442 name,
443 canonical_key,
444 inputs,
445 info.predicate,
446 info.param_name,
447 module_prefix,
448 );
449 // Round-4 finding 1: a `None` witness means we couldn't
450 // exhibit any inhabitant satisfying the predicate. Inserting
451 // the slot anyway makes Dafny silently fall back to
452 // `witness 0` even when the predicate excludes 0 — producing
453 // an unsound subset type. Skip the lift entirely: the
454 // backend will emit a plain `datatype` instead, which is
455 // honest about the missing invariant. The pure-fn / law
456 // paths still typecheck against the plain record.
457 let Some(witness) = witness else {
458 continue;
459 };
460 ir.refined_types.insert(
461 canonical_key,
462 RefinedTypeDecl {
463 name: name.clone(),
464 carrier_type: info.carrier_type.to_string(),
465 carrier_field: info.carrier_field.to_string(),
466 predicate_param: info.param_name.to_string(),
467 invariant,
468 witness: Some(witness),
469 // Filled in immediately below by `populate_refined_type_intervals`,
470 // which runs the interval analysis once over the just-built
471 // `refined_types` map. Left empty here so the two passes share
472 // a single source of truth (`interval::analyze`) instead of
473 // each construction site re-deriving the bound.
474 interval: None,
475 op_classes: Vec::new(),
476 },
477 );
478 }
479
480 // Back-fill each decl's derived interval + per-op classification by
481 // running the existing per-module interval analysis over the map we
482 // just built. Reuses `interval::analyze` verbatim (no forked logic),
483 // so the persisted fact on every `RefinedTypeDecl` is byte-identical
484 // to what `aver compile --explain-passes` reports for the same type.
485 // This makes the bound a queryable fact on the standard refinement-
486 // lower path — the home a future carrier-lowering codegen recognizer
487 // reads via `ctx.proof_ir.refined_types` (TypeId-keyed) without
488 // re-running the analysis behind the diagnostic flag.
489 populate_refined_type_intervals(inputs, ir);
490}
491
492/// Attach the interval analysis result to each `RefinedTypeDecl` in
493/// `ir.refined_types`. Called once at the tail of
494/// [`populate_refined_types`]; the analysis is keyed by the same opaque
495/// `TypeId` the decl map uses, so the join is a direct id lookup.
496fn populate_refined_type_intervals(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
497 let analysis = crate::ir::interval::analyze(&ir.refined_types, inputs);
498 for (type_id, decl) in ir.refined_types.iter_mut() {
499 let Some(per_type) = analysis.types.get(type_id) else {
500 continue;
501 };
502 // `interval_known` distinguishes a recognized bound from the
503 // conservative `Interval::unbounded()` decline; persist `None`
504 // for the decline so consumers don't mistake `[-inf, +inf]` for
505 // a real enclosure.
506 decl.interval = per_type.interval_known.then_some(per_type.interval);
507 decl.op_classes = per_type.ops.clone();
508 }
509}
510
511/// ETAP-2 SLICE 0+1: derive, per refinement-via-opaque type in scope, the
512/// constant interval its smart-constructor invariant proves over the
513/// carrier. The table is keyed by the opaque type's *bare* Aver name (e.g.
514/// `"IntRange"`). The bare-`i64` MIR pass matches a carrier
515/// parameter/local/return slot against this key — it extracts the bare name
516/// from the slot's `MirParam.ty` (which the lowerer fills with the Debug
517/// form `Named { id: …, name: "IntRange" }`, NOT the bare name; see
518/// `bare_named_type` in `bare_i64`) and seeds the slot with the proven bound.
519///
520/// The value is `interval_of_invariant(&predicate)` — byte-identical to the
521/// bound [`populate_refined_type_intervals`] persists on each
522/// `RefinedTypeDecl` (both build the same [`Predicate`] from
523/// [`refinement_info_for_in_scope`] + [`ProofLowerInputs::resolve_expr`] and
524/// run the same `interval` recognizer). No forked logic.
525///
526/// **Fail-closed.** A type whose invariant the recognizer does not
527/// understand returns `interval_known == false`; that entry is OMITTED from
528/// the table entirely, so the MIR pass never sees it and the carrier stays
529/// boxed. A carrier whose proven bound does not `fits_i64` is also kept
530/// (the table carries the raw `(Interval, bool)` so the seed site can apply
531/// `fits_i64` itself — see `carrier_interval` in `bare_i64`).
532pub fn carrier_interval_table(
533 inputs: &ProofLowerInputs,
534) -> HashMap<String, (crate::ir::interval::Interval, bool)> {
535 let mut table = HashMap::new();
536
537 let entry_typedefs = inputs.entry_items.iter().filter_map(|item| match item {
538 TopLevel::TypeDef(td) => Some((None::<&str>, td)),
539 _ => None,
540 });
541 let module_typedefs = inputs.dep_modules.iter().flat_map(|m| {
542 m.type_defs
543 .iter()
544 .map(move |td| (Some(m.prefix.as_str()), td))
545 });
546
547 for (module_prefix, td) in entry_typedefs.chain(module_typedefs) {
548 // Refinement-via-opaque is a single-carrier-field product; mirror the
549 // exact eligibility `populate_refined_types` applies so the keyed
550 // bound is the same fact the proof side carries.
551 let TypeDef::Product { name, fields, .. } = td else {
552 continue;
553 };
554 if fields.len() != 1 {
555 continue;
556 }
557 let Some(info) =
558 crate::codegen::common::refinement_info_for_in_scope(name, inputs, module_prefix)
559 else {
560 continue;
561 };
562 let invariant = Predicate {
563 free_vars: vec![(
564 info.param_name.to_string(),
565 QuantifierType::Plain(info.carrier_type.to_string()),
566 )],
567 expr: inputs.resolve_expr(info.predicate, module_prefix),
568 };
569 let (interval, interval_known) = crate::ir::interval::interval_of_invariant(&invariant);
570 // Fail-closed: an unrecognized invariant (`interval_known == false`)
571 // is OMITTED, so the carrier stays boxed.
572 if !interval_known {
573 continue;
574 }
575 // Key by the bare type name — the `MirParam.ty` string. Two modules
576 // may each declare a same-named carrier with different predicates;
577 // the bare name collides. Keep the FIRST (entry walks before deps),
578 // and on a same-name collision intersect to the tighter common bound
579 // (fail-closed: a narrower interval is always still a valid
580 // over-approximation of either inhabitant set, and a mismatch can
581 // only ever shrink eligibility, never wrongly widen it).
582 table
583 .entry(name.clone())
584 .and_modify(|(iv, known): &mut (crate::ir::interval::Interval, bool)| {
585 *iv = iv.intersect(interval);
586 *known = true;
587 })
588 .or_insert((interval, true));
589 }
590
591 table
592}
593
594/// ETAP-2 multi-field carrier-`i64`: derive, per `(record-type, Int-field)`
595/// pair in scope, the constant interval a MULTI-ARG smart constructor's guard
596/// proves over that field. This generalizes [`carrier_interval_table`] from
597/// the single-`value`-field carrier TYPE to a multi-field record whose 2+-arg
598/// smart constructor bounds each field independently.
599///
600/// The recognized shape is:
601/// ```text
602/// record Coord { x: Int, y: Int } // 2+ Int fields
603/// fn coord(x: Int, y: Int) -> Result<Coord, String>
604/// match <guard conjunction over x, y>
605/// true -> Result.Ok(Coord(x = x, y = y)) // param p_j -> field f_i
606/// false -> Result.Err("...")
607/// ```
608/// The Ok-branch `RecordCreate` maps each constructor PARAM to a record FIELD
609/// (`Coord(x = x, y = y)`). For each field, the guard is split on its
610/// `Bool.and` tree and only the leaf comparisons mentioning that field's bound
611/// param ALONE are kept (a cross-field condition like `x + y <= 50` mentions
612/// two params and is DROPPED — conservative/sound: a narrower per-field bound
613/// is always a valid over-approximation). Running [`interval_of_invariant`]
614/// over those single-var leaves yields the field's interval.
615///
616/// **Fail-closed.** A field with no proven single-var `fits_i64` bound is
617/// OMITTED (the field stays boxed `$AverInt`). A non-`Int` field, a param that
618/// the Ok branch does not bind one-to-one to a field, or an unrecognized guard
619/// all decline. The single-field path ([`carrier_interval_table`]) is
620/// untouched — this table is ADDITIVE and keyed by `(record, field)`.
621///
622/// Returns a map keyed by `(bare record name, field name)`.
623pub fn field_carrier_interval_table(
624 inputs: &ProofLowerInputs,
625) -> HashMap<(String, String), (crate::ir::interval::Interval, bool)> {
626 let mut table = HashMap::new();
627
628 let entry_typedefs = inputs.entry_items.iter().filter_map(|item| match item {
629 TopLevel::TypeDef(td) => Some((None::<&str>, td)),
630 _ => None,
631 });
632 let module_typedefs = inputs.dep_modules.iter().flat_map(|m| {
633 m.type_defs
634 .iter()
635 .map(move |td| (Some(m.prefix.as_str()), td))
636 });
637
638 for (module_prefix, td) in entry_typedefs.chain(module_typedefs) {
639 let TypeDef::Product { name, fields, .. } = td else {
640 continue;
641 };
642 // Single-field products are the existing carrier-TYPE path; skip them
643 // here so the two tables never both claim the same record.
644 if fields.len() < 2 {
645 continue;
646 }
647 // Every field must be a plain `Int` for the multi-field i64 erasure;
648 // a record mixing Int and non-Int fields keeps its non-Int fields
649 // boxed (only the Int fields with a proven bound become eligible).
650 let Some((ctor, ctor_prefix)) =
651 find_multi_field_smart_ctor(name, fields, inputs, module_prefix)
652 else {
653 continue;
654 };
655 // Map record FIELD name -> the constructor PARAM name that feeds it,
656 // read from the Ok-branch `RecordCreate`.
657 let field_to_param = match ctor_field_param_map(ctor, name, fields) {
658 Some(m) => m,
659 None => continue,
660 };
661 let guard = ctor_guard_predicate(ctor);
662 let Some(guard) = guard else { continue };
663 for (fname, ftype) in fields {
664 if ftype.trim() != "Int" {
665 continue;
666 }
667 let Some(param) = field_to_param.get(fname) else {
668 continue;
669 };
670 // Project the guard onto single-variable leaves mentioning ONLY
671 // this param. A cross-field leaf (two distinct params) is dropped.
672 let resolved_guard = inputs.resolve_expr(guard, ctor_prefix);
673 let leaves =
674 crate::codegen::common::flatten_bool_and_conjuncts_resolved(&resolved_guard);
675 let single_var: Vec<_> = leaves
676 .into_iter()
677 .filter(|leaf| resolved_leaf_mentions_only(leaf, param))
678 .collect();
679 if single_var.is_empty() {
680 continue;
681 }
682 // Rebuild a conjunction predicate over the kept leaves and run the
683 // SAME interval recognizer the single-field path uses.
684 let conj = rebuild_bool_and(single_var);
685 let invariant = Predicate {
686 free_vars: vec![(param.clone(), QuantifierType::Plain("Int".to_string()))],
687 expr: conj,
688 };
689 let (interval, interval_known) = crate::ir::interval::interval_of_invariant(&invariant);
690 if !interval_known {
691 continue;
692 }
693 // Same collision discipline as the type-keyed table: on a
694 // same-`(record, field)` collision across modules, intersect to
695 // the tighter common bound (fail-closed).
696 table
697 .entry((name.clone(), fname.clone()))
698 .and_modify(|(iv, known): &mut (crate::ir::interval::Interval, bool)| {
699 *iv = iv.intersect(interval);
700 *known = true;
701 })
702 .or_insert((interval, true));
703 }
704 }
705
706 table
707}
708
709/// Find the single recognized multi-arg smart constructor for `record_name`:
710/// a pure fn `mk(p1, ..., pN) -> Result<Rec, String>` whose body is a single
711/// two-arm `true -> Result.Ok(Rec(...)) | false -> Result.Err(_)` match. The
712/// param count must be >= 2 (a one-arg ctor is the single-field carrier path).
713/// Returns the `&FnDef` plus the module scope it was found in.
714fn find_multi_field_smart_ctor<'a>(
715 record_name: &str,
716 fields: &[(String, String)],
717 inputs: &ProofLowerInputs<'a>,
718 record_scope: Option<&str>,
719) -> Option<(&'a FnDef, Option<&'a str>)> {
720 let entry_fns = inputs.entry_items.iter().filter_map(|item| match item {
721 TopLevel::FnDef(fd) => Some((None::<&str>, fd)),
722 _ => None,
723 });
724 let module_fns = inputs.dep_modules.iter().flat_map(|m| {
725 m.fn_defs
726 .iter()
727 .map(move |fd| (Some(m.prefix.as_str()), fd))
728 });
729 for (scope, fd) in entry_fns.chain(module_fns) {
730 // The constructor lives in the same module as the record (opaque
731 // refinement is single-module); skip a same-named fn in another scope.
732 if scope != record_scope {
733 continue;
734 }
735 if !fd.return_type.starts_with("Result<") {
736 continue;
737 }
738 if !fd.return_type[7..].starts_with(record_name) {
739 continue;
740 }
741 if fd.params.len() < 2 {
742 continue;
743 }
744 let stmts = fd.body.stmts();
745 if stmts.len() != 1 {
746 continue;
747 }
748 let crate::ast::Stmt::Expr(body_expr) = &stmts[0] else {
749 continue;
750 };
751 let Expr::Match { arms, .. } = &body_expr.node else {
752 continue;
753 };
754 if !is_multi_field_ok_err_match(arms, record_name, fields) {
755 continue;
756 }
757 return Some((fd, scope));
758 }
759 None
760}
761
762/// True iff `arms` is the canonical multi-field smart-ctor shape:
763/// `true -> Result.Ok(Rec(f_i = p_j, ...))` covering EVERY field, and
764/// `false -> Result.Err(_)`.
765fn is_multi_field_ok_err_match(
766 arms: &[crate::ast::MatchArm],
767 record_name: &str,
768 fields: &[(String, String)],
769) -> bool {
770 if arms.len() != 2 {
771 return false;
772 }
773 let mut true_ok = false;
774 let mut false_err = false;
775 for arm in arms {
776 match &arm.pattern {
777 crate::ast::Pattern::Literal(Literal::Bool(true)) => {
778 if multi_field_ok_constructor(&arm.body, record_name, fields).is_some() {
779 true_ok = true;
780 }
781 }
782 crate::ast::Pattern::Literal(Literal::Bool(false)) => {
783 if multi_field_is_err_constructor(&arm.body) {
784 false_err = true;
785 }
786 }
787 _ => return false,
788 }
789 }
790 true_ok && false_err
791}
792
793/// Local mirror of `common::is_err_constructor` (which is private): the arm
794/// body is `Result.Err(_)`.
795fn multi_field_is_err_constructor(expr: &Spanned<Expr>) -> bool {
796 match &expr.node {
797 Expr::Constructor(name, Some(_)) => name == "Result.Err",
798 Expr::FnCall(callee, args) if args.len() == 1 => {
799 matches!(expr_to_dotted_name(&callee.node), Some(name) if name == "Result.Err")
800 }
801 _ => false,
802 }
803}
804
805/// Inspect a `Result.Ok(Rec(f_i = p_j, ...))` arm body and return the
806/// field-name -> param-name map when every field is set to a bare identifier.
807/// `None` if the body is not the expected `Result.Ok` of a `RecordCreate` of
808/// `record_name` covering exactly the declared fields with identifier values.
809fn multi_field_ok_constructor(
810 expr: &Spanned<Expr>,
811 record_name: &str,
812 fields: &[(String, String)],
813) -> Option<HashMap<String, String>> {
814 let (ctor_name, ctor_arg_node) = match &expr.node {
815 Expr::Constructor(name, Some(arg)) => (name.clone(), &arg.node),
816 Expr::FnCall(callee, args) if args.len() == 1 => {
817 let name = expr_to_dotted_name(&callee.node)?;
818 (name, &args[0].node)
819 }
820 _ => return None,
821 };
822 if ctor_name != "Result.Ok" {
823 return None;
824 }
825 let (t, create_fields) = match ctor_arg_node {
826 Expr::RecordCreate { type_name, fields } => (type_name.as_str(), fields),
827 _ => return None,
828 };
829 if t != record_name || create_fields.len() != fields.len() {
830 return None;
831 }
832 let mut map = HashMap::new();
833 for (fname, fvalue) in create_fields {
834 let param = match &fvalue.node {
835 Expr::Ident(n) | Expr::Resolved { name: n, .. } => n.clone(),
836 _ => return None,
837 };
838 map.insert(fname.clone(), param);
839 }
840 // Every declared field must be assigned by the Ok branch.
841 if fields.iter().any(|(fname, _)| !map.contains_key(fname)) {
842 return None;
843 }
844 Some(map)
845}
846
847/// The field -> param map of the smart constructor's Ok branch (the
848/// `true -> Result.Ok(Rec(...))` arm). Walks the body's single match.
849fn ctor_field_param_map(
850 fd: &FnDef,
851 record_name: &str,
852 fields: &[(String, String)],
853) -> Option<HashMap<String, String>> {
854 let stmts = fd.body.stmts();
855 let crate::ast::Stmt::Expr(body_expr) = stmts.first()? else {
856 return None;
857 };
858 let Expr::Match { arms, .. } = &body_expr.node else {
859 return None;
860 };
861 for arm in arms {
862 if matches!(
863 &arm.pattern,
864 crate::ast::Pattern::Literal(Literal::Bool(true))
865 ) {
866 return multi_field_ok_constructor(&arm.body, record_name, fields);
867 }
868 }
869 None
870}
871
872/// The smart constructor's guard predicate (the `Match` subject the Ok/Err
873/// arms branch on).
874fn ctor_guard_predicate(fd: &FnDef) -> Option<&Spanned<Expr>> {
875 let stmts = fd.body.stmts();
876 let crate::ast::Stmt::Expr(body_expr) = stmts.first()? else {
877 return None;
878 };
879 let Expr::Match { subject, .. } = &body_expr.node else {
880 return None;
881 };
882 Some(subject)
883}
884
885/// True iff every identifier leaf mentioned in `leaf` is `param` (so the leaf
886/// is a SINGLE-VARIABLE condition over that one param). A leaf naming two
887/// distinct params (a cross-field condition like `x + y <= 50`) returns false
888/// and is dropped by the caller — conservative/sound per-field projection.
889fn resolved_leaf_mentions_only(leaf: &Spanned<crate::ir::hir::ResolvedExpr>, param: &str) -> bool {
890 let mut only = true;
891 let mut saw = false;
892 collect_resolved_idents(leaf, &mut |name| {
893 if name == param {
894 saw = true;
895 } else {
896 only = false;
897 }
898 });
899 only && saw
900}
901
902/// Walk a resolved expression, invoking `f` for every identifier leaf
903/// (`Ident` / `Resolved`).
904fn collect_resolved_idents(e: &Spanned<crate::ir::hir::ResolvedExpr>, f: &mut impl FnMut(&str)) {
905 use crate::ir::hir::ResolvedExpr;
906 match &e.node {
907 ResolvedExpr::Ident(n) | ResolvedExpr::Resolved { name: n, .. } => f(n),
908 ResolvedExpr::BinOp(_, l, r) => {
909 collect_resolved_idents(l, f);
910 collect_resolved_idents(r, f);
911 }
912 ResolvedExpr::Neg(i) => collect_resolved_idents(i, f),
913 ResolvedExpr::Call(_, args) => {
914 for a in args {
915 collect_resolved_idents(a, f);
916 }
917 }
918 ResolvedExpr::Attr(o, _) => collect_resolved_idents(o, f),
919 _ => {}
920 }
921}
922
923/// Rebuild a `Bool.and` conjunction over the kept single-variable leaves.
924/// A single leaf returns itself; >1 fold into nested `Bool.and(...)` calls,
925/// matching the shape [`interval_of_invariant`] recognizes.
926fn rebuild_bool_and(
927 mut leaves: Vec<Spanned<crate::ir::hir::ResolvedExpr>>,
928) -> Spanned<crate::ir::hir::ResolvedExpr> {
929 use crate::ir::hir::{ResolvedCallee, ResolvedExpr};
930 let mut acc = leaves.remove(0);
931 for leaf in leaves {
932 let line = acc.line;
933 acc = Spanned::new(
934 ResolvedExpr::Call(
935 ResolvedCallee::Builtin("Bool.and".to_string()),
936 vec![acc, leaf],
937 ),
938 line,
939 );
940 }
941 acc
942}
943
944/// ETAP-2 multi-field carrier-`i64`: the per-`(record, field)` ELIGIBLE map —
945/// [`field_carrier_interval_table`] tightened the same way the single-field
946/// path tightens its proven-bound set. An entry survives only when its bound
947/// is recognized AND `fits_i64` AND the owning record is NOT demoted by any
948/// whole-program scan ([`multi_field_record_demotions`]):
949/// - the record is constructed UNGATED (a bare `RecordCreate` outside its
950/// own smart-ctor whose args are not all in-bounds literals) — that bypass
951/// could store an out-of-`i64` value the construct bridge would TRAP on;
952/// - the record (or a record reaching it) is used as a `Map` KEY — the
953/// Map-key codegen was not updated for the i64-erased fields;
954/// - the record (or a record reaching it) is used DIRECTLY as a `Map` VALUE
955/// (or through a record field / `Option` / `Result` that keeps it an inline
956/// struct ref in the values array) — that trips a separate, pre-existing
957/// record-as-Map-value validation bug, so the whole record stays boxed
958/// there. A carrier used as a `List` / `Vector` / `Tuple` ELEMENT now STAYS
959/// native: the per-field record eq/hash helpers dispatch a raw `i64.eq` /
960/// `i32.wrap_i64` for its i64-erased fields, so `List<Coord>` keeps
961/// `(field i64)(field i64)` elements that compile + run native. (`Option` /
962/// `Result` payloads are NOT demoted either — they hold the element as an
963/// inline struct ref, so the smart-ctor boundary
964/// `coord(...) -> Result<Coord, String>` keeps the native-i64 win.)
965///
966/// A demoted record keeps EVERY field boxed (the whole struct stays the
967/// pre-slice all-`$AverInt` layout). wasm-gc only; the Rust path passes the
968/// empty registry and never erases a field.
969///
970/// Returns a map keyed by `(bare record name, field name)`; an empty map
971/// reproduces the pre-slice all-`$AverInt` multi-field record byte-for-byte.
972pub fn field_carrier_eligible_intervals(
973 inputs: &ProofLowerInputs,
974 instantiations: &crate::ir::mir::InstantiationRegistry,
975) -> HashMap<(String, String), (crate::ir::interval::Interval, bool)> {
976 let table = field_carrier_interval_table(inputs);
977 if table.is_empty() {
978 return table;
979 }
980 // Proven-bound candidates: bound recognized AND `fits_i64`. The set of
981 // RECORD names with at least one such field drives the demotion scans
982 // (a demotion is per-record — a record constructed ungated / used as a
983 // Map key keeps ALL its fields boxed).
984 let record_candidates: HashSet<String> = table
985 .iter()
986 .filter(|(_, (iv, known))| *known && iv.fits_i64())
987 .map(|((rec, _), _)| rec.clone())
988 .collect();
989 if std::env::var("AVER_CARRIER_I64_SKIP_DEMOTION").is_ok() {
990 return table
991 .into_iter()
992 .filter(|((rec, _), (iv, known))| {
993 *known && iv.fits_i64() && record_candidates.contains(rec)
994 })
995 .collect();
996 }
997 let demoted_records =
998 multi_field_record_demotions(inputs, &record_candidates, &table, instantiations);
999 table
1000 .into_iter()
1001 .filter(|((rec, _), (iv, known))| {
1002 *known
1003 && iv.fits_i64()
1004 && record_candidates.contains(rec)
1005 && !demoted_records.contains(rec)
1006 })
1007 .collect()
1008}
1009
1010/// ETAP-2 multi-field carrier-`i64`: the RECORD-level fail-closed demotion
1011/// scan — the multi-field generalization of [`carrier_eligibility_demotions`].
1012/// A multi-field bounded record is demoted (every field kept boxed) when:
1013/// - **Scan 1 (ungated construction).** A `RecordCreate` / `RecordUpdate` of
1014/// the record in a fn OTHER than its recognized multi-arg smart constructor
1015/// can smuggle an out-of-`i64` value past the per-field gate (the construct
1016/// bridge `__aint_to_i64_checked` would TRAP). SAFE only when every field
1017/// argument is a constant literal provably inside that field's proven
1018/// interval; any non-literal / out-of-range field argument demotes.
1019/// - **Scan 2 (Map-key usage).** The record — directly or transitively as a
1020/// field of a record/Option/List/Tuple used as a Map KEY — reaches a
1021/// `Map<K, V>` key position; the Map-key codegen still expects the boxed
1022/// struct. Driven by the inference-complete resolved Map-key types, with
1023/// the textual annotation scan as a cheap backstop. Both reuse the SAME
1024/// `carriers_reachable_from` / `collect_map_key_carriers` closures the
1025/// single-field path uses.
1026/// - **Scan 3 (direct Map-VALUE usage).** The record reaches a `Map` VALUE
1027/// DIRECTLY (or through a record field / `Option` / `Result` that keeps it
1028/// an inline struct ref in the values array). That trips a separate,
1029/// pre-existing record-as-Map-value validation bug, so the record stays
1030/// boxed there. The walk STOPS at a `List` / `Vector` / `Tuple` boundary: a
1031/// carrier used as such a container ELEMENT now stays native i64 (the
1032/// per-field record eq/hash helpers dispatch the raw `i64.eq` /
1033/// `i32.wrap_i64` for an i64-erased field), so a carrier reachable only
1034/// THROUGH such a container — e.g. a `Map<K, List<Coord>>` value — is NOT
1035/// demoted. Driven by the inference-complete instantiation registry (every
1036/// `Map` the program uses), via `carriers_reachable_as_map_value`.
1037///
1038/// Fail-closed: a trip can only ever SHRINK the eligible set.
1039fn multi_field_record_demotions(
1040 inputs: &ProofLowerInputs,
1041 candidates: &HashSet<String>,
1042 field_intervals: &HashMap<(String, String), (crate::ir::interval::Interval, bool)>,
1043 instantiations: &crate::ir::mir::InstantiationRegistry,
1044) -> HashSet<String> {
1045 let mut demoted: HashSet<String> = HashSet::new();
1046 if candidates.is_empty() {
1047 return demoted;
1048 }
1049
1050 // Map each candidate record to its recognized multi-arg smart-ctor fn name
1051 // (the only construct site that gates the fields). A record with no
1052 // recognizable smart-ctor never reached `field_carrier_interval_table`, so
1053 // every candidate has one — but resolve defensively and demote on failure.
1054 let mut ctor_fn_of: HashMap<String, String> = HashMap::new();
1055 let record_defs = collect_product_defs(inputs);
1056 for name in candidates {
1057 let Some((fields, scope)) = record_defs.get(name) else {
1058 demoted.insert(name.clone());
1059 continue;
1060 };
1061 match find_multi_field_smart_ctor(name, fields, inputs, *scope) {
1062 Some((ctor, _)) => {
1063 ctor_fn_of.insert(name.clone(), ctor.name.clone());
1064 }
1065 None => {
1066 demoted.insert(name.clone());
1067 }
1068 }
1069 }
1070
1071 // ---- Scan 1: ungated construction --------------------------------------
1072 let all_fn_defs = inputs
1073 .entry_items
1074 .iter()
1075 .filter_map(|it| match it {
1076 TopLevel::FnDef(fd) => Some(fd),
1077 _ => None,
1078 })
1079 .chain(inputs.dep_modules.iter().flat_map(|m| m.fn_defs.iter()));
1080 for fd in all_fn_defs {
1081 for stmt in fd.body.stmts() {
1082 let expr = match stmt {
1083 crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => e,
1084 };
1085 carrier_walk_expr(expr, &mut |e| {
1086 let (type_name, create_fields): (&String, &[(String, Spanned<Expr>)]) = match e {
1087 Expr::RecordCreate { type_name, fields } => (type_name, fields),
1088 Expr::RecordUpdate {
1089 type_name, updates, ..
1090 } => (type_name, updates),
1091 _ => return,
1092 };
1093 if !candidates.contains(type_name) {
1094 return;
1095 }
1096 // The construct inside the record's own smart-ctor is gated.
1097 if ctor_fn_of.get(type_name) == Some(&fd.name) {
1098 return;
1099 }
1100 // Outside the smart-ctor: safe iff EVERY provided field value is
1101 // a constant literal inside that field's proven interval (an
1102 // in-bounds literal cannot smuggle an out-of-bound value past
1103 // the gate). A `RecordUpdate` that omits a bounded field copies
1104 // it from the (already-gated) base, which is safe too.
1105 let all_safe = create_fields.iter().all(|(fname, value)| {
1106 match field_intervals.get(&(type_name.clone(), fname.clone())) {
1107 // A non-bounded field (no eligible interval) is stored
1108 // boxed regardless, so it can't smuggle an i64-overflow.
1109 None => true,
1110 Some((iv, true)) => literal_in_interval(value, *iv),
1111 Some((_, false)) => false,
1112 }
1113 });
1114 if !all_safe {
1115 demoted.insert(type_name.clone());
1116 }
1117 });
1118 }
1119 }
1120
1121 // ---- Scan 2: Map-key usage (direct + transitive) -----------------------
1122 let record_fields = collect_record_fields(inputs);
1123 for (key, _value) in &instantiations.maps {
1124 carriers_reachable_from(
1125 key,
1126 candidates,
1127 &record_fields,
1128 &mut HashSet::new(),
1129 &mut demoted,
1130 );
1131 }
1132 for ty_str in all_type_annotations(inputs) {
1133 let ty = crate::types::parse_type_str(&ty_str);
1134 collect_map_key_carriers(&ty, candidates, &record_fields, &mut demoted);
1135 }
1136
1137 // ---- Scan 3: Map-VALUE usage (direct + transitive, list/vec/tuple-stopped)
1138 // A multi-field carrier reachable DIRECTLY as a `Map` VALUE (or through a
1139 // record field / `Option` / `Result` that keeps it an inline struct ref in
1140 // the values backing array) keeps all its fields BOXED — that case trips a
1141 // separate, pre-existing record-as-Map-value validation bug that is out of
1142 // scope here, so we fail closed exactly as before.
1143 //
1144 // A carrier used as a `List` / `Vector` / `Tuple` ELEMENT now STAYS native
1145 // i64: the per-field record eq/hash helpers dispatch a raw `i64.eq` /
1146 // `i32.wrap_i64` for an i64-erased field (gated on `is_eligible_carrier_
1147 // field`), so `List<Coord>` keeps `(field i64)(field i64)` elements and
1148 // `List.contains` / `==` over them compiles and runs native. The reachability
1149 // walk therefore STOPS at a `List` / `Vector` / `Tuple` boundary — a carrier
1150 // only reachable THROUGH such a container (e.g. `Map<K, List<Coord>>`, where
1151 // the Map value is a list ref whose elements are native) is NOT demoted.
1152 //
1153 // `Option` / `Result` payloads are likewise inline struct refs, so a carrier
1154 // behind them as a Map value stays eligible (the common smart-ctor boundary
1155 // `coord(...) -> Result<Coord, String>` keeps the native-i64 win).
1156 let mut map_value_seen: HashSet<String> = HashSet::new();
1157 for (_key, value) in &instantiations.maps {
1158 carriers_reachable_as_map_value(
1159 value,
1160 candidates,
1161 &record_fields,
1162 &mut map_value_seen,
1163 &mut demoted,
1164 );
1165 }
1166
1167 demoted
1168}
1169
1170/// ETAP-2 multi-field carrier-`i64`: the Map-VALUE reachability walk for the
1171/// demotion scan — like [`carriers_reachable_from`] but it STOPS at a `List` /
1172/// `Vector` / `Tuple` boundary. A carrier stored as a `List` / `Vector` /
1173/// `Tuple` element keeps its i64 fields native (the container's per-element
1174/// eq/hash dispatches the raw i64 ops), so reaching one only THROUGH such a
1175/// container (e.g. a `Map<K, List<Coord>>` value, a list-of-tuples value) does
1176/// NOT demote it. A carrier reachable as a DIRECT Map value, or through a
1177/// record field / `Option` / `Result` (all of which hold it as an inline struct
1178/// ref in the values backing array), still demotes — the record-as-Map-value
1179/// validation bug that motivates this scan is unchanged by the container slice.
1180fn carriers_reachable_as_map_value(
1181 value: &crate::ast::Type,
1182 candidates: &HashSet<String>,
1183 record_fields: &HashMap<String, Vec<String>>,
1184 seen: &mut HashSet<String>,
1185 demoted: &mut HashSet<String>,
1186) {
1187 use crate::ast::Type;
1188 let Some(name) = value.named_name() else {
1189 match value {
1190 // `Option` / `Result` keep the payload an inline struct ref in the
1191 // Map values array, so a carrier behind them is still a direct
1192 // value — descend.
1193 Type::Option(a) => {
1194 carriers_reachable_as_map_value(a, candidates, record_fields, seen, demoted);
1195 }
1196 Type::Result(a, b) => {
1197 carriers_reachable_as_map_value(a, candidates, record_fields, seen, demoted);
1198 carriers_reachable_as_map_value(b, candidates, record_fields, seen, demoted);
1199 }
1200 // A nested `Map` value is itself a Map values array — descend into
1201 // its value (its key is a separate Scan-2 concern, handled there).
1202 Type::Map(_k, v) => {
1203 carriers_reachable_as_map_value(v, candidates, record_fields, seen, demoted);
1204 }
1205 // `List` / `Vector` / `Tuple` make the carrier a native container
1206 // ELEMENT (now eligible) — STOP, do not demote anything inside.
1207 Type::List(_) | Type::Vector(_) | Type::Tuple(_) => {}
1208 _ => {}
1209 }
1210 return;
1211 };
1212 if !seen.insert(name.to_string()) {
1213 return;
1214 }
1215 if candidates.contains(name) {
1216 demoted.insert(name.to_string());
1217 }
1218 if let Some(fields) = record_fields.get(name) {
1219 for field_ty in fields {
1220 let parsed = crate::types::parse_type_str(field_ty);
1221 carriers_reachable_as_map_value(&parsed, candidates, record_fields, seen, demoted);
1222 }
1223 }
1224}
1225
1226/// A candidate record's declarations as the demotion scan needs them: its
1227/// `(field, type)` list plus the module scope it was found in (`None` = entry).
1228type ProductDef<'a> = (&'a [(String, String)], Option<&'a str>);
1229
1230/// Bare `Product` type name → its [`ProductDef`]. Lets the demotion scan
1231/// re-locate a candidate record's field declarations + smart constructor.
1232fn collect_product_defs<'a>(inputs: &ProofLowerInputs<'a>) -> HashMap<String, ProductDef<'a>> {
1233 let mut out: HashMap<String, ProductDef<'a>> = HashMap::new();
1234 for item in inputs.entry_items {
1235 if let TopLevel::TypeDef(TypeDef::Product { name, fields, .. }) = item {
1236 out.entry(name.clone()).or_insert((fields.as_slice(), None));
1237 }
1238 }
1239 for m in inputs.dep_modules {
1240 for td in &m.type_defs {
1241 if let TypeDef::Product { name, fields, .. } = td {
1242 out.entry(name.clone())
1243 .or_insert((fields.as_slice(), Some(m.prefix.as_str())));
1244 }
1245 }
1246 }
1247 out
1248}
1249
1250/// A `RecordCreate`/`RecordUpdate` field value is a constant integer literal
1251/// (`5` or `-5`) provably within the proven interval `iv`. Any other shape
1252/// (a param, a call, arithmetic) is not a provable constant ⇒ `false`.
1253fn literal_in_interval(value: &Spanned<Expr>, iv: crate::ir::interval::Interval) -> bool {
1254 let k: i128 = match &value.node {
1255 Expr::Literal(Literal::Int(n)) => *n as i128,
1256 Expr::Neg(inner) => match &inner.node {
1257 Expr::Literal(Literal::Int(n)) => -(*n as i128),
1258 _ => return false,
1259 },
1260 _ => return false,
1261 };
1262 iv.contains_point(k)
1263}
1264
1265/// ETAP-2 carrier-`i64` SLICE 2b FOLLOW-UP: the fail-closed eligibility
1266/// tightening. The bare carrier interval ([`carrier_interval_table`]) proves
1267/// only that the smart-constructor's invariant `fits_i64`; it does NOT prove
1268/// that every value of the carrier type actually went through that gate, nor
1269/// that the carrier's codegen is exercised only in positions the i64 erasure
1270/// supports. This scan removes a carrier from the eligible set when either
1271/// assumption is violated, so the carrier stays boxed (`$AverInt`) — the
1272/// safe, pre-slice representation that the VM and the boxed wasm-gc path
1273/// agree on.
1274///
1275/// Two whole-program scans, both fail-closed (a trip can only ever SHRINK the
1276/// eligible set, never widen it):
1277///
1278/// 1. **Ungated construction (closes the bare-constructor bypass).** A bare
1279/// record constructor `IntRange(value = n)` callable in the defining
1280/// module bypasses the smart-ctor's `0 <= n <= 100` gate. With `n`
1281/// overflowing `i64` the VM keeps full precision but the wasm-gc construct
1282/// bridge `__aint_to_i64_checked` TRAPS. A carrier `RecordCreate`d outside
1283/// its own recognized smart-constructor function is therefore ineligible —
1284/// UNLESS the construct's carrier-field argument is a constant literal that
1285/// provably lies inside the carrier's proven interval. Such a literal
1286/// construct (`IntRange(value = 0)` against a `[0, 100]` bound) cannot
1287/// smuggle an out-of-bound / i64-overflowing value past the gate, so it is
1288/// SAFE and does not demote — that pattern is exactly the in-bounds Err
1289/// fallback the slice's own carriers use (`unwrap`'s `IntRange(value = 0)`).
1290/// A non-literal argument, or a literal OUTSIDE the interval, is ungated
1291/// and DOES demote (this is the `mk(n) = IntRange(value = n)` bypass).
1292///
1293/// 2. **Map-key usage (closes the i64-erased Map-KEY codegen gap).** A
1294/// carrier used as a `Map` KEY type — directly (`Map<IntRange, V>`) or
1295/// transitively as a field of a record/type used as a key
1296/// (`Map<Coord, V>` with `Coord { x: IntRange }`) — fails wasm validation,
1297/// because the Map-key codegen was not updated for the i64-erased carrier.
1298/// Such a carrier is ineligible (the boxed key path it used before still
1299/// compiles). Map VALUES are unaffected and stay eligible.
1300///
1301/// The COMPLETE source of truth for which carriers are Map keys is
1302/// `resolved_map_keys` — the resolved `Map<K, V>` key types harvested from
1303/// the typed MIR (`ir::mir::discover_instantiations`). Because those types
1304/// come from inference, a carrier used as a key reaches this scan whether
1305/// its `Map` type was written as a fn-signature annotation, a LOCAL-BINDING
1306/// annotation (`m: Map<IntRange, Int> = …`), or NO annotation at all
1307/// (`m = Map.set({}, c, 5)`). A textual annotation scan cannot see the
1308/// last two; the resolved key type can. The annotation scan is retained as
1309/// a cheap fail-closed backstop only.
1310///
1311/// Returns the set of carrier type names (bare names, the same keys
1312/// [`carrier_interval_table`] uses) to REMOVE from the eligible set. The
1313/// caller subtracts this from the proven-bound set.
1314///
1315/// `resolved_map_keys`: every `Map<K, _>` key type the program instantiates,
1316/// per the typed-MIR instantiation registry. This is the inference-complete
1317/// Map-key signal that drives Scan 2.
1318pub fn carrier_eligibility_demotions(
1319 inputs: &ProofLowerInputs,
1320 candidates: &HashSet<String>,
1321 intervals: &HashMap<String, (crate::ir::interval::Interval, bool)>,
1322 resolved_map_keys: &[crate::ast::Type],
1323) -> HashSet<String> {
1324 let mut demoted: HashSet<String> = HashSet::new();
1325 if candidates.is_empty() {
1326 return demoted;
1327 }
1328
1329 // Map each candidate carrier to its recognized smart-constructor fn name.
1330 // `refinement_info_for_in_scope` is THE source of truth for "the
1331 // smart-ctor function" — the exact fn the interval recognizer keyed off.
1332 // For the wasm-gc path `dep_modules` is empty (the program is flattened
1333 // into `entry_items`), so the entry scope (`None`) resolves every
1334 // carrier; we still consult dep scopes for generality.
1335 let mut ctor_fn_of: HashMap<String, String> = HashMap::new();
1336 for name in candidates {
1337 if let Some(info) = crate::codegen::common::refinement_info_for_in_scope(name, inputs, None)
1338 {
1339 ctor_fn_of.insert(name.clone(), info.constructor_fn.to_string());
1340 } else {
1341 for m in inputs.dep_modules {
1342 if let Some(info) = crate::codegen::common::refinement_info_for_in_scope(
1343 name,
1344 inputs,
1345 Some(m.prefix.as_str()),
1346 ) {
1347 ctor_fn_of.insert(name.clone(), info.constructor_fn.to_string());
1348 break;
1349 }
1350 }
1351 }
1352 }
1353
1354 // ---- Scan 1: ungated construction --------------------------------------
1355 // Walk every fn body in the program. A `RecordCreate { type_name }` whose
1356 // `type_name` is a candidate carrier and which sits in a fn OTHER than
1357 // that carrier's smart-constructor is an ungated construct ⇒ demote,
1358 // UNLESS its carrier-field argument is a constant literal provably inside
1359 // the carrier's proven interval (an in-bounds literal can't smuggle an
1360 // out-of-bound value past the gate — fail-closed but not over-eager). If
1361 // we can't cleanly identify the smart-ctor (no entry in `ctor_fn_of`),
1362 // every non-literal-safe construct demotes — fail-closed.
1363 let all_fn_defs = inputs
1364 .entry_items
1365 .iter()
1366 .filter_map(|it| match it {
1367 TopLevel::FnDef(fd) => Some(fd),
1368 _ => None,
1369 })
1370 .chain(inputs.dep_modules.iter().flat_map(|m| m.fn_defs.iter()));
1371 for fd in all_fn_defs {
1372 for stmt in fd.body.stmts() {
1373 let expr = match stmt {
1374 crate::ast::Stmt::Binding(_, _, e) | crate::ast::Stmt::Expr(e) => e,
1375 };
1376 carrier_walk_expr(expr, &mut |e| {
1377 // Both a fresh construct (`RecordCreate`) and a record-update
1378 // (`RecordUpdate`, which sets the carrier's only field to an
1379 // arbitrary value) can smuggle a value past the smart-ctor
1380 // gate; treat both the same.
1381 let (type_name, fields): (&String, &[(String, Spanned<Expr>)]) = match e {
1382 Expr::RecordCreate { type_name, fields } => (type_name, fields),
1383 Expr::RecordUpdate {
1384 type_name, updates, ..
1385 } => (type_name, updates),
1386 _ => return,
1387 };
1388 if !candidates.contains(type_name) {
1389 return;
1390 }
1391 // The construct inside the carrier's own smart-ctor is the
1392 // gated one — never demotes.
1393 if ctor_fn_of.get(type_name) == Some(&fd.name) {
1394 return;
1395 }
1396 // Outside the smart-ctor: safe iff the (single) carrier field
1397 // is a constant literal inside the proven interval.
1398 let safe_literal = matches!(
1399 intervals.get(type_name),
1400 Some((iv, true)) if construct_arg_is_in_interval(fields, *iv)
1401 );
1402 if !safe_literal {
1403 demoted.insert(type_name.clone());
1404 }
1405 });
1406 }
1407 }
1408
1409 // ---- Scan 2: Map-key usage (direct + transitive) -----------------------
1410 // Build, per record type, the set of carrier names reachable through its
1411 // (transitive) fields, so a carrier nested inside a record used as a key
1412 // is demoted too.
1413 let record_fields = collect_record_fields(inputs);
1414
1415 // PRIMARY (complete) source of truth: the RESOLVED Map-key types harvested
1416 // from the typed MIR (`discover_instantiations`). Every `Map<K, V>` the
1417 // program actually instantiates is here — including ones whose key type is
1418 // only known after INFERENCE (a local-binding `m: Map<…>` annotation that
1419 // the textual scan never walks, or a fully inferred `m = Map.set({}, c,
1420 // 5)` with no annotation at all). A textual annotation scan is
1421 // fundamentally incomplete for these; the resolved key type is not. Demote
1422 // every carrier reachable from each resolved key (directly, or
1423 // transitively through a record / Option / List / Tuple field — the same
1424 // `carriers_reachable_from` closure the annotation path uses).
1425 for key in resolved_map_keys {
1426 carriers_reachable_from(
1427 key,
1428 candidates,
1429 &record_fields,
1430 &mut HashSet::new(),
1431 &mut demoted,
1432 );
1433 }
1434
1435 // BACKSTOP (cheap): the original textual scan over fn-param / fn-return /
1436 // record-field annotations. Redundant with the resolved-IR path above for
1437 // any Map the MIR sees, but kept so a Map type that appears ONLY in an
1438 // annotation position the MIR instantiation walk doesn't reach (e.g. a
1439 // signature whose body never constructs/uses the Map) still trips. It can
1440 // only ever ADD to `demoted` (fail-closed).
1441 for ty_str in all_type_annotations(inputs) {
1442 let ty = crate::types::parse_type_str(&ty_str);
1443 collect_map_key_carriers(&ty, candidates, &record_fields, &mut demoted);
1444 }
1445
1446 demoted
1447}
1448
1449/// A single-carrier-field `RecordCreate`'s field argument is a constant
1450/// integer literal provably within the proven interval `iv`. Handles the
1451/// bare literal `IntRange(value = 0)` and the negated literal
1452/// `IntRange(value = -5)` (parsed as `Expr::Neg(Literal(Int))`). Any other
1453/// shape (a parameter, a call, an arithmetic expression) is NOT a provable
1454/// constant ⇒ returns `false` ⇒ the construct is treated as ungated.
1455fn construct_arg_is_in_interval(
1456 fields: &[(String, Spanned<Expr>)],
1457 iv: crate::ir::interval::Interval,
1458) -> bool {
1459 // Refinement-via-opaque carriers are single-field products.
1460 let [(_, value)] = fields else {
1461 return false;
1462 };
1463 let k: i128 = match &value.node {
1464 Expr::Literal(Literal::Int(n)) => *n as i128,
1465 Expr::Neg(inner) => match &inner.node {
1466 Expr::Literal(Literal::Int(n)) => -(*n as i128),
1467 _ => return false,
1468 },
1469 _ => return false,
1470 };
1471 iv.contains_point(k)
1472}
1473
1474/// Local AST visitor (the proof-lower module has no shared walker). Pre-order
1475/// visit of every sub-expression. Mirrors `call_graph::walk_expr`.
1476fn carrier_walk_expr(expr: &Spanned<Expr>, visit: &mut impl FnMut(&Expr)) {
1477 visit(&expr.node);
1478 match &expr.node {
1479 Expr::FnCall(func, args) => {
1480 carrier_walk_expr(func, visit);
1481 for arg in args {
1482 carrier_walk_expr(arg, visit);
1483 }
1484 }
1485 Expr::TailCall(boxed) => {
1486 for arg in &boxed.args {
1487 carrier_walk_expr(arg, visit);
1488 }
1489 }
1490 Expr::Attr(obj, _) => carrier_walk_expr(obj, visit),
1491 Expr::BinOp(_, l, r) => {
1492 carrier_walk_expr(l, visit);
1493 carrier_walk_expr(r, visit);
1494 }
1495 Expr::Neg(inner) | Expr::ErrorProp(inner) => carrier_walk_expr(inner, visit),
1496 Expr::Match { subject, arms, .. } => {
1497 carrier_walk_expr(subject, visit);
1498 for arm in arms {
1499 carrier_walk_expr(&arm.body, visit);
1500 }
1501 }
1502 Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1503 for item in items {
1504 carrier_walk_expr(item, visit);
1505 }
1506 }
1507 Expr::MapLiteral(entries) => {
1508 for (k, v) in entries {
1509 carrier_walk_expr(k, visit);
1510 carrier_walk_expr(v, visit);
1511 }
1512 }
1513 Expr::Constructor(_, maybe) => {
1514 if let Some(inner) = maybe {
1515 carrier_walk_expr(inner, visit);
1516 }
1517 }
1518 Expr::InterpolatedStr(parts) => {
1519 for part in parts {
1520 if let crate::ast::StrPart::Parsed(e) = part {
1521 carrier_walk_expr(e, visit);
1522 }
1523 }
1524 }
1525 Expr::RecordCreate { fields, .. } => {
1526 for (_, e) in fields {
1527 carrier_walk_expr(e, visit);
1528 }
1529 }
1530 Expr::RecordUpdate { base, updates, .. } => {
1531 carrier_walk_expr(base, visit);
1532 for (_, e) in updates {
1533 carrier_walk_expr(e, visit);
1534 }
1535 }
1536 Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => {}
1537 }
1538}
1539
1540/// Bare record-type name → its field type strings (`Product` types only).
1541/// Used by Scan 2 to chase a carrier nested inside a record used as a Map
1542/// key.
1543fn collect_record_fields(inputs: &ProofLowerInputs) -> HashMap<String, Vec<String>> {
1544 let mut out: HashMap<String, Vec<String>> = HashMap::new();
1545 let entry_tds = inputs.entry_items.iter().filter_map(|it| match it {
1546 TopLevel::TypeDef(td) => Some(td),
1547 _ => None,
1548 });
1549 let dep_tds = inputs.dep_modules.iter().flat_map(|m| m.type_defs.iter());
1550 for td in entry_tds.chain(dep_tds) {
1551 if let TypeDef::Product { name, fields, .. } = td {
1552 out.entry(name.clone())
1553 .or_default()
1554 .extend(fields.iter().map(|(_, ty)| ty.clone()));
1555 }
1556 }
1557 out
1558}
1559
1560/// Every type annotation string in the program: fn param types, fn return
1561/// types, and record field types. A `Map<…>` anywhere here is a Map-key
1562/// usage candidate for Scan 2.
1563fn all_type_annotations(inputs: &ProofLowerInputs) -> Vec<String> {
1564 let mut out: Vec<String> = Vec::new();
1565 let push_fn = |fd: &FnDef, out: &mut Vec<String>| {
1566 for (_, ty) in &fd.params {
1567 out.push(ty.clone());
1568 }
1569 out.push(fd.return_type.clone());
1570 };
1571 for it in inputs.entry_items {
1572 match it {
1573 TopLevel::FnDef(fd) => push_fn(fd, &mut out),
1574 TopLevel::TypeDef(TypeDef::Product { fields, .. }) => {
1575 out.extend(fields.iter().map(|(_, ty)| ty.clone()));
1576 }
1577 _ => {}
1578 }
1579 }
1580 for m in inputs.dep_modules {
1581 for fd in &m.fn_defs {
1582 push_fn(fd, &mut out);
1583 }
1584 for td in &m.type_defs {
1585 if let TypeDef::Product { fields, .. } = td {
1586 out.extend(fields.iter().map(|(_, ty)| ty.clone()));
1587 }
1588 }
1589 }
1590 out
1591}
1592
1593/// Walk a parsed `Type`. For every `Map<K, _>` encountered, add every
1594/// candidate carrier REACHABLE from `K` (directly, or transitively through a
1595/// record's fields) to `demoted`. Recurses through every type constructor so
1596/// a `Map` buried inside `List<Map<…>>`, a `Result`/`Option` payload, a
1597/// tuple, etc. is still found.
1598fn collect_map_key_carriers(
1599 ty: &crate::ast::Type,
1600 candidates: &HashSet<String>,
1601 record_fields: &HashMap<String, Vec<String>>,
1602 demoted: &mut HashSet<String>,
1603) {
1604 use crate::ast::Type;
1605 match ty {
1606 Type::Map(key, value) => {
1607 carriers_reachable_from(key, candidates, record_fields, &mut HashSet::new(), demoted);
1608 // The KEY is the hazard; recurse into both so a nested Map in
1609 // either position is still inspected.
1610 collect_map_key_carriers(key, candidates, record_fields, demoted);
1611 collect_map_key_carriers(value, candidates, record_fields, demoted);
1612 }
1613 Type::Result(a, b) => {
1614 collect_map_key_carriers(a, candidates, record_fields, demoted);
1615 collect_map_key_carriers(b, candidates, record_fields, demoted);
1616 }
1617 Type::Option(a) | Type::List(a) | Type::Vector(a) => {
1618 collect_map_key_carriers(a, candidates, record_fields, demoted);
1619 }
1620 Type::Tuple(items) => {
1621 for t in items {
1622 collect_map_key_carriers(t, candidates, record_fields, demoted);
1623 }
1624 }
1625 Type::Fn(params, ret, _) => {
1626 for p in params {
1627 collect_map_key_carriers(p, candidates, record_fields, demoted);
1628 }
1629 collect_map_key_carriers(ret, candidates, record_fields, demoted);
1630 }
1631 _ => {}
1632 }
1633}
1634
1635/// Collect every candidate carrier reachable from a Map-KEY type `key`:
1636/// `key` itself if it names a carrier, plus (transitively) any carrier among
1637/// the fields of a record `key` names. `seen` guards self-referential
1638/// records.
1639fn carriers_reachable_from(
1640 key: &crate::ast::Type,
1641 candidates: &HashSet<String>,
1642 record_fields: &HashMap<String, Vec<String>>,
1643 seen: &mut HashSet<String>,
1644 demoted: &mut HashSet<String>,
1645) {
1646 use crate::ast::Type;
1647 let Some(name) = key.named_name() else {
1648 // A Map key that is itself a container (`Map<List<Carrier>, V>`) —
1649 // chase the inner element types too.
1650 match key {
1651 Type::Option(a) | Type::List(a) | Type::Vector(a) => {
1652 carriers_reachable_from(a, candidates, record_fields, seen, demoted);
1653 }
1654 Type::Tuple(items) => {
1655 for t in items {
1656 carriers_reachable_from(t, candidates, record_fields, seen, demoted);
1657 }
1658 }
1659 Type::Map(k, v) => {
1660 carriers_reachable_from(k, candidates, record_fields, seen, demoted);
1661 carriers_reachable_from(v, candidates, record_fields, seen, demoted);
1662 }
1663 Type::Result(a, b) => {
1664 carriers_reachable_from(a, candidates, record_fields, seen, demoted);
1665 carriers_reachable_from(b, candidates, record_fields, seen, demoted);
1666 }
1667 _ => {}
1668 }
1669 return;
1670 };
1671 if !seen.insert(name.to_string()) {
1672 return;
1673 }
1674 if candidates.contains(name) {
1675 demoted.insert(name.to_string());
1676 }
1677 if let Some(fields) = record_fields.get(name) {
1678 for field_ty in fields {
1679 let parsed = crate::types::parse_type_str(field_ty);
1680 carriers_reachable_from(&parsed, candidates, record_fields, seen, demoted);
1681 }
1682 }
1683}
1684
1685/// Walk `analyze_plans(inputs)` and populate `ProofIR.fn_contracts`.
1686///
1687/// Translation pass over the classifier output (`RecursionPlan`) —
1688/// no re-implementation. The diff test (`tests/proof_ir_diff.rs`)
1689/// pins what each `RecursionPlan` variant lowers to so divergence
1690/// between the classifier and the IR populator surfaces there.
1691/// Coverage today: `IntCountdownGuarded`, `LinearRecurrence2`,
1692/// `Sized*` (length / sizeOf / string-pos / int-ascending). Fuel-
1693/// only and Mutual* plans don't materialise as `FnContract` (their
1694/// recursion shape doesn't need IR-level pre-decisions; backends
1695/// emit fuel scaffolding inline).
1696pub fn populate_fn_contracts(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
1697 // Round-5 finding: walk per-scope so two modules each with a
1698 // recursive `foo` (or entry + module both declaring `foo`)
1699 // don't collide on the bare-name `plans: HashMap<String, _>`.
1700 // Aver's module DAG invariant rules out cross-module recursion
1701 // SCCs, so per-scope classification is the canonical view and
1702 // each `Module.fn` gets its own slot in `ir.fn_contracts`.
1703 for scope in inputs.scopes() {
1704 let (plans, issues) =
1705 crate::codegen::recursion::analyze_plans_in_scope(inputs, scope.as_deref(), false);
1706 ir.unclassified_fns
1707 .extend(issues.into_iter().map(|issue| crate::ir::UnclassifiedFn {
1708 line: issue.line,
1709 message: issue.message,
1710 }));
1711 populate_fn_contracts_for_scope(inputs, ir, scope.as_deref(), &plans);
1712 }
1713}
1714
1715fn populate_fn_contracts_for_scope(
1716 inputs: &ProofLowerInputs,
1717 ir: &mut ProofIR,
1718 scope: Option<&str>,
1719 plans: &HashMap<String, RecursionPlan>,
1720) {
1721 let scoped_fns: Vec<&FnDef> = inputs.pure_fns_in_scope(scope);
1722 let qualify = |bare: &str| -> crate::ir::FnKey {
1723 match scope {
1724 Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), bare),
1725 None => crate::ir::FnKey::entry(bare),
1726 }
1727 };
1728 // Contracts key by opaque `FnId`; SymbolTable is always present
1729 // (pipeline builds it unconditionally, `ProofLowerInputs.symbol_
1730 // table: &SymbolTable`).
1731 let symbols = inputs.symbol_table;
1732
1733 for (fn_name, plan) in plans {
1734 let Some(fd) = scoped_fns.iter().find(|fd| fd.name == *fn_name) else {
1735 continue;
1736 };
1737 let fn_key = qualify(fn_name);
1738 let Some(canonical_key) = symbols.fn_id_of(&fn_key) else {
1739 continue;
1740 };
1741
1742 // IntCountdown — fuel-encoded countdown on a single Int param.
1743 // Distinct from IntCountdownGuarded: external callers may pass
1744 // negatives (the classifier rejected closed-world status), so
1745 // backends emit a fuel helper with `n.natAbs + 1` initial fuel
1746 // rather than a native def with a precondition.
1747 if let RecursionPlan::IntCountdown { param_index } = plan {
1748 if let Some((param_name, _)) = fd.params.get(*param_index) {
1749 ir.fn_contracts.insert(
1750 canonical_key,
1751 FnContract {
1752 source_name: fn_name.clone(),
1753 recursion: Some(RecursionContract::Fuel {
1754 fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne {
1755 param: param_name.clone(),
1756 },
1757 }),
1758 },
1759 );
1760 }
1761 continue;
1762 }
1763
1764 // IntFloorDivCountdown — guard-validated literal-divisor
1765 // floor-division shrink. The classifier proved both
1766 // side-conditions (every self-call shrinks the param through
1767 // `Result.withDefault(Int.div(p, k), d)` with literal k >= 2,
1768 // and every self-call site's guard chain implies `p >= 1`),
1769 // so backends emit a native well-founded def on `p.toNat`.
1770 if let RecursionPlan::IntFloorDivCountdown {
1771 param_index,
1772 divisor,
1773 helper_fn,
1774 } = plan
1775 {
1776 if let Some((param_name, _)) = fd.params.get(*param_index) {
1777 ir.fn_contracts.insert(
1778 canonical_key,
1779 FnContract {
1780 source_name: fn_name.clone(),
1781 recursion: Some(RecursionContract::WellFoundedToNat {
1782 param: param_name.clone(),
1783 floor_div: Some(crate::ir::FloorDivShrink {
1784 divisor: *divisor,
1785 helper_fn: helper_fn.clone(),
1786 }),
1787 }),
1788 },
1789 );
1790 }
1791 continue;
1792 }
1793
1794 // IntAscending — fuel formula `(bound - n).natAbs + 1`. The
1795 // bound stays as `Spanned<Expr>` so backends render it through
1796 // their own emitters (it can be a literal, a fn param, or a
1797 // small arith expression).
1798 if let RecursionPlan::IntAscending { param_index, bound } = plan {
1799 if let Some((param_name, _)) = fd.params.get(*param_index) {
1800 ir.fn_contracts.insert(
1801 canonical_key,
1802 FnContract {
1803 source_name: fn_name.clone(),
1804 recursion: Some(RecursionContract::Fuel {
1805 fuel_metric: crate::ir::FuelMetric::BoundMinusParamNatAbsPlusOne {
1806 param: param_name.clone(),
1807 bound: inputs.resolve_expr(bound, scope),
1808 },
1809 }),
1810 },
1811 );
1812 }
1813 continue;
1814 }
1815
1816 // ListStructural — structural recursion on a List<_> param.
1817 // Lean/Dafny don't actually use a fuel helper for this on
1818 // recent backends (structural recursion is natively
1819 // terminating); the metric stays as `SeqLenPlusOne` for
1820 // backend-symmetric framing, and the consumer ignores it
1821 // when emitting plain structural recursion.
1822 if let RecursionPlan::ListStructural { param_index } = plan {
1823 if let Some((param_name, _)) = fd.params.get(*param_index) {
1824 ir.fn_contracts.insert(
1825 canonical_key,
1826 FnContract {
1827 source_name: fn_name.clone(),
1828 recursion: Some(RecursionContract::Fuel {
1829 fuel_metric: crate::ir::FuelMetric::SeqLenPlusOne {
1830 param: param_name.clone(),
1831 },
1832 }),
1833 },
1834 );
1835 }
1836 continue;
1837 }
1838
1839 // SizeOfStructural — recursion on a user ADT (e.g. an AST
1840 // type). Fuel metric `sizeOf(call_frame) + 1`. The classifier
1841 // doesn't pin a single bound param — `sizeOf` measures the
1842 // whole frame — so the IR variant carries no param name.
1843 if matches!(plan, RecursionPlan::SizeOfStructural) {
1844 ir.fn_contracts.insert(
1845 canonical_key,
1846 FnContract {
1847 source_name: fn_name.clone(),
1848 recursion: Some(RecursionContract::Fuel {
1849 fuel_metric: crate::ir::FuelMetric::SizeOfPlusOne,
1850 }),
1851 },
1852 );
1853 continue;
1854 }
1855
1856 // StringPosAdvance — `(s, pos)`-shape recursion: `s` invariant
1857 // (first param, String), `pos` advances (second param, Int).
1858 // Fuel formula `s.length - pos`.
1859 if matches!(plan, RecursionPlan::StringPosAdvance) {
1860 if let (Some((string_param, _)), Some((pos_param, _))) =
1861 (fd.params.first(), fd.params.get(1))
1862 {
1863 ir.fn_contracts.insert(
1864 canonical_key,
1865 FnContract {
1866 source_name: fn_name.clone(),
1867 recursion: Some(RecursionContract::Fuel {
1868 fuel_metric: crate::ir::FuelMetric::StringLenMinusPos {
1869 string_param: string_param.clone(),
1870 pos_param: pos_param.clone(),
1871 },
1872 }),
1873 },
1874 );
1875 }
1876 continue;
1877 }
1878
1879 // Mutual-recursion SCCs — each member of the SCC gets its own
1880 // plan with the same family. All three lower to a Lex fuel
1881 // metric; the params vector + rank distinguish per-shape /
1882 // per-member roles.
1883 //
1884 // - MutualIntCountdown: every member counts down its first
1885 // Int param; rank stays 0 (no inter-member ranking — every
1886 // edge decreases the shared dimension).
1887 // - MutualStringPosAdvance { rank }: (s, pos) shape across
1888 // the SCC; rank distinguishes members for same-measure
1889 // inter-fn edges.
1890 // - MutualSizeOfRanked { rank }: sizeOf measures the whole
1891 // call frame; rank distinguishes members. No bound param —
1892 // the empty params vec signals "frame-level measure".
1893 match plan {
1894 RecursionPlan::MutualIntCountdown => {
1895 let params = fd
1896 .params
1897 .first()
1898 .map(|(n, _)| vec![n.clone()])
1899 .unwrap_or_default();
1900 ir.fn_contracts.insert(
1901 canonical_key,
1902 FnContract {
1903 source_name: fn_name.clone(),
1904 recursion: Some(RecursionContract::Fuel {
1905 fuel_metric: crate::ir::FuelMetric::Lex { params, rank: 0 },
1906 }),
1907 },
1908 );
1909 continue;
1910 }
1911 RecursionPlan::MutualStringPosAdvance { rank } => {
1912 let params = fd.params.iter().take(2).map(|(n, _)| n.clone()).collect();
1913 ir.fn_contracts.insert(
1914 canonical_key,
1915 FnContract {
1916 source_name: fn_name.clone(),
1917 recursion: Some(RecursionContract::Fuel {
1918 fuel_metric: crate::ir::FuelMetric::Lex {
1919 params,
1920 rank: *rank,
1921 },
1922 }),
1923 },
1924 );
1925 continue;
1926 }
1927 RecursionPlan::MutualSizeOfRanked { rank } => {
1928 ir.fn_contracts.insert(
1929 canonical_key,
1930 FnContract {
1931 source_name: fn_name.clone(),
1932 recursion: Some(RecursionContract::Fuel {
1933 fuel_metric: crate::ir::FuelMetric::Lex {
1934 params: Vec::new(),
1935 rank: *rank,
1936 },
1937 }),
1938 },
1939 );
1940 continue;
1941 }
1942 RecursionPlan::LinearRecurrence2 => {
1943 ir.fn_contracts.insert(
1944 canonical_key,
1945 FnContract {
1946 source_name: fn_name.clone(),
1947 recursion: Some(RecursionContract::LinearRecurrence2),
1948 },
1949 );
1950 continue;
1951 }
1952 _ => {}
1953 }
1954
1955 let RecursionPlan::IntCountdownGuarded {
1956 param_index,
1957 base_arm_literal,
1958 base_arm_body,
1959 wildcard_arm_body,
1960 precondition,
1961 } = plan
1962 else {
1963 continue;
1964 };
1965 let Some((countdown_param_name, _)) = fd.params.get(*param_index) else {
1966 continue;
1967 };
1968
1969 let precondition_predicates: Vec<Predicate> = precondition
1970 .iter()
1971 .map(|clause| Predicate {
1972 free_vars: vec![(
1973 countdown_param_name.clone(),
1974 QuantifierType::Plain("Int".to_string()),
1975 )],
1976 expr: inputs.resolve_expr(clause, scope),
1977 })
1978 .collect();
1979
1980 ir.fn_contracts.insert(
1981 canonical_key,
1982 FnContract {
1983 source_name: fn_name.clone(),
1984 recursion: Some(RecursionContract::Native {
1985 precondition: precondition_predicates,
1986 measure: Measure::NatAbsInt {
1987 param: countdown_param_name.clone(),
1988 },
1989 preservation: PreservationProof::IntCountdownLiteralZero,
1990 decrease: DecreaseProof::NatAbsCountdown,
1991 body: NativeIntCountdownBody {
1992 base_arm_literal: *base_arm_literal,
1993 base_arm_body: inputs.resolve_expr(base_arm_body, scope),
1994 wildcard_arm_body: inputs.resolve_expr(wildcard_arm_body, scope),
1995 },
1996 }),
1997 },
1998 );
1999 }
2000}
2001
2002/// Walk every verify block, lift `VerifyKind::Law` entries into
2003/// `ProofIR.law_theorems`.
2004///
2005/// Extracts the law's shape (quantifiers from `givens`, premises
2006/// from `when`, claim from `lhs == rhs`) and pins a `ProofStrategy`
2007/// via [`classify_law_strategy`]. Covered strategies: Reflexive,
2008/// Commutative / Associative / IdentityElement / AntiCommutative /
2009/// UnaryEqualsBinary (arithmetic wrappers), Induction (recursive
2010/// ADTs), LibraryAxiom (Map set/get), MapUpdatePostcondition,
2011/// MapKeyTrackedIncrement, SpecEquivalence{,SimpNormalized},
2012/// LinearIntSpecEquivalence, EffectfulSpecEquivalence (with Oracle
2013/// Lift), LinearArithmetic (catch-all over an unfold chain).
2014/// Unmatched shapes pin `BackendDispatch` and fall through to the
2015/// backend's residual chain (linear_recurrence2 emit + sampled /
2016/// guarded-domain fallback).
2017pub fn populate_law_theorems(inputs: &ProofLowerInputs, ir: &mut ProofIR) {
2018 use crate::ast::{TopLevel, VerifyKind};
2019 use crate::ir::{LawTheorem, Predicate, Quantifier, QuantifierType};
2020
2021 let symbols = inputs.symbol_table;
2022
2023 // Verify-law blocks to lower, each tagged with the prefix of the
2024 // dep module that owns it (`None` = entry). The entry's own verify
2025 // blocks come from `entry_items`; dependency modules' proven laws
2026 // come from `ModuleInfo.verify_laws` (the cross-file law pool — a
2027 // dep law is lowered with the SAME strategy classification an entry
2028 // law of that shape gets, so it auto-proves and can be cited by a
2029 // consumer). The DAG invariant keeps the bare fn name unambiguous
2030 // within each scope.
2031 let entry_verifies = inputs.entry_items.iter().filter_map(|item| match item {
2032 TopLevel::Verify(vb) => Some((None, vb)),
2033 _ => None,
2034 });
2035 let dep_verifies = inputs.dep_modules.iter().flat_map(|m| {
2036 m.verify_laws
2037 .iter()
2038 .map(move |vb| (Some(m.prefix.as_str()), vb))
2039 });
2040 for (owning_prefix, vb) in entry_verifies.chain(dep_verifies) {
2041 let VerifyKind::Law(law) = &vb.kind else {
2042 continue;
2043 };
2044
2045 let quantifiers: Vec<Quantifier> = law
2046 .givens
2047 .iter()
2048 .map(|g| Quantifier {
2049 name: g.name.clone(),
2050 binder_type: QuantifierType::Plain(g.type_name.clone()),
2051 })
2052 .collect();
2053
2054 // The fn this law targets, keyed by its owning scope. For an
2055 // entry law the bare name resolves to an entry `FnId`; for a
2056 // dep law it resolves through `FnKey::in_module(prefix, name)`.
2057 // When the fn isn't in the symbol table (verify block targeting
2058 // a fn that doesn't exist), skip the law silently — the
2059 // typechecker / verify-driver surfaces the missing target
2060 // elsewhere.
2061 let target_key = match owning_prefix {
2062 Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), &vb.fn_name),
2063 None => crate::ir::FnKey::entry(&vb.fn_name),
2064 };
2065 let Some(fn_id) = symbols.fn_id_of(&target_key) else {
2066 continue;
2067 };
2068
2069 // Scope for resolving the law's expressions: derived from the
2070 // target fn's owning module, NOT hardcoded to entry.
2071 let law_scope: Option<String> = symbols
2072 .fn_entry(fn_id)
2073 .key
2074 .scope_str()
2075 .map(|s| s.to_string());
2076 let law_scope_ref = law_scope.as_deref();
2077
2078 let premises: Vec<Predicate> = match &law.when {
2079 Some(when_expr) => vec![Predicate {
2080 free_vars: quantifiers
2081 .iter()
2082 .map(|q| (q.name.clone(), q.binder_type.clone()))
2083 .collect(),
2084 expr: inputs.resolve_expr(when_expr, law_scope_ref),
2085 }],
2086 None => Vec::new(),
2087 };
2088
2089 let strategy = classify_law_strategy(
2090 law,
2091 &vb.fn_name,
2092 inputs,
2093 &ir.refined_types,
2094 &ir.fn_contracts,
2095 law_scope_ref,
2096 );
2097
2098 ir.law_theorems.push(LawTheorem {
2099 fn_id,
2100 law_name: law.name.clone(),
2101 quantifiers,
2102 premises,
2103 claim_lhs: inputs.resolve_expr(&law.lhs, law_scope_ref),
2104 claim_rhs: inputs.resolve_expr(&law.rhs, law_scope_ref),
2105 strategy,
2106 });
2107 }
2108
2109 // Demand-driven well-founded graduation for the floor-division
2110 // window family: the figures' proof templates rest on the
2111 // power-of-two fn's defining equations and functional-induction
2112 // principle, which the fuel encoding destroys (the fuel arg on
2113 // the recursive call differs from the callee's own measure, so
2114 // nothing universal is provable through `__fuel`). Upgrade the
2115 // cited pow fn's contract from `Fuel { NatAbsPlusOne }` to the
2116 // native `WellFoundedToNat` form (`floor_div: None` — the guarded
2117 // subtractive countdown whose `n <= 0` base guard puts `n >= 1`
2118 // in the decreasing goal's context, so `omega` closes the
2119 // measure bare). Scoped on purpose: a pow-shaped fn in a file
2120 // with no recognized window law keeps its established fuel
2121 // emission, so nothing outside the family moves.
2122 let window_pow_fns: HashSet<String> = ir
2123 .law_theorems
2124 .iter()
2125 .filter_map(|t| match &t.strategy {
2126 crate::ir::ProofStrategy::FloorDivWindow { figure } => Some(match figure {
2127 crate::ir::FloorWindowFigure::PowPositive { pow_fn } => pow_fn.clone(),
2128 crate::ir::FloorWindowFigure::PowSumSplit { pow_fn } => pow_fn.clone(),
2129 crate::ir::FloorWindowFigure::SigWindow { pow_fn, .. } => pow_fn.clone(),
2130 crate::ir::FloorWindowFigure::ProductWindow { pow_fn, .. } => pow_fn.clone(),
2131 }),
2132 _ => None,
2133 })
2134 .collect();
2135 for pow_fn in window_pow_fns {
2136 let Some(fn_id) = symbols.fn_id_of(&crate::ir::FnKey::entry(&pow_fn)) else {
2137 continue;
2138 };
2139 let Some(contract) = ir.fn_contracts.get_mut(&fn_id) else {
2140 continue;
2141 };
2142 if let Some(crate::ir::RecursionContract::Fuel {
2143 fuel_metric: crate::ir::FuelMetric::NatAbsPlusOne { param },
2144 }) = &contract.recursion
2145 {
2146 contract.recursion = Some(crate::ir::RecursionContract::WellFoundedToNat {
2147 param: param.clone(),
2148 floor_div: None,
2149 });
2150 }
2151 }
2152}
2153
2154/// Pick the strategy `LawLower` should pin on a `(fn, law)` pair.
2155///
2156/// Decision order — specific algebraic properties first, then
2157/// generic linear-arithmetic catch-all, then `BackendDispatch`:
2158/// 1. `Reflexive` — `law.lhs ≡ law.rhs` syntactically.
2159/// 2. `Commutative { op }` — fn body is `a <op> b`, claim is
2160/// `f(a, b) = f(b, a)` (op restricted to commutative ones).
2161/// 3. `Associative { op }` — same body, 3 givens, assoc claim.
2162/// 4. `IdentityElement { op }` — `f(a, e) = a` (or `f(e, a) = a`),
2163/// where `e` is the op's identity. Covers Add/Mul both-sided
2164/// plus Sub right-sided.
2165/// 5. `AntiCommutative { op: Sub, neg_on_rhs }` — `f(a, b) =
2166/// -f(b, a)` form. Sub-only (Mul has no anti-commutative law).
2167/// 6. `UnaryEqualsBinary { inner_fn }` — outer fn is unary, claim
2168/// binds it to the inner binary fn at a constant.
2169/// 7. `LinearArithmetic { unfold_fns, ... }` — catch-all when the
2170/// law reduces to linear arith after unfolding the call chain.
2171/// 8. `EnumConstantFold { unfold_fns }` — ground law over fixed
2172/// enum/ADT constructor args, scalar return (#466).
2173/// 9. `FiniteDomainCases { givens }` — every given ranges over a
2174/// closed finite domain (Bool / fieldless enum, product ≤ 16);
2175/// closes by exhaustive `cases` enumeration.
2176/// 10. `RingIdentity { unfold_fns }` — unconditional ring identity
2177/// over Int-component records (cross-multiplication equality);
2178/// runs before the prelude-simp rung, which would otherwise claim
2179/// the shape and park it on a caught sorry.
2180/// 11. `IntDecimalRoundtrip { … }` — canonical decimal-Int
2181/// parse/serialize roundtrip over a recognized string-pos scanner;
2182/// runs before the prelude-simp rung, which would otherwise claim
2183/// the shape and park it on a caught sorry.
2184/// 12. `SimpOverPreludeLemmas { … }` — builtin-roundtrip shape; the
2185/// Lean backend renders it AFTER its legacy chain, so it fires
2186/// exactly where the bare-`sorry` universal used to.
2187/// 13. `BackendDispatch` — backend's ad-hoc chain decides.
2188///
2189/// (The induction/spec-equivalence/Map families detected between
2190/// these rungs are documented at their detector sites below.)
2191fn classify_law_strategy(
2192 law: &crate::ast::VerifyLaw,
2193 fn_name: &str,
2194 inputs: &ProofLowerInputs,
2195 refined_types: &std::collections::HashMap<crate::ir::TypeId, crate::ir::RefinedTypeDecl>,
2196 fn_contracts: &std::collections::HashMap<crate::ir::FnId, crate::ir::FnContract>,
2197 scope: Option<&str>,
2198) -> crate::ir::ProofStrategy {
2199 use crate::ir::ProofStrategy;
2200
2201 // Result-pipeline chain equivalence (stage 8b of #232) — `?`
2202 // propagation `chain_qm(x)` vs nested-match `chain_manual(x)`.
2203 // Both sides unfold to the same nested match; the proof closes
2204 // by `unfold + repeat split`.
2205 if law.when.is_none()
2206 && let Some(s) = detect_result_pipeline_chain_equivalence(law, fn_name, inputs)
2207 {
2208 return s;
2209 }
2210 // Wrapper-over-recursion with monoidal accumulator (stage 8 of
2211 // #232) — runs before generic induction because its aux-lemma
2212 // template closes laws naive induction can't (e.g. `sum(xs) ==
2213 // sumDirect(xs)` where `sum(xs) = sumTR(xs, 0)`). Detected
2214 // when `fn_name` is registered as a `WrapperOverRecursion`
2215 // pattern in `ProgramShape` AND the law shape is
2216 // `wrapper(g) == other(g)` AND the inner fn body matches the
2217 // monoidal-accumulator template.
2218 if law.when.is_none()
2219 && let Some(s) = detect_wrapper_over_recursion(law, fn_name, inputs)
2220 {
2221 return s;
2222 }
2223 // Tail-recursive fold with a FIXED base param (TIP prop_35,
2224 // `exp x y = qexp x y one`). The 2-given inline-wrapper shape the
2225 // `WrapperOverRecursion` recognizer can't reach: a 3-arg loop whose
2226 // extra leading param is held fixed and whose combine multiplies the
2227 // accumulator by that fixed param. Emits the accumulator-generalization
2228 // lemma plus the main universal law.
2229 if law.when.is_none()
2230 && let Some(s) = detect_tailrec_fixed_base_fold(law, inputs)
2231 {
2232 return s;
2233 }
2234 // Structural induction runs first — when any given binds a
2235 // recursive ADT, induction over its variants is the canonical
2236 // proof. Reflexive could also fire on `f(t) = f(t)` for `t: Tree`
2237 // but induction subsumes (one trivial case per variant) and is
2238 // the legacy chain's first pick. `when` clauses block induction
2239 // — a non-closing `when` law would emit a 2-arm induction ladder
2240 // (2 sorries) instead of the bounded sampled-domain fallback,
2241 // regressing output cleanliness; a non-regressing when-aware
2242 // induction path is a follow-up.
2243 if law.when.is_none()
2244 && let Some(param) = detect_induction_target(law, inputs)
2245 {
2246 return ProofStrategy::Induction { param };
2247 }
2248 if law.lhs == law.rhs {
2249 return ProofStrategy::Reflexive;
2250 }
2251 // Binary-wrapper-shaped laws first. `wrapper_binop` returns
2252 // `None` for non-binary fns — unary wrappers are tried after
2253 // this block falls through.
2254 if let Some(op) = wrapper_binop(fn_name, inputs) {
2255 if detect_wrapper_commutative(law, fn_name, op) {
2256 return ProofStrategy::Commutative { op };
2257 }
2258 if detect_wrapper_associative(law, fn_name, op) {
2259 return ProofStrategy::Associative { op };
2260 }
2261 if detect_wrapper_identity(law, fn_name, op) {
2262 return ProofStrategy::IdentityElement { op };
2263 }
2264 // Sub right-identity collapses into IdentityElement —
2265 // same emit (`simp [fn]`), different lhs/rhs shape. The
2266 // detector validates the right-side `f(a, 0) = a` form
2267 // (`f(0, a) = -a` doesn't equal `a`, so Sub is one-sided).
2268 if matches!(op, crate::ast::BinOp::Sub) && detect_wrapper_sub_right_identity(law, fn_name) {
2269 return ProofStrategy::IdentityElement { op };
2270 }
2271 // Anti-commutative is Sub-specific (Add/Mul are
2272 // commutative, no anti-commutativity). The op tag keeps
2273 // it parameterised even though only Sub currently fires.
2274 if matches!(op, crate::ast::BinOp::Sub)
2275 && let Some(neg_on_rhs) = detect_wrapper_sub_anti_commutative(law, fn_name)
2276 {
2277 return ProofStrategy::AntiCommutative { op, neg_on_rhs };
2278 }
2279 }
2280 // Unary fn equal to binary fn at a constant — `fn_name` is the
2281 // unary outer; the binary fn name is captured for backends.
2282 if let Some(inner_fn) = detect_wrapper_unary_equivalence(law, fn_name, inputs) {
2283 return ProofStrategy::UnaryEqualsBinary { inner_fn };
2284 }
2285 // Library axiom instances — Map.has-after-set, Map.get-after-set.
2286 // Specific shape, single-line `simpa using axiom` emit on Lean.
2287 if let Some((axiom, args)) = detect_map_set_axiom(law) {
2288 let resolved_args: Vec<_> = args.iter().map(|a| inputs.resolve_expr(a, scope)).collect();
2289 return ProofStrategy::LibraryAxiom {
2290 axiom,
2291 args: resolved_args,
2292 };
2293 }
2294 // Tracked-counter increment: specialised body template + `+ 1`
2295 // rhs. Checked before the more general MapUpdatePostcondition so
2296 // the tighter strategy wins for this shape.
2297 if let Some(inc) = detect_map_key_tracked_increment(law, fn_name, inputs) {
2298 return ProofStrategy::MapKeyTrackedIncrement {
2299 outer_fn: inc.outer_fn,
2300 map_arg: inputs.resolve_expr(&inc.map_arg, scope),
2301 key_arg: inputs.resolve_expr(&inc.key_arg, scope),
2302 };
2303 }
2304 // Post-condition of an inline-defined map-update fn — case-split
2305 // over `Map.get m k` and apply the `Map.set` axioms.
2306 if let Some(post) = detect_map_update_postcondition(law, fn_name, inputs) {
2307 return ProofStrategy::MapUpdatePostcondition {
2308 outer_fn: post.outer_fn,
2309 kind: post.kind,
2310 map_arg: inputs.resolve_expr(&post.map_arg, scope),
2311 key_arg: inputs.resolve_expr(&post.key_arg, scope),
2312 extra_unfolds: post.extra_unfolds,
2313 };
2314 }
2315 // Functional equivalence of `vb.fn_name` and a same-named spec
2316 // fn whose body is syntactically identical to the impl's.
2317 if let Some(extra_unfolds) = detect_spec_equivalence(law, fn_name, inputs) {
2318 return ProofStrategy::SpecEquivalence { extra_unfolds };
2319 }
2320 // Broader spec equivalence — bodies differ syntactically but
2321 // normalize to same under substitution + arithmetic identity
2322 // folding. Runs after the strict `SpecEquivalence` so the
2323 // tighter detector wins when both would match.
2324 if let Some(extra_unfolds) = detect_simp_normalized_spec_equivalence(law, fn_name, inputs) {
2325 return ProofStrategy::SpecEquivalenceSimpNormalized { extra_unfolds };
2326 }
2327 // Linear-Int spec equivalence — substituted bodies are pure
2328 // linear arithmetic over Int givens; decided by `omega` / LIA.
2329 if let Some((unfolded_impl, unfolded_spec)) =
2330 detect_linear_int_spec_equivalence(law, fn_name, inputs)
2331 {
2332 return ProofStrategy::LinearIntSpecEquivalence {
2333 unfolded_impl: inputs.resolve_expr(&unfolded_impl, scope),
2334 unfolded_spec: inputs.resolve_expr(&unfolded_spec, scope),
2335 };
2336 }
2337 // Effectful counterpart — Oracle Lift normalises both sides
2338 // (oracle args injected into impl call) and the lowerer matches
2339 // the canonical `impl(args) == spec(args)` shape on the
2340 // rewritten form. Fires on real oracle-spec laws like
2341 // `pickPair() => pairSpec(BranchPath.Root, rnd)`.
2342 if let Some(spec_fn) = detect_effectful_spec_equivalence(law, fn_name, inputs) {
2343 return ProofStrategy::EffectfulSpecEquivalence {
2344 impl_fn: fn_name.to_string(),
2345 spec_fn,
2346 };
2347 }
2348 // Second-order linear recurrence (fib / fibSpec shape). Detector
2349 // validates impl as tail-rec wrapper, spec as direct second-order
2350 // recurrence, helper as their shared affine worker — all three
2351 // shapes pinned in `lean::recurrence`. Backends consume the
2352 // (impl_fn, spec_fn, helper_fn) names from IR; the proof template
2353 // differs per target (Lean Nat-helper + induction; Dafny still
2354 // pending — issue #116).
2355 if let Some((spec_fn, helper_fn)) =
2356 detect_linear_recurrence2_spec_equivalence(law, fn_name, inputs)
2357 {
2358 return ProofStrategy::LinearRecurrence2SpecEquivalence {
2359 impl_fn: fn_name.to_string(),
2360 spec_fn,
2361 helper_fn,
2362 };
2363 }
2364 // Linear arithmetic over an unfold chain — generic catch-all.
2365 // Named for the semantic, not the backend tactic.
2366 if let Some(plan) = detect_simp_omega_unfold(law, fn_name, inputs, refined_types) {
2367 return ProofStrategy::LinearArithmetic {
2368 unfold_fns: plan.unfold_fns,
2369 wrapper_return: plan.wrapper_return,
2370 smart_guard: plan.smart_guard,
2371 lifted: plan.lifted,
2372 };
2373 }
2374 // Ground constant-fold over fixed ADT/enum constructors — the
2375 // last typed fallback before `BackendDispatch`. Fires only for the
2376 // narrow shape no earlier detector accepts: a non-recursive fn with
2377 // ≥1 non-Int param, whose every non-Int param is pinned to a
2378 // constructor literal at the law's call site(s). LinearArithmetic
2379 // rejected it (non-Int param), Induction rejected it (no recursive
2380 // ADT given) — so this can't steal a law another strategy owns.
2381 if law.when.is_none()
2382 && let Some(unfold_fns) = detect_enum_constant_fold(law, fn_name, inputs)
2383 {
2384 return ProofStrategy::EnumConstantFold { unfold_fns };
2385 }
2386 // Closed finite-domain enumeration — the final typed fallback
2387 // before `BackendDispatch`. Fires when EVERY given ranges over a
2388 // closed, small domain (Bool or an all-fieldless user enum, ≤ 16
2389 // total combinations): exhaustive `cases` over the givens yields
2390 // ground goals per leaf, so deliberately NO call-shape inspection,
2391 // NO return-type gate and NO recursion gate — closed enumeration
2392 // makes those irrelevant (fuel-wrapped callees compute through
2393 // constant-measure constructor args). That is exactly why this is
2394 // a NEW detector and not a relaxation of `EnumConstantFold`, whose
2395 // literal-pinning / non-recursive / scalar-return gates are
2396 // load-bearing for its simp cascade.
2397 if law.when.is_none()
2398 && let Some(givens) = detect_finite_domain_cases(law, inputs)
2399 {
2400 return ProofStrategy::FiniteDomainCases { givens };
2401 }
2402 // Unconditional ring identity over Int-component records — runs
2403 // BEFORE the prelude-simp rung because that rung would otherwise
2404 // claim the shape (record givens, non-recursive pure cone) and
2405 // park it on a caught sorry: its minimal simp set has no AC-ring
2406 // normalization, and the permutational package this strategy
2407 // emits cannot be added there (it would loop or destroy the
2408 // normal forms other strategies rely on). Every earlier rung has
2409 // already declined: LinearArithmetic rejects non-Int record
2410 // givens, EnumConstantFold needs constructor-literal-pinned
2411 // params, FiniteDomainCases needs closed finite domains — so the
2412 // pin cannot steal a law a cheaper strategy closes today.
2413 if law.when.is_none()
2414 && let Some(unfold_fns) = detect_ring_identity(law, fn_name, inputs)
2415 {
2416 return ProofStrategy::RingIdentity { unfold_fns };
2417 }
2418 // Decimal-Int parse/serialize roundtrip — runs BEFORE the prelude-
2419 // simp rung because that rung would otherwise claim the shape (the
2420 // lhs cone is fuel-wrapped with measure-closed args) and park it on
2421 // a caught sorry the scanner barrier guarantees. The detector
2422 // validates the ENTIRE canonical parser shape (head-char dispatch
2423 // arms, single recognized scanner, slice + `Int.fromString` leaf),
2424 // so it cannot fire on the #469 prelude-simp laws (`finishInt` /
2425 // `finishNumber` / `afterIntChar` / `finishString` — wrong arity or
2426 // non-literal second arg at the law call site).
2427 if law.when.is_none()
2428 && let Some(s) = detect_int_decimal_roundtrip(law, fn_name, inputs, fn_contracts)
2429 {
2430 return s;
2431 }
2432 // Escaped-string parse/serialize roundtrip — the string-escape
2433 // sibling of the decimal roundtrip above, and like it placed
2434 // BEFORE the prelude-simp rung, which would otherwise claim the
2435 // shape (fuel-wrapped lhs cone) and park it on a caught sorry.
2436 // The detector validates the ENTIRE producer/consumer pair
2437 // (classifier escape table aligned arm-by-arm with the consumer's
2438 // escape dispatcher, control-escape prefix, threshold agreement,
2439 // fuel contracts), so it cannot fire on any shape whose
2440 // synthesized suffix-invariant proof would not close.
2441 if law.when.is_none()
2442 && let Some(s) = detect_string_escape_roundtrip(law, inputs, fn_contracts)
2443 {
2444 return s;
2445 }
2446 // Floor-division window family — laws over a power-of-two fn, a
2447 // guard-validated floor-halving binary-exponent fn, and the
2448 // window predicates built from them. The detectors are
2449 // deliberately narrow (exactly the hand-validated figures —
2450 // pow positivity, the pow sum homomorphism, the significand
2451 // window, the product window) and key on structure plus the
2452 // exponent fn's `WellFoundedToNat` contract, never on names.
2453 // Runs after every cheaper rung declined: LinearArithmetic
2454 // rejects the `Result.withDefault` cone and recursive callees,
2455 // Induction needs a recursive-ADT given, EnumConstantFold /
2456 // FiniteDomainCases need non-Int / closed domains — so the pin
2457 // cannot steal a law another strategy closes today.
2458 if let Some(figure) = detect_floor_window(law, fn_name, inputs, fn_contracts) {
2459 return ProofStrategy::FloorDivWindow { figure };
2460 }
2461 // Builtin-roundtrip simp over the prelude's spec-lemma registry —
2462 // the very last typed fallback. The Lean backend deliberately
2463 // renders this strategy AFTER its whole legacy ad-hoc chain (see
2464 // `lean::law_auto`), so pinning it here cannot steal a law any
2465 // legacy fallback closes today: it fires exactly where the
2466 // sampled-sorry path used to emit a bare-`sorry` universal.
2467 if law.when.is_none()
2468 && let Some(s) = detect_simp_over_prelude_lemmas(law, fn_name, inputs, fn_contracts)
2469 {
2470 return s;
2471 }
2472 ProofStrategy::BackendDispatch
2473}
2474
2475mod finite_domain;
2476mod floor_window;
2477mod induction;
2478mod int_decimal_roundtrip;
2479mod map_laws;
2480mod refinement;
2481mod ring;
2482mod simp;
2483mod spec_equivalence;
2484mod string_escape_roundtrip;
2485mod wrapper_laws;
2486
2487pub(crate) use induction::LawProofCone;
2488
2489use finite_domain::*;
2490use floor_window::*;
2491use induction::*;
2492use int_decimal_roundtrip::*;
2493use map_laws::*;
2494use refinement::*;
2495use ring::*;
2496use simp::*;
2497use spec_equivalence::*;
2498use string_escape_roundtrip::*;
2499use wrapper_laws::*;