Skip to main content

aver/codegen/
common.rs

1use std::collections::HashSet;
2
3use crate::ast::{
4    Expr, FnBody, FnDef, Literal, MatchArm, Pattern, Spanned, Stmt, StrPart, TailCallData,
5    TopLevel, TypeDef, TypeVariant, VerifyBlock, VerifyGivenDomain, VerifyKind,
6};
7use crate::codegen::CodegenContext;
8use crate::types::Type;
9
10/// A "refinement record" is the canonical `refinement-via-opaque`
11/// pattern: a single-field `record X { carrier: T }` paired with a
12/// validating smart constructor
13///   `fn fromX(p: T) -> Result<X, _>` body = `match <pred-in-p> with`
14///   `    true  -> Result.Ok(X(carrier = p))`
15///   `    false -> Result.Err("...")`
16///
17/// Detecting this shape lets backends emit the type as a true
18/// dependent / subset type (`def X := { n : T // P n }` in Lean,
19/// `type X = n: T | P n` in Dafny) instead of a flat product, which
20/// in turn collapses universal-law proofs into one-liners
21/// (`rw [Int.add_comm]`) by carrying the invariant inside the type
22/// rather than threading it through ad-hoc tactic plumbing.
23#[derive(Debug, Clone)]
24pub struct RefinementInfo<'a> {
25    /// Carrier-type annotation as written in the record field
26    /// (`"Int"`, `"Float"`, …). Backends emit this as the
27    /// subset's underlying type.
28    pub carrier_type: &'a str,
29    /// Carrier-field name (e.g. `"value"`). Lean projects through
30    /// `.val` on a Subtype, so users of the carrier field have to
31    /// rewrite `n.value → n.val` when the host type is refined.
32    pub carrier_field: &'a str,
33    /// Name of the smart constructor's input parameter (`"n"` in
34    /// `fromInt(n: Int) → Result<X, _>`). Used when substituting
35    /// the law's quantified variable into the predicate.
36    pub param_name: &'a str,
37    /// Source-level name of the smart constructor itself (`"fromInt"`).
38    /// The interval analysis gates its transparent-wrapper peel on this
39    /// name so only the real smart constructor — never an arbitrary
40    /// one-arg helper — is treated as identity over its argument.
41    pub constructor_fn: &'a str,
42    /// AST node for the bool predicate the smart constructor
43    /// branches on — the body's `Match { subject = <here>, ... }`.
44    pub predicate: &'a Spanned<Expr>,
45}
46
47/// Inspect inputs for a refinement-via-opaque record by `type_name`.
48/// Returns `Some(info)` iff there's exactly one matching smart
49/// constructor and the record has a single carrier field.
50///
51/// Defaults to "any scope" — walks entry + every dep module's
52/// type_defs and fn_defs to find the record + smart constructor.
53/// Use [`refinement_info_for_in_scope`] when two modules declare a
54/// refined record of the same bare name (`A.Natural` vs `B.Natural`)
55/// and you need each scope's *own* predicate; the unscoped form
56/// returns whichever record walked first.
57pub fn refinement_info_for<'a>(
58    type_name: &str,
59    inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
60) -> Option<RefinementInfo<'a>> {
61    refinement_info_for_walk(type_name, inputs, None)
62}
63
64/// Module-scoped variant of [`refinement_info_for`]. `scope =
65/// None` means "look in entry items only"; `scope = Some(prefix)`
66/// means "look in the dep module whose `prefix` matches".
67///
68/// Refinement-via-opaque is a single-module pattern: the record is
69/// declared `exposes opaque [X]` and the smart constructor lives in
70/// the same module (the carrier field isn't reachable from outside,
71/// so the constructor can't reside elsewhere). Scoping the search
72/// to one module gives each scope its own predicate, which is what
73/// `populate_refined_types` needs so canonical `A.Natural` and
74/// `B.Natural` slots don't share a predicate from whichever module
75/// happened to walk first.
76pub fn refinement_info_for_in_scope<'a>(
77    type_name: &str,
78    inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
79    scope: Option<&str>,
80) -> Option<RefinementInfo<'a>> {
81    refinement_info_for_walk(type_name, inputs, Some(scope))
82}
83
84fn refinement_info_for_walk<'a>(
85    type_name: &str,
86    inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
87    // Outer Option: None = "any scope" (legacy); Some(inner) =
88    // module-scoped. Inner Option: None = entry, Some(prefix) =
89    // module by prefix.
90    scope_filter: Option<Option<&str>>,
91) -> Option<RefinementInfo<'a>> {
92    // Stage 6b of #232: when the caller threaded a `ProgramShape`
93    // through `ProofLowerInputs.program_shape`, look up the typed
94    // pattern directly instead of walking the AST. The
95    // `detection_payload_matches_refinement_info_for` test guards
96    // that the two paths return the same carrier/predicate, so this
97    // adapter is behavior-preserving.
98    if let Some(shape) = inputs.program_shape
99        && let Some(info) = refinement_info_from_shape(shape, inputs, type_name, scope_filter)
100    {
101        return Some(info);
102    }
103    refinement_info_for_walk_legacy(type_name, inputs, scope_filter)
104}
105
106/// Stage 6b adapter: pick the typed `RefinementSmartConstructor`
107/// pattern from `ProgramShape.patterns` and project it back into the
108/// legacy `RefinementInfo<'a>` shape (with `&'a Spanned<Expr>`
109/// predicate). The predicate is borrowed from the matching smart
110/// constructor's body in `inputs.entry_items` / `inputs.dep_modules` —
111/// the pattern's owned clone is only used for the cross-check test.
112fn refinement_info_from_shape<'a>(
113    shape: &crate::analysis::shape::ProgramShape,
114    inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
115    type_name: &str,
116    scope_filter: Option<Option<&str>>,
117) -> Option<RefinementInfo<'a>> {
118    use crate::analysis::shape::ModulePattern;
119
120    let pattern_match = shape.patterns.iter().find_map(|p| {
121        let ModulePattern::RefinementSmartConstructor {
122            scope,
123            type_name: ptn,
124            constructor_fn,
125            ..
126        } = p
127        else {
128            return None;
129        };
130        if ptn != type_name {
131            return None;
132        }
133        let scope_ok = match (scope_filter, scope.as_deref()) {
134            (None, _) => true,
135            (Some(None), None) => true,
136            (Some(None), Some(_)) => false,
137            (Some(Some(want)), Some(have)) => want == have,
138            (Some(Some(_)), None) => false,
139        };
140        if !scope_ok {
141            return None;
142        }
143        Some((scope.clone(), constructor_fn.clone()))
144    })?;
145
146    let (scope, constructor_fn) = pattern_match;
147
148    let (carrier_field, carrier_type) = match scope.as_deref() {
149        None => inputs.entry_items.iter().find_map(|i| match i {
150            TopLevel::TypeDef(TypeDef::Product { name, fields, .. })
151                if name == type_name && fields.len() == 1 =>
152            {
153                let (fname, ftype) = &fields[0];
154                Some((fname.as_str(), ftype.as_str()))
155            }
156            _ => None,
157        }),
158        Some(prefix) => inputs
159            .dep_modules
160            .iter()
161            .find(|m| m.prefix == prefix)
162            .and_then(|m| {
163                m.type_defs.iter().find_map(|td| match td {
164                    TypeDef::Product { name, fields, .. }
165                        if name == type_name && fields.len() == 1 =>
166                    {
167                        let (fname, ftype) = &fields[0];
168                        Some((fname.as_str(), ftype.as_str()))
169                    }
170                    _ => None,
171                })
172            }),
173    }?;
174
175    let constructor_fd: &'a FnDef = match scope.as_deref() {
176        None => inputs.entry_items.iter().find_map(|i| match i {
177            TopLevel::FnDef(fd) if fd.name == constructor_fn => Some(fd),
178            _ => None,
179        }),
180        Some(prefix) => inputs
181            .dep_modules
182            .iter()
183            .find(|m| m.prefix == prefix)
184            .and_then(|m| m.fn_defs.iter().find(|fd| fd.name == constructor_fn)),
185    }?;
186
187    let (param_name, _) = constructor_fd.params.first()?;
188    let stmts = constructor_fd.body.stmts();
189    let Stmt::Expr(body_expr) = stmts.first()? else {
190        return None;
191    };
192    let Expr::Match { subject, .. } = &body_expr.node else {
193        return None;
194    };
195
196    Some(RefinementInfo {
197        carrier_type,
198        carrier_field,
199        param_name,
200        constructor_fn: constructor_fd.name.as_str(),
201        predicate: subject,
202    })
203}
204
205fn refinement_info_for_walk_legacy<'a>(
206    type_name: &str,
207    inputs: &crate::codegen::proof_lower::ProofLowerInputs<'a>,
208    scope_filter: Option<Option<&str>>,
209) -> Option<RefinementInfo<'a>> {
210    let entry_only = matches!(scope_filter, Some(None));
211    let module_only_prefix = match scope_filter {
212        Some(Some(p)) => Some(p),
213        _ => None,
214    };
215    let allow_entry = scope_filter.is_none() || entry_only;
216    let entry_typedefs: Vec<&'a TypeDef> = if allow_entry {
217        inputs
218            .entry_items
219            .iter()
220            .filter_map(|item| match item {
221                TopLevel::TypeDef(td) => Some(td),
222                _ => None,
223            })
224            .collect()
225    } else {
226        Vec::new()
227    };
228    let module_typedefs: Vec<&'a TypeDef> = inputs
229        .dep_modules
230        .iter()
231        .filter(|m| match (scope_filter, module_only_prefix) {
232            (None, _) => true,
233            (Some(None), _) => false,
234            (Some(Some(_)), Some(p)) => m.prefix == p,
235            _ => false,
236        })
237        .flat_map(|m| m.type_defs.iter())
238        .collect();
239    let (carrier_field, carrier_type) = entry_typedefs
240        .into_iter()
241        .chain(module_typedefs)
242        .find_map(|td| match td {
243            TypeDef::Product { name, fields, .. } if name == type_name && fields.len() == 1 => {
244                let (fname, ftype) = &fields[0];
245                Some((fname.as_str(), ftype.as_str()))
246            }
247            _ => None,
248        })?;
249
250    let entry_fns: Vec<&'a FnDef> = if allow_entry {
251        inputs
252            .entry_items
253            .iter()
254            .filter_map(|item| match item {
255                TopLevel::FnDef(fd) => Some(fd),
256                _ => None,
257            })
258            .collect()
259    } else {
260        Vec::new()
261    };
262    let module_fns: Vec<&'a FnDef> = inputs
263        .dep_modules
264        .iter()
265        .filter(|m| match (scope_filter, module_only_prefix) {
266            (None, _) => true,
267            (Some(None), _) => false,
268            (Some(Some(_)), Some(p)) => m.prefix == p,
269            _ => false,
270        })
271        .flat_map(|m| m.fn_defs.iter())
272        .collect();
273    for fd in entry_fns.into_iter().chain(module_fns) {
274        if !fd.return_type.starts_with("Result<") {
275            continue;
276        }
277        if !fd.return_type[7..].starts_with(type_name) {
278            continue;
279        }
280        if fd.params.len() != 1 {
281            continue;
282        }
283        let (param_name, _) = &fd.params[0];
284        let stmts = fd.body.stmts();
285        if stmts.len() != 1 {
286            continue;
287        }
288        let Stmt::Expr(body_expr) = &stmts[0] else {
289            continue;
290        };
291        let Expr::Match { subject, arms } = &body_expr.node else {
292            continue;
293        };
294        if !is_bool_ok_err_match(arms, type_name, carrier_field, param_name) {
295            continue;
296        }
297        return Some(RefinementInfo {
298            carrier_type,
299            carrier_field,
300            param_name,
301            constructor_fn: fd.name.as_str(),
302            predicate: subject,
303        });
304    }
305    None
306}
307
308/// True iff a two-arm bool match is the canonical refinement shape:
309/// `true -> Result.Ok(<TypeName>(<carrier_field> = <param>))` and
310/// `false -> Result.Err(_)`. Required so we don't mis-classify a
311/// random `match … -> Result.Ok(...) | -> Result.Err(...)` (e.g. an
312/// effectful pipeline) as a smart constructor.
313pub fn is_refinement_bool_ok_err_match(
314    arms: &[MatchArm],
315    type_name: &str,
316    carrier_field: &str,
317    param_name: &str,
318) -> bool {
319    is_bool_ok_err_match(arms, type_name, carrier_field, param_name)
320}
321
322fn is_bool_ok_err_match(
323    arms: &[MatchArm],
324    type_name: &str,
325    carrier_field: &str,
326    param_name: &str,
327) -> bool {
328    if arms.len() != 2 {
329        return false;
330    }
331    let mut true_ok = false;
332    let mut false_err = false;
333    for arm in arms {
334        match &arm.pattern {
335            Pattern::Literal(Literal::Bool(true)) => {
336                if is_ok_constructor_with_identity(&arm.body, type_name, carrier_field, param_name)
337                {
338                    true_ok = true;
339                }
340            }
341            Pattern::Literal(Literal::Bool(false)) => {
342                if is_err_constructor(&arm.body) {
343                    false_err = true;
344                }
345            }
346            _ => return false,
347        }
348    }
349    true_ok && false_err
350}
351
352fn is_ok_constructor_with_identity(
353    expr: &Spanned<Expr>,
354    type_name: &str,
355    carrier_field: &str,
356    param_name: &str,
357) -> bool {
358    // Result.Ok(<TypeName>(<carrier_field> = <param>))
359    let (ctor_name, ctor_arg_node) = match &expr.node {
360        Expr::Constructor(name, Some(arg)) => (name.clone(), &arg.node),
361        Expr::FnCall(callee, args) if args.len() == 1 => {
362            let Some(name) = expr_to_dotted_name(&callee.node) else {
363                return false;
364            };
365            (name, &args[0].node)
366        }
367        _ => return false,
368    };
369    if ctor_name != "Result.Ok" {
370        return false;
371    }
372    let (t, fields) = match ctor_arg_node {
373        Expr::RecordCreate {
374            type_name: t,
375            fields,
376        } => (t.as_str(), fields),
377        _ => return false,
378    };
379    if t != type_name || fields.len() != 1 {
380        return false;
381    }
382    let (fname, fvalue) = &fields[0];
383    if fname != carrier_field {
384        return false;
385    }
386    // Post-resolver bodies have `Expr::Resolved` instead of
387    // `Expr::Ident` for fn-param references; accept both shapes so
388    // detection works regardless of which stage of the pipeline we
389    // run in.
390    match &fvalue.node {
391        Expr::Ident(name) | Expr::Resolved { name, .. } => name == param_name,
392        _ => false,
393    }
394}
395
396/// Walk `lhs`/`rhs` looking for `RecordCreate { type_name: X, fields:
397/// [(_, Ident(given_name))] }` where `X` is a refinement record whose
398/// carrier matches `given_type`. Returns the refined type name when
399/// found, so callers can lift `given_name`'s quantifier from the
400/// carrier type to the refined type. Without this, theorems would
401/// emit `∀ (a : Int), … RecordCreate(a) …` where the smart-
402/// constructor predicate has to be discharged from `a`'s `when`
403/// clause inside the theorem type — which is exactly what the
404/// previous heuristic-laden auto-proof had to work around.
405pub fn refinement_lift_for_given(
406    given_name: &str,
407    given_type: &str,
408    lhs: &Spanned<Expr>,
409    rhs: &Spanned<Expr>,
410    ctx: &CodegenContext,
411) -> Option<String> {
412    // Float carriers don't get lifted: `Int.add_comm` exists and is
413    // universally provable in Lean's `Int` model, but `Float.add_
414    // comm` doesn't hold across IEEE 754 — `NaN ≠ NaN` blows up
415    // the universal claim. Sample-form assertions (concrete Float
416    // values, no NaN in the declared `given` domain) still pass
417    // through the older auto-proof shape; we only lift when the
418    // underlying arithmetic has a true universal law.
419    if given_type == "Float" {
420        return None;
421    }
422    // Round-4 finding 2: return the *canonical key* the IR uses
423    // (e.g. `AAA.Natural` for module-owned, bare `Natural` for
424    // entry), not the bare `TypeDef.name`. Downstream
425    // `strip_refinement_wrappers` / `when_is_redundant` / backend
426    // emit all key off this same identifier, so two modules with
427    // distinct refined `Natural` records can no longer merge under
428    // a single bare name.
429    let mut result: Option<String> = None;
430    search_refinement_wrapper(lhs, given_name, given_type, ctx, &mut result);
431    search_refinement_wrapper(rhs, given_name, given_type, ctx, &mut result);
432    result
433}
434
435fn search_refinement_wrapper(
436    expr: &Spanned<Expr>,
437    given_name: &str,
438    given_type: &str,
439    ctx: &CodegenContext,
440    result: &mut Option<String>,
441) {
442    if result.is_some() {
443        return;
444    }
445    match &expr.node {
446        Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
447            let (_, fvalue) = &fields[0];
448            let matches_var = matches!(
449                &fvalue.node,
450                Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == given_name
451            );
452            if matches_var
453                && let Some((canonical_key, decl)) = find_refined_type_with_key(ctx, type_name)
454                && decl.carrier_type == given_type
455            {
456                *result = Some(canonical_key);
457                return;
458            }
459            for (_, v) in fields {
460                search_refinement_wrapper(v, given_name, given_type, ctx, result);
461            }
462        }
463        Expr::FnCall(callee, args) => {
464            search_refinement_wrapper(callee, given_name, given_type, ctx, result);
465            for a in args {
466                search_refinement_wrapper(a, given_name, given_type, ctx, result);
467            }
468        }
469        Expr::BinOp(_, l, r) => {
470            search_refinement_wrapper(l, given_name, given_type, ctx, result);
471            search_refinement_wrapper(r, given_name, given_type, ctx, result);
472        }
473        Expr::Attr(o, _) => search_refinement_wrapper(o, given_name, given_type, ctx, result),
474        Expr::Neg(i) | Expr::ErrorProp(i) => {
475            search_refinement_wrapper(i, given_name, given_type, ctx, result);
476        }
477        Expr::Match { subject, arms } => {
478            search_refinement_wrapper(subject, given_name, given_type, ctx, result);
479            for arm in arms {
480                search_refinement_wrapper(&arm.body, given_name, given_type, ctx, result);
481            }
482        }
483        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
484            for it in items {
485                search_refinement_wrapper(it, given_name, given_type, ctx, result);
486            }
487        }
488        Expr::Constructor(_, Some(arg)) => {
489            search_refinement_wrapper(arg, given_name, given_type, ctx, result);
490        }
491        _ => {}
492    }
493}
494
495/// Strip `RecordCreate { type_name: X, fields: [(_, Ident(g))] }` →
496/// `Ident(g)` when `g` is in `lifted_vars` and `X` is the refined
497/// type those vars were lifted to. Used after `refinement_lift_for_
498/// given` decides the lift: theorem body talks about `g : Natural`
499/// directly, so the `Natural(value = g)` wrapper that aver source
500/// wrote becomes redundant noise.
501/// Rewrite bare references to refinement-lifted given variables
502/// (`a` → `a.val`) inside scalar / arithmetic contexts so the emitted
503/// Lean expression typechecks against the Subtype carrier.
504///
505/// Background: a `verify ... law` with `given a: Int` whose body
506/// wraps `a` in `Natural(value = a)` lifts the quantifier from
507/// `∀ (a : Int)` to `∀ (a : Natural)`. The law's `when` clause stays
508/// in the user's variable space — `when a >= 10` references the bare
509/// `a`, which in Lean is now the Subtype `Natural := { v : Int //
510/// v ≥ 0 }`, NOT the underlying Int. `a >= 10` then fails to
511/// synthesize an `LE Natural` / `OfNat Natural 10` instance.
512///
513/// Transform projects bare lifted-var Idents to `.val` ONLY inside
514/// `BinOp` comparators and the `Bool.and` / `Bool.or` chains the
515/// parser builds for multi-`when` lines. Lifted-var refs in function
516/// arguments (e.g. `add(a, b)` over `a, b : Natural`) stay bare —
517/// those positions DO expect the Subtype carrier, not the underlying
518/// Int. The two cases mirror `strip_refinement_wrappers`'s
519/// counterpart (stripping `Natural(value = a)` → `a` in
520/// function-arg positions).
521pub fn project_lifted_idents_to_val(
522    expr: &Spanned<Expr>,
523    lifted_vars: &std::collections::HashMap<String, String>,
524) -> Spanned<Expr> {
525    if lifted_vars.is_empty() {
526        return expr.clone();
527    }
528    let new_node = match &expr.node {
529        Expr::BinOp(op, l, r) if is_comparator_binop(*op) => {
530            let l_proj = project_lifted_ident_leaf(l, lifted_vars);
531            let r_proj = project_lifted_ident_leaf(r, lifted_vars);
532            Expr::BinOp(*op, Box::new(l_proj), Box::new(r_proj))
533        }
534        Expr::FnCall(callee, args) => {
535            let name = expr_to_dotted_name(&callee.node);
536            if matches!(name.as_deref(), Some("Bool.and") | Some("Bool.or")) {
537                Expr::FnCall(
538                    callee.clone(),
539                    args.iter()
540                        .map(|a| project_lifted_idents_to_val(a, lifted_vars))
541                        .collect(),
542                )
543            } else {
544                return expr.clone();
545            }
546        }
547        _ => return expr.clone(),
548    };
549    Spanned::new(new_node, expr.line)
550}
551
552fn is_comparator_binop(op: crate::ast::BinOp) -> bool {
553    use crate::ast::BinOp::*;
554    matches!(op, Lt | Gt | Lte | Gte | Eq | Neq)
555}
556
557fn project_lifted_ident_leaf(
558    expr: &Spanned<Expr>,
559    lifted_vars: &std::collections::HashMap<String, String>,
560) -> Spanned<Expr> {
561    let target_name = match &expr.node {
562        Expr::Ident(n) | Expr::Resolved { name: n, .. } => n,
563        _ => return expr.clone(),
564    };
565    if lifted_vars.contains_key(target_name) {
566        Spanned::new(
567            Expr::Attr(
568                Box::new(Spanned::new(Expr::Ident(target_name.clone()), expr.line)),
569                "val".to_string(),
570            ),
571            expr.line,
572        )
573    } else {
574        expr.clone()
575    }
576}
577
578pub fn strip_refinement_wrappers(
579    expr: &Spanned<Expr>,
580    lifted_vars: &std::collections::HashMap<String, String>,
581    ctx: &CodegenContext,
582) -> Spanned<Expr> {
583    let new_node = match &expr.node {
584        Expr::RecordCreate { type_name, fields } if fields.len() == 1 => {
585            let (_, fvalue) = &fields[0];
586            let var_name = match &fvalue.node {
587                Expr::Ident(n) | Expr::Resolved { name: n, .. } => Some(n.clone()),
588                _ => None,
589            };
590            // Round-4 finding 2: `lifted_vars[name]` holds the
591            // *canonical key* (`AAA.Natural` for module-owned, bare
592            // for entry). Canonicalise the AST's `type_name` against
593            // the same resolver so a bare `Natural` written inside a
594            // dep module strips against `AAA.Natural`, not the
595            // unrelated entry-bare slot of the same name.
596            let canonical_for_ast = find_refined_type_with_key(ctx, type_name).map(|(k, _)| k);
597            if let Some(name) = var_name
598                && let Some(refined) = lifted_vars.get(&name)
599                && canonical_for_ast.as_deref() == Some(refined.as_str())
600            {
601                return Spanned::new(Expr::Ident(name), expr.line);
602            }
603            let new_fields: Vec<(String, Spanned<Expr>)> = fields
604                .iter()
605                .map(|(n, v)| (n.clone(), strip_refinement_wrappers(v, lifted_vars, ctx)))
606                .collect();
607            Expr::RecordCreate {
608                type_name: type_name.clone(),
609                fields: new_fields,
610            }
611        }
612        Expr::FnCall(callee, args) => Expr::FnCall(
613            Box::new(strip_refinement_wrappers(callee, lifted_vars, ctx)),
614            args.iter()
615                .map(|a| strip_refinement_wrappers(a, lifted_vars, ctx))
616                .collect(),
617        ),
618        Expr::BinOp(op, l, r) => Expr::BinOp(
619            *op,
620            Box::new(strip_refinement_wrappers(l, lifted_vars, ctx)),
621            Box::new(strip_refinement_wrappers(r, lifted_vars, ctx)),
622        ),
623        Expr::Attr(o, f) => Expr::Attr(
624            Box::new(strip_refinement_wrappers(o, lifted_vars, ctx)),
625            f.clone(),
626        ),
627        Expr::Neg(i) => Expr::Neg(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx))),
628        Expr::ErrorProp(i) => {
629            Expr::ErrorProp(Box::new(strip_refinement_wrappers(i, lifted_vars, ctx)))
630        }
631        _ => expr.node.clone(),
632    };
633    Spanned::new(new_node, expr.line)
634}
635
636/// Swap a comparison BinOp's operands canonically: `a OP b` ≡ `b OP' a`
637/// where OP' is the commutator-flipped op (`Lt ↔ Gt`, `Lte ↔ Gte`,
638/// `Eq` and `Neq` symmetric). Returns `None` for non-comparator BinOps.
639/// Used by `predicate_syntactic_eq` so `0 <= a` matches `a >= 0` for the
640/// `when`-vs-refinement-invariant check.
641pub fn swap_comparison_operands_op(op: &crate::ast::BinOp) -> Option<crate::ast::BinOp> {
642    use crate::ast::BinOp::*;
643    match op {
644        Lt => Some(Gt),
645        Gt => Some(Lt),
646        Lte => Some(Gte),
647        Gte => Some(Lte),
648        Eq => Some(Eq),
649        Neq => Some(Neq),
650        _ => None,
651    }
652}
653
654/// Structural equality on Aver predicate expressions with commutator
655/// relaxation: at every `BinOp` comparator node, allow the operands +
656/// operator to be swapped. Both `a >= 0` and `0 <= a` compare equal,
657/// recursively. Non-comparator BinOps (`Add`, `Sub`, ...) and other
658/// `Expr` variants fall through to the derived `PartialEq` on
659/// `Spanned<Expr>` (which compares `.node` only — line numbers don't
660/// participate). Used by the `when`-vs-refinement-invariant identity
661/// check so a redundantly-written user `when` gets recognised even when
662/// the operand order doesn't match the smart constructor's predicate
663/// verbatim.
664pub fn predicate_syntactic_eq(a: &Spanned<Expr>, b: &Spanned<Expr>) -> bool {
665    match (&a.node, &b.node) {
666        (Expr::BinOp(op_a, la, ra), Expr::BinOp(op_b, lb, rb)) => {
667            if op_a == op_b && predicate_syntactic_eq(la, lb) && predicate_syntactic_eq(ra, rb) {
668                return true;
669            }
670            if let Some(swapped) = swap_comparison_operands_op(op_a)
671                && &swapped == op_b
672                && predicate_syntactic_eq(la, rb)
673                && predicate_syntactic_eq(ra, lb)
674            {
675                return true;
676            }
677            false
678        }
679        _ => a.node == b.node,
680    }
681}
682
683/// Flatten a chain of `Bool.and(a, b)` calls into the flat list of
684/// leaf predicates. Aver's `when a >= 0` / `when b >= 0` syntax folds
685/// multiple `when` lines into nested `Bool.and(prev, next)` at parse
686/// time (see `parser/blocks.rs`'s law-block loop), so the predicate
687/// arrives at codegen as `Bool.and(Bool.and(p1, p2), p3)`. Identity
688/// checks against per-given refinement invariants need the flat shape.
689pub fn flatten_bool_and_conjuncts(expr: &Spanned<Expr>) -> Vec<Spanned<Expr>> {
690    if let Expr::FnCall(callee, args) = &expr.node
691        && args.len() == 2
692        && let Some(name) = expr_to_dotted_name(&callee.node)
693        && name == "Bool.and"
694    {
695        let mut out = flatten_bool_and_conjuncts(&args[0]);
696        out.extend(flatten_bool_and_conjuncts(&args[1]));
697        return out;
698    }
699    vec![expr.clone()]
700}
701
702/// `ResolvedExpr` mirror of [`flatten_bool_and_conjuncts`]. After the
703/// resolver lifts `Bool.and(a, b)` into
704/// `ResolvedExpr::Call(ResolvedCallee::Builtin("Bool.and"), args)`,
705/// the same recursive split holds.
706pub fn flatten_bool_and_conjuncts_resolved(
707    expr: &Spanned<crate::ir::hir::ResolvedExpr>,
708) -> Vec<Spanned<crate::ir::hir::ResolvedExpr>> {
709    use crate::ir::hir::{ResolvedCallee, ResolvedExpr};
710    if let ResolvedExpr::Call(ResolvedCallee::Builtin(name), args) = &expr.node
711        && name == "Bool.and"
712        && args.len() == 2
713    {
714        let mut out = flatten_bool_and_conjuncts_resolved(&args[0]);
715        out.extend(flatten_bool_and_conjuncts_resolved(&args[1]));
716        return out;
717    }
718    vec![expr.clone()]
719}
720
721/// `ResolvedExpr` mirror of [`predicate_syntactic_eq`].
722pub fn predicate_syntactic_eq_resolved(
723    a: &Spanned<crate::ir::hir::ResolvedExpr>,
724    b: &Spanned<crate::ir::hir::ResolvedExpr>,
725) -> bool {
726    use crate::ir::hir::ResolvedExpr;
727    match (&a.node, &b.node) {
728        (ResolvedExpr::BinOp(op_a, la, ra), ResolvedExpr::BinOp(op_b, lb, rb)) => {
729            if op_a == op_b
730                && predicate_syntactic_eq_resolved(la, lb)
731                && predicate_syntactic_eq_resolved(ra, rb)
732            {
733                return true;
734            }
735            if let Some(swapped) = swap_comparison_operands_op(op_a)
736                && &swapped == op_b
737                && predicate_syntactic_eq_resolved(la, rb)
738                && predicate_syntactic_eq_resolved(ra, lb)
739            {
740                return true;
741            }
742            false
743        }
744        _ => a.node == b.node,
745    }
746}
747
748/// `ResolvedExpr` mirror of [`substitute_ident_in_expr`]. Rewrites
749/// every `Ident(from)` / `Resolved { name: from, .. }` leaf to
750/// `Ident(to)` — the slot identity (if any) is dropped because the
751/// substitution targets a free variable name that doesn't have a
752/// slot in the resolver's local table. Used by proof-mode
753/// `when`-redundancy check + smart-guard predicate substitution
754/// after the IR carries pre-resolved expressions.
755pub fn substitute_ident_in_resolved_expr(
756    expr: &Spanned<crate::ir::hir::ResolvedExpr>,
757    from: &str,
758    to: &str,
759) -> Spanned<crate::ir::hir::ResolvedExpr> {
760    use crate::ir::hir::{ResolvedExpr, ResolvedMatchArm, ResolvedStrPart};
761    let line = expr.line;
762    let rec = |e: &Spanned<ResolvedExpr>| substitute_ident_in_resolved_expr(e, from, to);
763    let new_node = match &expr.node {
764        ResolvedExpr::Ident(name) | ResolvedExpr::Resolved { name, .. } if name == from => {
765            ResolvedExpr::Ident(to.to_string())
766        }
767        ResolvedExpr::Literal(_) | ResolvedExpr::Ident(_) | ResolvedExpr::Resolved { .. } => {
768            return expr.clone();
769        }
770        ResolvedExpr::Attr(obj, field) => ResolvedExpr::Attr(Box::new(rec(obj)), field.clone()),
771        ResolvedExpr::Call(callee, args) => {
772            ResolvedExpr::Call(callee.clone(), args.iter().map(&rec).collect())
773        }
774        ResolvedExpr::BinOp(op, left, right) => {
775            ResolvedExpr::BinOp(*op, Box::new(rec(left)), Box::new(rec(right)))
776        }
777        ResolvedExpr::Neg(inner) => ResolvedExpr::Neg(Box::new(rec(inner))),
778        ResolvedExpr::Match { subject, arms } => ResolvedExpr::Match {
779            subject: Box::new(rec(subject)),
780            arms: arms
781                .iter()
782                .map(|arm| ResolvedMatchArm {
783                    pattern: arm.pattern.clone(),
784                    body: Box::new(rec(&arm.body)),
785                    binding_slots: std::sync::OnceLock::new(),
786                })
787                .collect(),
788        },
789        ResolvedExpr::Ctor(ctor, args) => {
790            ResolvedExpr::Ctor(ctor.clone(), args.iter().map(&rec).collect())
791        }
792        ResolvedExpr::ErrorProp(inner) => ResolvedExpr::ErrorProp(Box::new(rec(inner))),
793        ResolvedExpr::InterpolatedStr(parts) => ResolvedExpr::InterpolatedStr(
794            parts
795                .iter()
796                .map(|p| match p {
797                    ResolvedStrPart::Literal(_) => p.clone(),
798                    ResolvedStrPart::Parsed(inner) => ResolvedStrPart::Parsed(Box::new(rec(inner))),
799                })
800                .collect(),
801        ),
802        ResolvedExpr::List(items) => ResolvedExpr::List(items.iter().map(&rec).collect()),
803        ResolvedExpr::Tuple(items) => ResolvedExpr::Tuple(items.iter().map(&rec).collect()),
804        ResolvedExpr::IndependentProduct(items, flag) => {
805            ResolvedExpr::IndependentProduct(items.iter().map(&rec).collect(), *flag)
806        }
807        ResolvedExpr::MapLiteral(entries) => {
808            ResolvedExpr::MapLiteral(entries.iter().map(|(k, v)| (rec(k), rec(v))).collect())
809        }
810        ResolvedExpr::RecordCreate {
811            type_id,
812            type_name,
813            fields,
814        } => ResolvedExpr::RecordCreate {
815            type_id: *type_id,
816            type_name: type_name.clone(),
817            fields: fields.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
818        },
819        ResolvedExpr::RecordUpdate {
820            type_id,
821            type_name,
822            base,
823            updates,
824        } => ResolvedExpr::RecordUpdate {
825            type_id: *type_id,
826            type_name: type_name.clone(),
827            base: Box::new(rec(base)),
828            updates: updates.iter().map(|(n, v)| (n.clone(), rec(v))).collect(),
829        },
830        ResolvedExpr::TailCall { target, args } => ResolvedExpr::TailCall {
831            target: *target,
832            args: args.iter().map(&rec).collect(),
833        },
834    };
835    Spanned::new(new_node, line)
836}
837
838/// Walk `expr` and rename every `Ident(from)` / `Resolved { name: from
839/// }` to `Ident(to)`. Lives here (not in `recursion`) because three
840/// proof-mode predicate sources reach for the same substitution:
841/// caller-guard extraction translates caller's local-var name to
842/// callee's param name; opaque-type `when`-redundancy check translates
843/// smart constructor's param name to the law's given name; future
844/// callers (verify-law domain translation, etc.) will too. Single
845/// definition keeps Lean and Dafny in sync.
846pub fn substitute_ident_in_expr(expr: &Spanned<Expr>, from: &str, to: &str) -> Spanned<Expr> {
847    use crate::ast::{MatchArm, StrPart, TailCallData};
848    let line = expr.line;
849    let new_node = match &expr.node {
850        Expr::Ident(name) | Expr::Resolved { name, .. } if name == from => {
851            Expr::Ident(to.to_string())
852        }
853        Expr::Literal(_) | Expr::Ident(_) | Expr::Resolved { .. } => return expr.clone(),
854        Expr::Attr(obj, field) => Expr::Attr(
855            Box::new(substitute_ident_in_expr(obj, from, to)),
856            field.clone(),
857        ),
858        Expr::FnCall(callee, args) => Expr::FnCall(
859            Box::new(substitute_ident_in_expr(callee, from, to)),
860            args.iter()
861                .map(|a| substitute_ident_in_expr(a, from, to))
862                .collect(),
863        ),
864        Expr::BinOp(op, left, right) => Expr::BinOp(
865            *op,
866            Box::new(substitute_ident_in_expr(left, from, to)),
867            Box::new(substitute_ident_in_expr(right, from, to)),
868        ),
869        Expr::Neg(inner) => Expr::Neg(Box::new(substitute_ident_in_expr(inner, from, to))),
870        Expr::Match { subject, arms } => Expr::Match {
871            subject: Box::new(substitute_ident_in_expr(subject, from, to)),
872            arms: arms
873                .iter()
874                .map(|arm| MatchArm {
875                    pattern: arm.pattern.clone(),
876                    body: Box::new(substitute_ident_in_expr(&arm.body, from, to)),
877                    binding_slots: std::sync::OnceLock::new(),
878                })
879                .collect(),
880        },
881        Expr::Constructor(name, arg) => Expr::Constructor(
882            name.clone(),
883            arg.as_ref()
884                .map(|inner| Box::new(substitute_ident_in_expr(inner, from, to))),
885        ),
886        Expr::ErrorProp(inner) => {
887            Expr::ErrorProp(Box::new(substitute_ident_in_expr(inner, from, to)))
888        }
889        Expr::InterpolatedStr(parts) => Expr::InterpolatedStr(
890            parts
891                .iter()
892                .map(|part| match part {
893                    StrPart::Literal(_) => part.clone(),
894                    StrPart::Parsed(inner) => {
895                        StrPart::Parsed(Box::new(substitute_ident_in_expr(inner, from, to)))
896                    }
897                })
898                .collect(),
899        ),
900        Expr::List(items) => Expr::List(
901            items
902                .iter()
903                .map(|item| substitute_ident_in_expr(item, from, to))
904                .collect(),
905        ),
906        Expr::Tuple(items) => Expr::Tuple(
907            items
908                .iter()
909                .map(|item| substitute_ident_in_expr(item, from, to))
910                .collect(),
911        ),
912        Expr::IndependentProduct(items, flag) => Expr::IndependentProduct(
913            items
914                .iter()
915                .map(|item| substitute_ident_in_expr(item, from, to))
916                .collect(),
917            *flag,
918        ),
919        Expr::MapLiteral(entries) => Expr::MapLiteral(
920            entries
921                .iter()
922                .map(|(k, v)| {
923                    (
924                        substitute_ident_in_expr(k, from, to),
925                        substitute_ident_in_expr(v, from, to),
926                    )
927                })
928                .collect(),
929        ),
930        Expr::RecordCreate { type_name, fields } => Expr::RecordCreate {
931            type_name: type_name.clone(),
932            fields: fields
933                .iter()
934                .map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
935                .collect(),
936        },
937        Expr::RecordUpdate {
938            type_name,
939            base,
940            updates,
941        } => Expr::RecordUpdate {
942            type_name: type_name.clone(),
943            base: Box::new(substitute_ident_in_expr(base, from, to)),
944            updates: updates
945                .iter()
946                .map(|(n, v)| (n.clone(), substitute_ident_in_expr(v, from, to)))
947                .collect(),
948        },
949        Expr::TailCall(boxed) => Expr::TailCall(Box::new(TailCallData::new(
950            boxed.target.clone(),
951            boxed
952                .args
953                .iter()
954                .map(|a| substitute_ident_in_expr(a, from, to))
955                .collect(),
956        ))),
957    };
958    Spanned::new(new_node, line)
959}
960
961/// True iff every refinement-lifted given's invariant is
962/// syntactically captured by some clause of `when` (and vice versa —
963/// a bijection between conjuncts). Used by both Lean and Dafny law
964/// emitters to decide whether `when` is provably redundant with the
965/// types of the lifted givens; if yes, drop it from the theorem
966/// premise (carrier is now the type's invariant); if no, keep it so
967/// the user's stronger / orthogonal predicate stays part of the claim
968/// and isn't silently lost.
969///
970/// Same `Spanned<Expr>`-as-predicate path opaque smart constructors
971/// already use — `refinement_info_for` provides the invariant, the
972/// substitution maps the smart constructor's param name into the
973/// given's variable space, and `predicate_syntactic_eq` does the
974/// commutator-relaxed compare. No new representation, no parallel
975/// emitter.
976pub fn when_is_redundant_with_refinement_lifts(
977    when_expr: &Spanned<Expr>,
978    lifted_vars: &std::collections::HashMap<String, String>,
979    ctx: &CodegenContext,
980) -> bool {
981    if lifted_vars.is_empty() {
982        return false;
983    }
984    // The `when` clause arrives as raw AST (verify block parses
985    // straight from source); the refinement invariants on the other
986    // side now ride [`crate::ir::hir::ResolvedExpr`] (Phase E PR 12
987    // Scope A). Resolve the `when` once and run the conjunct
988    // comparison in resolved space so identity holds modulo the
989    // resolver's canonicalisation (e.g. `Bool.and` lifts to
990    // `ResolvedCallee::Builtin("Bool.and")` on both sides).
991    let resolved_when = ctx.resolve_expr(when_expr, ctx.active_module_scope().as_deref());
992    let when_conjuncts = flatten_bool_and_conjuncts_resolved(&resolved_when);
993    // Flatten BOTH sides — IntRange-style refinement predicates carry a
994    // compound `Bool.and(n >= 0, n <= 100)` invariant; without
995    // flattening, a `when Bool.and(a >= 0, a <= 100)` user clause
996    // (which the parser also flattens into atoms during conjunct
997    // walk) would length-mismatch and keep the now-redundant
998    // premise. Same flatten on both sides keeps natural / positive
999    // / int_range behavior identical to pre-fix.
1000    // Round-3 finding 3: same canonical-IR routing as
1001    // `search_refinement_wrapper`. The legacy `refinement_info_for`
1002    // walks every module looking for a same-bare-name record, so
1003    // two modules with distinct predicates would share whichever
1004    // walked first. `find_refined_type` reads from
1005    // `proof_ir.refined_types` keyed by canonical `Module.Name`.
1006    let mut lifted_predicates: Vec<Spanned<crate::ir::hir::ResolvedExpr>> = Vec::new();
1007    for (given_name, refined_type) in lifted_vars {
1008        let Some(decl) = find_refined_type(ctx, refined_type) else {
1009            return false;
1010        };
1011        let substituted = substitute_ident_in_resolved_expr(
1012            &decl.invariant.expr,
1013            decl.predicate_param.as_str(),
1014            given_name,
1015        );
1016        lifted_predicates.extend(flatten_bool_and_conjuncts_resolved(&substituted));
1017    }
1018    if when_conjuncts.len() != lifted_predicates.len() {
1019        return false;
1020    }
1021    let mut matched = vec![false; lifted_predicates.len()];
1022    for wc in &when_conjuncts {
1023        let Some(idx) = (0..lifted_predicates.len())
1024            .find(|&i| !matched[i] && predicate_syntactic_eq_resolved(wc, &lifted_predicates[i]))
1025        else {
1026            return false;
1027        };
1028        matched[idx] = true;
1029    }
1030    true
1031}
1032
1033fn is_err_constructor(expr: &Spanned<Expr>) -> bool {
1034    match &expr.node {
1035        Expr::Constructor(name, Some(_)) => name == "Result.Err",
1036        Expr::FnCall(callee, args) if args.len() == 1 => {
1037            matches!(
1038                expr_to_dotted_name(&callee.node),
1039                Some(name) if name == "Result.Err"
1040            )
1041        }
1042        _ => false,
1043    }
1044}
1045
1046// Backend-neutral predicates on AST items — all three codegen backends
1047// (Lean, Dafny, Rust) want the same view of "is this pure?",
1048// "self-referencing type?", and "what's the name of this type def?".
1049
1050/// A function is pure if it declares no effects and isn't `main`.
1051pub fn is_pure_fn(fd: &FnDef) -> bool {
1052    fd.effects.is_empty() && fd.name != "main"
1053}
1054
1055/// Issue #128: do all of a law's givens carry a singleton domain?
1056///
1057/// Resolve a (possibly bare) type name to its [`RefinedTypeDecl`]
1058/// in `ctx.proof_ir.refined_types`. The IR is keyed by canonical
1059/// name (bare for entry types, `Module.Name` for module types);
1060/// callers see whatever the AST node carried (`Expr::RecordCreate
1061/// { type_name: "Natural" }` is bare, `obj.ty()` on a stamped
1062/// expression can be either).
1063///
1064/// Resolution order:
1065///   1. Direct lookup (covers exact-canonical hits and bare entry
1066///      types).
1067///   2. If no direct hit, walk `ctx.modules` for a type def with
1068///      this bare name and try `Module.Name` keys until one
1069///      resolves.
1070///
1071/// Returns `None` if no match — consistent with the previous
1072/// `HashMap::get` semantics. Two modules declaring the same bare
1073/// name (`A.Natural` + `B.Natural` with distinct predicates) each
1074/// get their own canonical slot now; a direct-canonical-key lookup
1075/// disambiguates correctly, and a bare-name lookup resolves to the
1076/// first module that owns the name (typechecker's
1077/// `cross_module_same_named_types_do_not_merge` rejects mixed
1078/// usage upstream, so the order-dependence is observed only when a
1079/// project carries one such refined type as actually-used and the
1080/// other as dead-code in deps).
1081pub fn find_refined_type<'a>(
1082    ctx: &'a CodegenContext,
1083    name: &str,
1084) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1085    find_refined_type_with_key_scoped(ctx, name, None).map(|(_, d)| d)
1086}
1087
1088/// Canonical backend key for a nominal `Type::Named` reference.
1089///
1090/// Epic #180 Phase 6 — preferred entry point for backend
1091/// registries (record-field maps, eq-helper registration,
1092/// type-default chasing, newtype underlying lookups) that
1093/// previously extracted `name` directly out of
1094/// `Type::Named { name, .. }`. The raw `name` field is
1095/// source-faithful (so a bare `Shape` reference inside module
1096/// `A` stays as `"Shape"` even when the declaration is
1097/// canonically `A.Shape`). Registry routing keyed on raw `name`
1098/// silently collides two modules' same-bare-name types under
1099/// one slot — keying by the canonical form is identity-safe by
1100/// construction.
1101///
1102/// Resolution order:
1103///
1104/// 1. `id: Some(type_id)` (the canonical case post-typecheck) →
1105///    `symbol_table.type_entry(id).key.canonical()`.
1106/// 2. `id: None` (builtins / unresolved refs that never went
1107///    through `canonicalise_type`) → fall back to the source-
1108///    faithful `name`. The resolver stamps `id: Some(_)`
1109///    whenever the name binds to a declared type, so `id: None`
1110///    survives only for refs the symbol table doesn't know.
1111/// 3. Non-`Named` types (compound shapes like `List<X>`,
1112///    `Result<A, B>`, tuples, maps) → `None`. Backend layout
1113///    registries key compound types by a canonical compound
1114///    string (`format!("List<{}>", inner)`) the caller builds
1115///    itself; this helper covers only the nominal case.
1116pub fn backend_named_type_key(ctx: &CodegenContext, ty: &crate::types::Type) -> Option<String> {
1117    let crate::types::Type::Named { id, name } = ty else {
1118        return None;
1119    };
1120    if let Some(type_id) = id {
1121        return Some(ctx.symbol_table.type_entry(*type_id).key.canonical());
1122    }
1123    Some(name.clone())
1124}
1125
1126/// Canonical backend key for a [`TypeDef`] declaration.
1127///
1128/// Epic #180 Phase 6 — paired with [`backend_named_type_key`] at
1129/// the registry-storage side. Backend registries insert
1130/// `TypeDef`s under the canonical key (`"Shape"` for entry,
1131/// `"A.Shape"` for module-owned) so cross-module same-bare-name
1132/// types occupy distinct slots and the
1133/// `backend_named_type_key`-derived lookup at the expression
1134/// side hits the right declaration.
1135///
1136/// Falls back to the bare `type_def_name(td)` when the symbol
1137/// table doesn't index this `TypeDef` (synthetic / builtin
1138/// inserts) — registry insertion paths accept both during the
1139/// migration window.
1140pub fn backend_type_def_key(ctx: &CodegenContext, td: &crate::ast::TypeDef) -> String {
1141    let key = type_key_for_decl(ctx, td);
1142    if ctx.symbol_table.type_id_of(&key).is_some() {
1143        key.canonical()
1144    } else {
1145        type_def_name(td).to_string()
1146    }
1147}
1148
1149/// Direct `TypeId` lookup against `ProofIR.refined_types`.
1150///
1151/// Epic #180 Phase 6 — preferred entry point for callers holding a
1152/// typed `Type::Named { id: Some(_), .. }` reference (the
1153/// typechecker / resolver canonicalises Named refs with `id` when
1154/// the name binds to a declared type). Skips the
1155/// name → `TypeKey` → `TypeId` chain that
1156/// [`find_refined_type_with_key_scoped`] walks for bare-string
1157/// callers — and is identity-safe across cross-module same-bare-
1158/// name twins, since `TypeId` carries the module disambiguation.
1159///
1160/// Returns `None` for builtins / unresolved refs whose `id` was
1161/// never stamped; callers should fall back to the name-based path
1162/// in that case (see [`find_refined_type_for_named`] for the
1163/// combined helper).
1164pub fn find_refined_type_by_id(
1165    ctx: &CodegenContext,
1166    type_id: crate::ir::TypeId,
1167) -> Option<&crate::ir::proof_ir::RefinedTypeDecl> {
1168    ctx.proof_ir.refined_types.get(&type_id)
1169}
1170
1171/// Refined-type lookup for a `Type::Named` ref. Prefers the
1172/// id-direct path when `id: Some(_)` is stamped, falls back to the
1173/// name-keyed resolution chain otherwise (builtins, unresolved
1174/// refs).
1175///
1176/// Epic #180 Phase 6 — preferred over hand-rolled
1177/// `Type::Named { name, .. }` destructure + `find_refined_type`
1178/// at sites that consume `Spanned<ResolvedExpr>.ty()`.
1179pub fn find_refined_type_for_named<'a>(
1180    ctx: &'a CodegenContext,
1181    named: &crate::types::Type,
1182) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1183    let crate::types::Type::Named { id, name } = named else {
1184        return None;
1185    };
1186    match id {
1187        Some(type_id) => find_refined_type_by_id(ctx, *type_id),
1188        None => find_refined_type(ctx, name),
1189    }
1190}
1191
1192/// Scope-aware fn-contract resolver. `scope = Some(prefix)` directs
1193/// bare-name lookups to that module's slot before falling back to
1194/// entry / module-walk. Mirror of `find_refined_type_scoped` for fn
1195/// contracts — without it, two modules with same-bare-name
1196/// recursive fns silently merge under whichever module-walk hit
1197/// first.
1198///
1199/// Prefer `find_fn_contract_for_fn` whenever you have a `&FnDef` in
1200/// hand: it resolves identity by pointer-eq against
1201/// `ctx.modules[*].fn_defs` and avoids the bare-name footgun this
1202/// scoped lookup still inherits from string-based call sites.
1203pub fn find_fn_contract_scoped<'a>(
1204    ctx: &'a CodegenContext,
1205    name: &str,
1206    scope: Option<&str>,
1207) -> Option<&'a crate::ir::proof_ir::FnContract> {
1208    // Round-7: `ProofIR.fn_contracts` is keyed by opaque `FnId`
1209    // after the phase-E migration. Resolve `name` → `FnKey` →
1210    // `FnId` through the symbol table, then look up the contract.
1211    // Without a symbol table (legacy callers that haven't enabled
1212    // `run_build_symbols`) this returns `None`, which the
1213    // downstream emit path treats the same as "no contract" — the
1214    // table is part of every production build flow.
1215    let symbols = &ctx.symbol_table;
1216    let bare = name.rsplit('.').next().unwrap_or(name);
1217    let name_is_already_qualified = name.contains('.');
1218    let try_key = |key: crate::ir::FnKey| -> Option<&'a crate::ir::proof_ir::FnContract> {
1219        let id = symbols.fn_id_of(&key)?;
1220        ctx.proof_ir.fn_contracts.get(&id)
1221    };
1222    if let Some(prefix) = scope
1223        && !name_is_already_qualified
1224        && let Some(c) = try_key(crate::ir::FnKey::in_module(prefix.to_string(), bare))
1225    {
1226        return Some(c);
1227    }
1228    let direct_key =
1229        if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1230            crate::ir::FnKey::in_module(prefix.to_string(), bare_part)
1231        } else {
1232            crate::ir::FnKey::entry(name)
1233        };
1234    if let Some(c) = try_key(direct_key) {
1235        return Some(c);
1236    }
1237    for m in &ctx.modules {
1238        for fd in &m.fn_defs {
1239            if fd.name == bare
1240                && let Some(c) = try_key(crate::ir::FnKey::in_module(m.prefix.clone(), bare))
1241            {
1242                return Some(c);
1243            }
1244        }
1245    }
1246    None
1247}
1248
1249/// Scoped membership check.
1250pub fn fn_contract_exists_scoped(ctx: &CodegenContext, name: &str, scope: Option<&str>) -> bool {
1251    find_fn_contract_scoped(ctx, name, scope).is_some()
1252}
1253
1254/// Round-7: build a [`crate::ir::FnKey`] from a borrowed `&FnDef`.
1255/// The owning module is resolved by pointer-comparison against
1256/// `ctx.modules[*].fn_defs` (entry items / synthesized variants /
1257/// `extra_fn_defs` fall through to `FnKey::entry`). This is the
1258/// single resolver consumers should reach for from emit code with
1259/// a `&FnDef` in hand — produces the typed key the IR maps store
1260/// instead of leaving the bare-name collision risk in place.
1261pub fn fn_key_for_decl(ctx: &CodegenContext, fd: &FnDef) -> crate::ir::FnKey {
1262    match fn_owning_scope_for(ctx, fd) {
1263        Some(prefix) => crate::ir::FnKey::in_module(prefix.to_string(), fd.name.clone()),
1264        None => crate::ir::FnKey::entry(fd.name.clone()),
1265    }
1266}
1267
1268/// Round-7: build a [`crate::ir::TypeKey`] from a borrowed
1269/// `&TypeDef`. Same shape as [`fn_key_for_decl`] — pointer-eq
1270/// against `ctx.modules[*].type_defs` resolves the owning module.
1271pub fn type_key_for_decl(ctx: &CodegenContext, td: &TypeDef) -> crate::ir::TypeKey {
1272    for m in &ctx.modules {
1273        for t in &m.type_defs {
1274            if std::ptr::eq(t, td) {
1275                return crate::ir::TypeKey::in_module(m.prefix.clone(), type_def_name(td));
1276            }
1277        }
1278    }
1279    crate::ir::TypeKey::entry(type_def_name(td))
1280}
1281
1282/// Round-7: resolve a (possibly bare) type name from the AST into
1283/// a [`crate::ir::TypeKey`]. `scope` is the emit-time current scope
1284/// (`Some(prefix)` inside a per-module emit loop, `None` for
1285/// entry). Bare names prefer the current scope, then entry, then
1286/// fall back to module-walk first match — mirroring
1287/// [`find_refined_type_scoped`]'s resolution order.
1288pub fn type_key_for_name(
1289    ctx: &CodegenContext,
1290    name: &str,
1291    scope: Option<&str>,
1292) -> crate::ir::TypeKey {
1293    let bare = name.rsplit('.').next().unwrap_or(name);
1294    let name_is_qualified = name.contains('.');
1295    if let Some(prefix) = scope
1296        && !name_is_qualified
1297    {
1298        for m in &ctx.modules {
1299            if m.prefix == prefix && m.type_defs.iter().any(|td| type_def_name(td) == bare) {
1300                return crate::ir::TypeKey::in_module(prefix.to_string(), bare);
1301            }
1302        }
1303    }
1304    // Entry-declared types win the bare lookup when no scope is
1305    // active (or when scope's module doesn't own the name).
1306    if !name_is_qualified && ctx.type_defs.iter().any(|td| type_def_name(td) == bare) {
1307        return crate::ir::TypeKey::entry(bare);
1308    }
1309    if name_is_qualified
1310        && let Some((prefix, bare_part)) = name.rsplit_once('.')
1311        && ctx.modules.iter().any(|m| m.prefix == prefix)
1312    {
1313        return crate::ir::TypeKey::in_module(prefix.to_string(), bare_part);
1314    }
1315    for m in &ctx.modules {
1316        if m.type_defs.iter().any(|td| type_def_name(td) == bare) {
1317            return crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
1318        }
1319    }
1320    // Unresolved — fall back to entry key with the original text.
1321    // Callers using this typically end up with a miss in the IR
1322    // map, which is the same outcome as the pre-FnKey
1323    // `find_refined_type` returning `None`.
1324    crate::ir::TypeKey::entry(name)
1325}
1326
1327/// Round-6: resolve a `&FnDef`'s owning scope by pointer comparison
1328/// against `ctx.modules[*].fn_defs`. Pointer-eq sidesteps the
1329/// bare-name collision — `&CountdownA.fn_defs[0]` and
1330/// `&CountdownB.fn_defs[0]` are *distinct* addresses even with
1331/// `fd.name = "countdown"` in both.
1332///
1333/// Returns `None` when the `fd` is not in any dep module — entry
1334/// items, synthesized buffered variants, and `extra_fn_defs` all
1335/// fall through. Callers treat `None` as "entry scope".
1336pub fn fn_owning_scope_for<'a>(ctx: &'a CodegenContext, fd: &FnDef) -> Option<&'a str> {
1337    for m in &ctx.modules {
1338        for f in &m.fn_defs {
1339            if std::ptr::eq(f, fd) {
1340                return Some(m.prefix.as_str());
1341            }
1342        }
1343    }
1344    None
1345}
1346
1347/// Convenience: [`find_fn_contract_scoped`] with the scope resolved
1348/// by pointer-eq against `ctx.modules`. Use this from emit sites
1349/// that have `&FnDef` in hand — never the bare-name variant — so a
1350/// module-owned recursive fn always resolves to its OWN canonical
1351/// slot, even if another module exports a same-bare-name fn.
1352pub fn find_fn_contract_for_fn<'a>(
1353    ctx: &'a CodegenContext,
1354    fd: &FnDef,
1355) -> Option<&'a crate::ir::proof_ir::FnContract> {
1356    // Direct path: resolve the `&FnDef` to a `FnKey` via pointer-eq
1357    // scope detection, then look up the `FnId` in the symbol table
1358    // and the contract by ID. Cleaner than the bare-name resolver
1359    // chain because the source `&FnDef` already identifies its
1360    // owning module unambiguously.
1361    let symbols = &ctx.symbol_table;
1362    let fn_key = fn_key_for_decl(ctx, fd);
1363    let fn_id = symbols.fn_id_of(&fn_key)?;
1364    ctx.proof_ir.fn_contracts.get(&fn_id)
1365}
1366
1367/// Membership check counterpart of [`find_fn_contract_for_fn`].
1368pub fn fn_contract_exists_for_fn(ctx: &CodegenContext, fd: &FnDef) -> bool {
1369    find_fn_contract_for_fn(ctx, fd).is_some()
1370}
1371
1372/// Resolve `&FnDef` to the opaque [`crate::ir::FnId`] from the
1373/// symbol table. Pointer-eq scope detection routes module-owned
1374/// fns through their canonical `Module.fn` key. Returns `None` when
1375/// the fn isn't registered (built-ins / synthesized variants the
1376/// table excludes by design).
1377pub fn fn_id_for_decl(ctx: &CodegenContext, fd: &FnDef) -> Option<crate::ir::FnId> {
1378    let fn_key = fn_key_for_decl(ctx, fd);
1379    ctx.symbol_table.fn_id_of(&fn_key)
1380}
1381
1382/// Resolve a dotted source-level name (`fn`, `Module.fn`) to the
1383/// opaque `FnId`. Used by emit code that walks AST expressions
1384/// (e.g. detecting calls to opaque-emitted fns inside a law body)
1385/// — keeps the FnKey resolution in one place instead of letting
1386/// each walker re-derive identity from bare strings.
1387pub fn fn_id_for_dotted_name(ctx: &CodegenContext, dotted: &str) -> Option<crate::ir::FnId> {
1388    let key = if let Some((prefix, bare)) = dotted.rsplit_once('.') {
1389        crate::ir::FnKey::in_module(prefix.to_string(), bare)
1390    } else {
1391        crate::ir::FnKey::entry(dotted)
1392    };
1393    ctx.symbol_table.fn_id_of(&key)
1394}
1395
1396/// Canonical-key-aware resolver — returns `(canonical_key, decl)`
1397/// so consumers thread one stable identifier through refinement
1398/// lift / strip-wrappers / when-redundancy / backend emit instead
1399/// of recomputing identity from bare AST names.
1400pub fn find_refined_type_with_key<'a>(
1401    ctx: &'a CodegenContext,
1402    name: &str,
1403) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1404    find_refined_type_with_key_scoped(ctx, name, None)
1405}
1406
1407/// Module-scope-aware variant of [`find_refined_type`]. When the
1408/// caller knows which module's emit-pass is in flight (`scope =
1409/// Some(module.prefix)`), the resolver tries `Module.Name` *before*
1410/// falling back to module-walk ordering. Two modules with same-bare
1411/// refined records (`A.Natural` + `B.Natural`, distinct predicates)
1412/// then resolve to the *current scope*'s entry — bare references
1413/// inside module B's emit pick up `B.Natural`, not whichever module
1414/// happened to populate first.
1415///
1416/// Resolution order (revised after review round 3):
1417///   1. If `scope = Some(prefix)` AND `name` is bare, try
1418///      `prefix.bare` FIRST — without this an entry-level
1419///      refined record with the same bare name would shadow the
1420///      module's own slot from inside that module's emit pass.
1421///   2. Direct lookup of `name` as written (covers exact-canonical
1422///      hits + bare-entry from entry-scope callers).
1423///   3. Walk modules to find owners and try each canonical key.
1424pub fn find_refined_type_scoped<'a>(
1425    ctx: &'a CodegenContext,
1426    name: &str,
1427    scope: Option<&str>,
1428) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1429    find_refined_type_with_key_scoped(ctx, name, scope).map(|(_, d)| d)
1430}
1431
1432/// Round-4 central canonical resolver. Both `find_refined_type` and
1433/// `find_refined_type_scoped` reduce to this. Returns the exact
1434/// `(canonical_name, &decl)` pair stored in `ProofIR.refined_types`
1435/// so downstream code can re-key safely (no string heuristics). The
1436/// canonical name is the string form of the `TypeKey` the IR resolved
1437/// the decl from (e.g. `AAA.Natural`) — even though the map itself is
1438/// keyed by opaque `TypeId` after phase E2, consumers expect a
1439/// human-readable identifier here for diagnostics and `defmt`-style
1440/// canonical comparison.
1441///
1442/// `ctx.symbol_table` is always populated after phase E (pipeline
1443/// builds it unconditionally); the lookup returns `None` only when
1444/// the requested type isn't registered (built-ins / non-existent
1445/// names) or when the type has no `RefinedTypeDecl` slot.
1446pub fn find_refined_type_with_key_scoped<'a>(
1447    ctx: &'a CodegenContext,
1448    name: &str,
1449    scope: Option<&str>,
1450) -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1451    let symbols = &ctx.symbol_table;
1452    let bare = name.rsplit('.').next().unwrap_or(name);
1453    let name_is_already_qualified = name.contains('.');
1454    let try_key =
1455        |key: crate::ir::TypeKey| -> Option<(String, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1456            let id = symbols.type_id_of(&key)?;
1457            let decl = ctx.proof_ir.refined_types.get(&id)?;
1458            Some((key.canonical(), decl))
1459        };
1460    if let Some(prefix) = scope
1461        && !name_is_already_qualified
1462        && let Some(hit) = try_key(crate::ir::TypeKey::in_module(prefix.to_string(), bare))
1463    {
1464        return Some(hit);
1465    }
1466    let direct_key =
1467        if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1468            crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
1469        } else {
1470            crate::ir::TypeKey::entry(name)
1471        };
1472    if let Some(hit) = try_key(direct_key) {
1473        return Some(hit);
1474    }
1475    for m in &ctx.modules {
1476        for td in &m.type_defs {
1477            if type_def_name(td) == bare
1478                && let Some(hit) = try_key(crate::ir::TypeKey::in_module(m.prefix.clone(), bare))
1479            {
1480                return Some(hit);
1481            }
1482        }
1483    }
1484    None
1485}
1486
1487/// Same resolution shape as [`find_refined_type`] but driven by the
1488/// in-flight `refined_types` map plus dep-module list — used inside
1489/// `proof_lower` where there is no `CodegenContext` yet. Resolves
1490/// `name` → `TypeKey` → opaque `TypeId` through the supplied symbol
1491/// table, then looks up the decl by id.
1492pub fn resolve_refined_type_in<'a>(
1493    refined_types: &'a std::collections::HashMap<
1494        crate::ir::TypeId,
1495        crate::ir::proof_ir::RefinedTypeDecl,
1496    >,
1497    symbols: &crate::ir::SymbolTable,
1498    modules: &[crate::codegen::ModuleInfo],
1499    name: &str,
1500) -> Option<&'a crate::ir::proof_ir::RefinedTypeDecl> {
1501    resolve_refined_type_in_with_key(refined_types, symbols, modules, name).map(|(_, d)| d)
1502}
1503
1504/// Same as [`resolve_refined_type_in`] but returns the canonical
1505/// `TypeId` paired with the decl — used by IR-internal callers
1506/// (the proof-lower side `walk_for_refinement_carrier`) so they
1507/// thread the opaque identity through downstream comparisons
1508/// without re-introducing bare-name heuristics.
1509pub fn resolve_refined_type_in_with_key<'a>(
1510    refined_types: &'a std::collections::HashMap<
1511        crate::ir::TypeId,
1512        crate::ir::proof_ir::RefinedTypeDecl,
1513    >,
1514    symbols: &crate::ir::SymbolTable,
1515    modules: &[crate::codegen::ModuleInfo],
1516    name: &str,
1517) -> Option<(crate::ir::TypeId, &'a crate::ir::proof_ir::RefinedTypeDecl)> {
1518    let bare = name.rsplit('.').next().unwrap_or(name);
1519    let name_is_already_qualified = name.contains('.');
1520    let direct_key =
1521        if name_is_already_qualified && let Some((prefix, bare_part)) = name.rsplit_once('.') {
1522            crate::ir::TypeKey::in_module(prefix.to_string(), bare_part)
1523        } else {
1524            crate::ir::TypeKey::entry(name)
1525        };
1526    if let Some(id) = symbols.type_id_of(&direct_key)
1527        && let Some(decl) = refined_types.get(&id)
1528    {
1529        return Some((id, decl));
1530    }
1531    for m in modules {
1532        for td in &m.type_defs {
1533            if type_def_name(td) == bare {
1534                let canonical = crate::ir::TypeKey::in_module(m.prefix.clone(), bare);
1535                if let Some(id) = symbols.type_id_of(&canonical)
1536                    && let Some(decl) = refined_types.get(&id)
1537                {
1538                    return Some((id, decl));
1539                }
1540            }
1541        }
1542    }
1543    None
1544}
1545
1546/// A `verify fn law … given a: T = [v]` with one value per given binds
1547/// the law to a single concrete point. Combined with
1548/// [`law_rhs_is_independent_of_givens`] this is the "sample-only"
1549/// shape — the universal form is vacuous (RHS is a constant,
1550/// LHS-with-one-input doesn't span anything). Asociative-style laws
1551/// also have singleton givens but their RHS *uses* the bound names
1552/// (`add a (add b c)`), so the universal form there is genuinely
1553/// the asociativity statement and stays.
1554pub fn all_givens_are_singletons(law: &crate::ast::VerifyLaw) -> bool {
1555    !law.givens.is_empty()
1556        && law.givens.iter().all(|g| match &g.domain {
1557            VerifyGivenDomain::Explicit(values) => values.len() == 1,
1558            VerifyGivenDomain::IntRange { start, end } => start == end,
1559        })
1560}
1561
1562/// Issue #128: extract fn names from `proof_ir.unclassified_fns`
1563/// messages. The diagnostic prose is the source of truth (the
1564/// `UnclassifiedFn` struct doesn't carry a separate `name` field
1565/// today); each message starts `recursive function 'NAME' is outside
1566/// proof subset (...)`. Extract the quoted name so the law gate has
1567/// a `HashSet<String>` to test fn-call expressions against.
1568pub fn unclassified_fn_names(ctx: &CodegenContext) -> HashSet<String> {
1569    ctx.proof_ir
1570        .unclassified_fns
1571        .iter()
1572        .filter_map(|uf| {
1573            let s = &uf.message;
1574            let start = s.find('\'')?;
1575            let rest = &s[start + 1..];
1576            let end = rest.find('\'')?;
1577            Some(rest[..end].to_string())
1578        })
1579        .collect()
1580}
1581
1582/// Issue #128: does the law call any fn the proof-mode classifier
1583/// rejected as "outside proof subset"?
1584///
1585/// `size`, `toSorted`, `blackDepth` and friends compile to
1586/// fuel-bounded helpers (`size__fuel` / `toSorted__fuel`) that the
1587/// `induction` tactic can't drive — even when the universal claim is
1588/// mathematically true (`∀ t, size t = (toSorted t).length`). The
1589/// auto-proof matcher emits an `induction t with …` chain that
1590/// leaves the goal under fuel, lake fails. Skip the universal on
1591/// this shape; the expanded `_sample_N` lemmas remain decidable
1592/// (concrete inputs unfold fuel finitely).
1593pub fn law_calls_unclassified_fn(
1594    law: &crate::ast::VerifyLaw,
1595    unclassified: &HashSet<String>,
1596) -> bool {
1597    if unclassified.is_empty() {
1598        return false;
1599    }
1600    expr_calls_named(&law.lhs, unclassified) || expr_calls_named(&law.rhs, unclassified)
1601}
1602
1603fn expr_calls_named(expr: &Spanned<Expr>, names: &HashSet<String>) -> bool {
1604    match &expr.node {
1605        Expr::FnCall(callee, args) => {
1606            // Resolve the callee through the same path other emitters use —
1607            // covers bare (`size`), dotted (`Dep.toSorted`), and resolved
1608            // module-qualified shapes. The unclassified set is populated
1609            // from the recursion classifier's diagnostics, which today
1610            // emit the local `fd.name` (bare) regardless of which module
1611            // the fn lives in (see `recursion::detect`'s issue messages).
1612            // Match BOTH the resolved canonical name and its bare suffix
1613            // so `Dep.toSorted(xs)` lands on the gate when the set holds
1614            // just `toSorted` — and so a future migration to qualified
1615            // diagnostic names keeps working.
1616            let direct = expr_to_dotted_name(&callee.node)
1617                .map(|n| {
1618                    let bare = n.rsplit('.').next().unwrap_or(n.as_str());
1619                    names.contains(n.as_str()) || names.contains(bare)
1620                })
1621                .unwrap_or(false);
1622            direct
1623                || expr_calls_named(callee, names)
1624                || args.iter().any(|a| expr_calls_named(a, names))
1625        }
1626        Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
1627            expr_calls_named(inner, names)
1628        }
1629        Expr::BinOp(_, l, r) => expr_calls_named(l, names) || expr_calls_named(r, names),
1630        Expr::Match { subject, arms } => {
1631            expr_calls_named(subject, names)
1632                || arms.iter().any(|a| expr_calls_named(&a.body, names))
1633        }
1634        Expr::Constructor(_, Some(arg)) => expr_calls_named(arg, names),
1635        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1636            items.iter().any(|i| expr_calls_named(i, names))
1637        }
1638        Expr::MapLiteral(entries) => entries
1639            .iter()
1640            .any(|(k, v)| expr_calls_named(k, names) || expr_calls_named(v, names)),
1641        Expr::RecordCreate { fields, .. } => fields.iter().any(|(_, v)| expr_calls_named(v, names)),
1642        Expr::RecordUpdate { base, updates, .. } => {
1643            expr_calls_named(base, names) || updates.iter().any(|(_, v)| expr_calls_named(v, names))
1644        }
1645        Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1646            crate::ast::StrPart::Parsed(inner) => expr_calls_named(inner, names),
1647            crate::ast::StrPart::Literal(_) => false,
1648        }),
1649        Expr::TailCall(boxed) => {
1650            let crate::ast::TailCallData { target, args, .. } = boxed.as_ref();
1651            names.contains(target) || args.iter().any(|a| expr_calls_named(a, names))
1652        }
1653        _ => false,
1654    }
1655}
1656
1657/// The pure recursive fns that THREAD an accumulator over a USER-ADT driver —
1658/// a param fed a RECONSTRUCTED expression in every self-call
1659/// (`param_threaded_in_recursion`) while NO param is a structural `List`
1660/// (`single_list_structural_param_index` is `None`), the `triTR` / `qfac` /
1661/// `qexp` shape. These are exactly the fns the recursion classifier learned to
1662/// accept as structural `def`s (`single_adt_structural_param_index`): before, a
1663/// growing accumulator left them unclassified, so [`law_calls_unclassified_fn`]
1664/// bounded every law that referenced them; now they are classified and that gate
1665/// no longer fires. A law verified ON such a fn closes by `induction …
1666/// generalizing acc`, but a law that merely REFERENCES one (`fac(x) => qfac(x,
1667/// 1)` verified on `fac`) still can't close by simple induction — it needs the
1668/// inner fn's accumulator-decomposition lemma. The list-accumulator folds
1669/// (`qrev`) are DELIBERATELY excluded: they were always `ListStructural`
1670/// (classified), the fuel gate never bounded laws over them, and a consumer law
1671/// `myRev(x) => Lib.qrev(x, [])` genuinely closes by citing the dep's proven
1672/// `qrev` law — re-gating those would regress the cross-file law pool.
1673pub fn accumulator_fold_fn_names(ctx: &CodegenContext) -> HashSet<String> {
1674    use crate::codegen::recursion::detect::{
1675        param_decremented_in_recursion, param_threaded_in_recursion,
1676        single_list_structural_param_index,
1677    };
1678    // Exactly the shape the new classifier arm mints
1679    // (`single_adt_structural_param_index`): a recursive USER-ADT driver that
1680    // strictly shrinks every call, a threaded sibling accumulator, and NO
1681    // structural `List` param. The recursive-ADT-driver requirement is what keeps
1682    // this OFF fns an EARLIER classifier arm already handled — an Int-countdown /
1683    // floor-division / ascending fold also "threads" a non-bare-local arg
1684    // (`acc + n`, `n - 1`), was always classified and never fuel-bounded, and its
1685    // laws must keep proving universally.
1686    let threads_over_recursive_adt = |fd: &FnDef| -> bool {
1687        single_list_structural_param_index(fd).is_none()
1688            && (0..fd.params.len()).any(|i| param_threaded_in_recursion(fd, i))
1689            && fd.params.iter().enumerate().any(|(i, (_, ty))| {
1690                ctx_type_is_recursive_sum(ctx, ty.trim()) && param_decremented_in_recursion(fd, i)
1691            })
1692    };
1693    ctx.fn_defs
1694        .iter()
1695        .chain(ctx.modules.iter().flat_map(|m| m.fn_defs.iter()))
1696        .filter(|fd| threads_over_recursive_adt(fd))
1697        .map(|fd| fd.name.clone())
1698        .collect()
1699}
1700
1701/// `true` iff `type_name` names a directly-recursive user sum type in `ctx`
1702/// (some variant field carries the type itself, bare or inside a generic) —
1703/// the `Nat` of a Peano accumulator fold. Mirrors `is_recursive_sum` /
1704/// `variants_fields_contain_type` in the Lean induction codegen.
1705fn ctx_type_is_recursive_sum(ctx: &CodegenContext, type_name: &str) -> bool {
1706    use crate::ast::TypeDef;
1707    ctx.type_defs
1708        .iter()
1709        .chain(ctx.modules.iter().flat_map(|m| m.type_defs.iter()))
1710        .any(|td| match td {
1711            TypeDef::Sum { name, variants, .. } if name == type_name => variants.iter().any(|v| {
1712                v.fields.iter().any(|f| {
1713                    let f = f.trim();
1714                    f == type_name
1715                        || f.contains(&format!("<{type_name}"))
1716                        || f.contains(&format!("{type_name}>"))
1717                        || f.contains(&format!(", {type_name}"))
1718                        || f.contains(&format!("{type_name},"))
1719                })
1720            }),
1721            _ => false,
1722        })
1723}
1724
1725/// The user fn an accumulator fold COMBINES with — the head of the threaded
1726/// accumulator argument in its single self-call (`plus` of `triTR(m, plus(n,
1727/// acc))`, `mul` of `factTR(m, mul(n, acc))`). `None` for a non-fold or a
1728/// non-`FnCall` accumulator step (a builtin `+`/`*` step lowers to a `BinOp`,
1729/// not a user fn, and the List corner needs no algebraic helpers).
1730fn accfold_combine_fn(fd: &FnDef) -> Option<String> {
1731    use crate::ast::Expr;
1732    use crate::codegen::recursion::detect::{
1733        call_matches, collect_calls_from_body, param_threaded_in_recursion,
1734    };
1735    let acc_idx = (0..fd.params.len()).find(|&i| param_threaded_in_recursion(fd, i))?;
1736    let calls: Vec<Vec<&Spanned<Expr>>> = collect_calls_from_body(fd.body.as_ref())
1737        .into_iter()
1738        .filter(|(name, _)| call_matches(name, &fd.name))
1739        .map(|(_, args)| args)
1740        .collect();
1741    let acc_arg = calls.first()?.get(acc_idx).copied()?;
1742    let Expr::FnCall(callee, _) = &acc_arg.node else {
1743        return None;
1744    };
1745    expr_to_dotted_name(&callee.node)
1746}
1747
1748/// `e` is `fn(arg0, arg1, …)` with each arg the bare ident named in `arg_names`.
1749fn call_args_are_idents(e: &Spanned<Expr>, fn_name: &str, arg_names: &[&str]) -> bool {
1750    let Expr::FnCall(callee, args) = &e.node else {
1751        return false;
1752    };
1753    expr_to_dotted_name(&callee.node).as_deref() == Some(fn_name)
1754        && args.len() == arg_names.len()
1755        && args.iter().zip(arg_names).all(
1756            |(a, n)| matches!(&a.node, Expr::Ident(x) | Expr::Resolved { name: x, .. } if x == n),
1757        )
1758}
1759
1760/// `true` iff `vb` is a commutativity law for `combine`: `combine(a, b) =>
1761/// combine(b, a)` over two givens, no premise.
1762fn law_is_commutativity(vb: &VerifyBlock, combine: &str) -> bool {
1763    let crate::ast::VerifyKind::Law(law) = &vb.kind else {
1764        return false;
1765    };
1766    if law.givens.len() != 2 || law.when.is_some() {
1767        return false;
1768    }
1769    let a = law.givens[0].name.as_str();
1770    let b = law.givens[1].name.as_str();
1771    (call_args_are_idents(&law.lhs, combine, &[a, b])
1772        && call_args_are_idents(&law.rhs, combine, &[b, a]))
1773        || (call_args_are_idents(&law.lhs, combine, &[b, a])
1774            && call_args_are_idents(&law.rhs, combine, &[a, b]))
1775}
1776
1777/// `true` iff `vb` is an associativity law for `combine`: `combine(combine(a,
1778/// b), c) => combine(a, combine(b, c))` over three givens, no premise (either
1779/// nesting may be on the lhs).
1780fn law_is_associativity(vb: &VerifyBlock, combine: &str) -> bool {
1781    let crate::ast::VerifyKind::Law(law) = &vb.kind else {
1782        return false;
1783    };
1784    if law.givens.len() != 3 || law.when.is_some() {
1785        return false;
1786    }
1787    let a = law.givens[0].name.as_str();
1788    let b = law.givens[1].name.as_str();
1789    let c = law.givens[2].name.as_str();
1790    // `combine(combine(a, b), c)` — outer call, inner first arg.
1791    let nested = |e: &Spanned<Expr>| -> bool {
1792        let Expr::FnCall(callee, args) = &e.node else {
1793            return false;
1794        };
1795        expr_to_dotted_name(&callee.node).as_deref() == Some(combine)
1796            && args.len() == 2
1797            && call_args_are_idents(&args[0], combine, &[a, b])
1798            && matches!(&args[1].node, Expr::Ident(x) | Expr::Resolved { name: x, .. } if x == c)
1799    };
1800    // `combine(a, combine(b, c))` — outer call, inner second arg.
1801    let flat = |e: &Spanned<Expr>| -> bool {
1802        let Expr::FnCall(callee, args) = &e.node else {
1803            return false;
1804        };
1805        expr_to_dotted_name(&callee.node).as_deref() == Some(combine)
1806            && args.len() == 2
1807            && matches!(&args[0].node, Expr::Ident(x) | Expr::Resolved { name: x, .. } if x == a)
1808            && call_args_are_idents(&args[1], combine, &[b, c])
1809    };
1810    (nested(&law.lhs) && flat(&law.rhs)) || (nested(&law.rhs) && flat(&law.lhs))
1811}
1812
1813/// `true` iff a Nat accumulator-generalizing law verified ON `verified_fn` can
1814/// CLOSE its universal on Dafny. Unlike Lean (which bridges the user monoid fn
1815/// to builtin `Nat` arithmetic that Z3 knows is associative/commutative), Dafny
1816/// sees the user `plus` / `mul` over the ADT as opaque, so the datatype-induction
1817/// proof only discharges when the file ALSO provides commutativity AND
1818/// associativity laws for the fold's combine fn — which the generic driver
1819/// proves and cites. Absent those helpers the universal would ERROR (Dafny
1820/// cannot sorry), so it must stay sample-only. The List corner never reaches
1821/// here (its accumulator combine is a builtin `BinOp`, so `accfold_combine_fn`
1822/// is `None`, and list folds are excluded from `accumulator_fold_fn_names`).
1823pub fn nat_accfold_self_closeable(
1824    ctx: &CodegenContext,
1825    verified_fn: &str,
1826    accgen_law_name: &str,
1827) -> bool {
1828    if !accumulator_fold_fn_names(ctx).contains(verified_fn) {
1829        return false;
1830    }
1831    let Some(fd) = ctx.fn_def_by_name(verified_fn, ctx.active_module_scope().as_deref()) else {
1832        return false;
1833    };
1834    let Some(combine) = accfold_combine_fn(fd) else {
1835        return false;
1836    };
1837    // Restrict to an ADDITIVE monoid combine (`plus`: base arm returns the 2nd
1838    // param). Its commutativity / associativity prove generically by structural
1839    // induction, so comm+assoc helpers are SUFFICIENT to close the accGen. A
1840    // multiplicative combine (`mul`: base arm returns the zero constructor) also
1841    // needs distributivity — comm+assoc alone do not even prove there — so it
1842    // stays sample-only rather than ungating into a Z3 error.
1843    if !ctx
1844        .fn_def_by_name(&combine, ctx.active_module_scope().as_deref())
1845        .is_some_and(combine_is_additive_monoid)
1846    {
1847        return false;
1848    }
1849    // Only count comm/assoc helpers the citation engine will actually hoist into
1850    // the proof: laws EARLIER in source than the accGen block in THIS module
1851    // (`eligible_cites` is earlier-in-source, same-module). A helper declared
1852    // after the accGen — or in a dependency — is present but un-citable, so
1853    // ungating on it would emit a body missing the algebra it needs and Dafny
1854    // would error instead of cleanly omitting.
1855    let citable: Vec<&VerifyBlock> = ctx
1856        .items
1857        .iter()
1858        .filter_map(|i| match i {
1859            TopLevel::Verify(vb) => Some(vb),
1860            _ => None,
1861        })
1862        .take_while(|vb| {
1863            !matches!(&vb.kind, VerifyKind::Law(l)
1864                if vb.fn_name == verified_fn && l.name == accgen_law_name)
1865        })
1866        .collect();
1867    let has_comm = citable.iter().any(|vb| law_is_commutativity(vb, &combine));
1868    let has_assoc = citable.iter().any(|vb| law_is_associativity(vb, &combine));
1869    has_comm && has_assoc
1870}
1871
1872/// `true` iff `fd` is a 2-param ADDITIVE-monoid fn — some base (nullary-
1873/// constructor) match arm returns the second parameter unchanged (`plus`'s
1874/// `Z -> y`). A multiplicative monoid's base arm returns the zero constructor
1875/// (`mul`'s `Z -> Z`) instead, so it fails this test.
1876fn combine_is_additive_monoid(fd: &FnDef) -> bool {
1877    use crate::ast::{Expr, Pattern, Stmt};
1878    if fd.params.len() != 2 {
1879        return false;
1880    }
1881    let second = fd.params[1].0.as_str();
1882    let [Stmt::Expr(body)] = fd.body.stmts() else {
1883        return false;
1884    };
1885    let Expr::Match { arms, .. } = &body.node else {
1886        return false;
1887    };
1888    arms.iter().any(|arm| {
1889        matches!(&arm.pattern, Pattern::Constructor(_, binders) if binders.is_empty())
1890            && matches!(&arm.body.node, Expr::Ident(n) | Expr::Resolved { name: n, .. } if n == second)
1891    })
1892}
1893
1894/// `true` iff `law`'s lhs/rhs calls a recursive accumulator-threading fn OTHER
1895/// than `verified_fn` — the foreign-fold hazard (see
1896/// [`accumulator_fold_fn_names`]). Such a law can't close by simple induction
1897/// (it needs the inner fn's accumulator-decomposition lemma) and must stay
1898/// bounded, exactly as it did before the recursion classifier learned the
1899/// threaded-accumulator shape. The verified fn is excluded so an accumulator-
1900/// generalizing law verified ON the fold (`triTR(n, acc) => plus(triSpec(n),
1901/// acc)`) is NOT gated — Lean closes it by `induction … generalizing acc`.
1902/// Lean uses THIS (it has the generalizing emit); Dafny uses
1903/// [`law_calls_any_accumulator_fold`] because it has no such emit.
1904pub fn law_calls_foreign_accumulator_fold(
1905    ctx: &CodegenContext,
1906    law: &crate::ast::VerifyLaw,
1907    verified_fn: &str,
1908) -> bool {
1909    let foreign: HashSet<String> = accumulator_fold_fn_names(ctx)
1910        .into_iter()
1911        .filter(|n| n != verified_fn)
1912        .collect();
1913    law_calls_unclassified_fn(law, &foreign)
1914}
1915
1916/// Dafny's bound-vs-attempt decision for a law touching an accumulator fold,
1917/// now that the Dafny backend HAS a datatype-induction generalizing emit. Bound
1918/// (sample-only) when:
1919///   * the law references a FOREIGN fold (needs that fold's own decomposition
1920///     lemma, never available — the `fac(x) => qfac(x, 1)` shape), OR
1921///   * the law is verified ON a Nat fold whose universal cannot close here
1922///     (`nat_accfold_self_closeable` is false — no commutativity / associativity
1923///     helper laws for the combine fn, which Z3 needs over the opaque ADT).
1924///
1925/// A self-fold WITH those helpers is NOT bound: the datatype-induction hint plus
1926/// the cited algebra discharge it as a real universal. The List corner is never
1927/// in `accumulator_fold_fn_names`, so its accumulator law always attempts (and
1928/// closes — builtin `Int` arithmetic needs no helper).
1929pub fn dafny_should_bound_accumulator_fold(
1930    ctx: &CodegenContext,
1931    law: &crate::ast::VerifyLaw,
1932    verified_fn: &str,
1933) -> bool {
1934    law_calls_foreign_accumulator_fold(ctx, law, verified_fn)
1935        || (accumulator_fold_fn_names(ctx).contains(verified_fn)
1936            && !nat_accfold_self_closeable(ctx, verified_fn, &law.name))
1937}
1938
1939/// Issue #128: is the law's RHS independent of every given identifier?
1940///
1941/// `checkRight L V R => Tree.Black Empty 1 Empty` — RHS mentions no
1942/// given. The `∀ L V R, checkRight L V R = Tree.Black Empty 1 Empty`
1943/// universal is then either trivially false (`checkRight` is not a
1944/// constant) or vacuous; either way the per-sample lemma is the only
1945/// meaningful claim.
1946/// `add a (add b c) => add (add a b) c` — RHS uses `a`, `b`, `c`,
1947/// so the universal is the real asociativity theorem and is kept.
1948pub fn law_rhs_is_independent_of_givens(law: &crate::ast::VerifyLaw) -> bool {
1949    let given_names: HashSet<&str> = law.givens.iter().map(|g| g.name.as_str()).collect();
1950    if given_names.is_empty() {
1951        return true;
1952    }
1953    !expr_references_any_ident(&law.rhs, &given_names)
1954}
1955
1956fn expr_references_any_ident(expr: &Spanned<Expr>, names: &HashSet<&str>) -> bool {
1957    match &expr.node {
1958        Expr::Ident(name) | Expr::Resolved { name, .. } => names.contains(name.as_str()),
1959        Expr::Attr(inner, _) | Expr::ErrorProp(inner) | Expr::Neg(inner) => {
1960            expr_references_any_ident(inner, names)
1961        }
1962        Expr::FnCall(callee, args) => {
1963            expr_references_any_ident(callee, names)
1964                || args.iter().any(|a| expr_references_any_ident(a, names))
1965        }
1966        Expr::BinOp(_, l, r) => {
1967            expr_references_any_ident(l, names) || expr_references_any_ident(r, names)
1968        }
1969        Expr::Match { subject, arms } => {
1970            expr_references_any_ident(subject, names)
1971                || arms
1972                    .iter()
1973                    .any(|a| expr_references_any_ident(&a.body, names))
1974        }
1975        Expr::Constructor(_, Some(arg)) => expr_references_any_ident(arg, names),
1976        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
1977            items.iter().any(|i| expr_references_any_ident(i, names))
1978        }
1979        Expr::MapLiteral(entries) => entries.iter().any(|(k, v)| {
1980            expr_references_any_ident(k, names) || expr_references_any_ident(v, names)
1981        }),
1982        Expr::RecordCreate { fields, .. } => fields
1983            .iter()
1984            .any(|(_, v)| expr_references_any_ident(v, names)),
1985        Expr::RecordUpdate { base, updates, .. } => {
1986            expr_references_any_ident(base, names)
1987                || updates
1988                    .iter()
1989                    .any(|(_, v)| expr_references_any_ident(v, names))
1990        }
1991        Expr::InterpolatedStr(parts) => parts.iter().any(|p| match p {
1992            crate::ast::StrPart::Parsed(inner) => expr_references_any_ident(inner, names),
1993            crate::ast::StrPart::Literal(_) => false,
1994        }),
1995        Expr::TailCall(boxed) => {
1996            let crate::ast::TailCallData { args, .. } = boxed.as_ref();
1997            args.iter().any(|a| expr_references_any_ident(a, names))
1998        }
1999        _ => false,
2000    }
2001}
2002
2003/// Oracle v1: does the LHS of a verify-law project through Oracle's
2004/// runtime trace buffer?
2005///
2006/// Trace-buffer projections (`fn().trace.event(k)`,
2007/// `.trace.group(N).branch(M).event(K)`, `.trace.length()`,
2008/// `.trace.contains(...)`) are observable only at runtime — the lifted
2009/// proof-side fn returns the bare value, with no `.trace` field.
2010///
2011/// The detection requires *both* signals:
2012///   (a) an `Attr(_, "trace")` somewhere in the expression — the
2013///       projection lands on the trace buffer, and
2014///   (b) a method call from Oracle's trace API (`.event`, `.group`,
2015///       `.branch`, `.length`, `.contains`) on the result of (a).
2016///
2017/// Requiring both rules out a false positive where a user-defined
2018/// record happens to have a `trace` field (`record Log { trace:
2019/// String, ... }`) — `log.trace` matches (a) but not (b), so the
2020/// universal proof stays in normal emit. The cases-form trace block
2021/// emitter (`emit_verify_trace_block_proofs`) already commented out
2022/// the same shape; this is the law-form mirror.
2023pub fn law_lhs_has_trace_projection(expr: &Spanned<Expr>) -> bool {
2024    expr_has_trace_field(expr) && expr_has_trace_api_call(expr)
2025}
2026
2027fn expr_has_trace_field(expr: &Spanned<Expr>) -> bool {
2028    match &expr.node {
2029        Expr::Attr(inner, field) => field == "trace" || expr_has_trace_field(inner),
2030        Expr::FnCall(callee, args) => {
2031            expr_has_trace_field(callee) || args.iter().any(expr_has_trace_field)
2032        }
2033        Expr::BinOp(_, l, r) => expr_has_trace_field(l) || expr_has_trace_field(r),
2034        Expr::Match { subject, arms } => {
2035            expr_has_trace_field(subject) || arms.iter().any(|a| expr_has_trace_field(&a.body))
2036        }
2037        Expr::ErrorProp(inner) => expr_has_trace_field(inner),
2038        Expr::Constructor(_, Some(arg)) => expr_has_trace_field(arg),
2039        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
2040            items.iter().any(expr_has_trace_field)
2041        }
2042        _ => false,
2043    }
2044}
2045
2046/// Oracle's trace-buffer API surface. Every method that
2047/// `infer_trace_method` in the typechecker recognises as a
2048/// trace-only call. Keep in sync with
2049/// `src/types/checker/infer/expr.rs::infer_trace_method`.
2050const TRACE_API_METHODS: &[&str] = &["event", "group", "branch", "length", "contains", "count"];
2051
2052fn expr_has_trace_api_call(expr: &Spanned<Expr>) -> bool {
2053    match &expr.node {
2054        Expr::FnCall(callee, args) => {
2055            let direct = matches!(
2056                &callee.node,
2057                Expr::Attr(_, method) if TRACE_API_METHODS.contains(&method.as_str())
2058            );
2059            direct || expr_has_trace_api_call(callee) || args.iter().any(expr_has_trace_api_call)
2060        }
2061        Expr::Attr(inner, _) | Expr::ErrorProp(inner) => expr_has_trace_api_call(inner),
2062        Expr::BinOp(_, l, r) => expr_has_trace_api_call(l) || expr_has_trace_api_call(r),
2063        Expr::Match { subject, arms } => {
2064            expr_has_trace_api_call(subject)
2065                || arms.iter().any(|a| expr_has_trace_api_call(&a.body))
2066        }
2067        Expr::Constructor(_, Some(arg)) => expr_has_trace_api_call(arg),
2068        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
2069            items.iter().any(expr_has_trace_api_call)
2070        }
2071        _ => false,
2072    }
2073}
2074
2075/// True when the type definition mentions its own name somewhere in a
2076/// field or variant payload (recursive ADT).
2077pub fn is_recursive_type_def(td: &TypeDef) -> bool {
2078    match td {
2079        TypeDef::Sum { name, variants, .. } => is_recursive_sum(name, variants),
2080        TypeDef::Product { name, fields, .. } => is_recursive_product(name, fields),
2081    }
2082}
2083
2084/// The declared name of a type definition.
2085pub fn type_def_name(td: &TypeDef) -> &str {
2086    match td {
2087        TypeDef::Sum { name, .. } | TypeDef::Product { name, .. } => name,
2088    }
2089}
2090
2091/// Granular variant of [`is_recursive_type_def`] taking a sum's
2092/// `(name, variants)` split — some backends already have the parts
2093/// separated and don't want to rebuild a `TypeDef` just to query.
2094pub fn is_recursive_sum(name: &str, variants: &[TypeVariant]) -> bool {
2095    variants
2096        .iter()
2097        .any(|v| v.fields.iter().any(|f| type_ref_contains(f, name)))
2098}
2099
2100/// Granular variant of [`is_recursive_type_def`] for products.
2101pub fn is_recursive_product(name: &str, fields: &[(String, String)]) -> bool {
2102    fields.iter().any(|(_, ty)| type_ref_contains(ty, name))
2103}
2104
2105fn type_ref_contains(annotation: &str, type_name: &str) -> bool {
2106    // Direct match or any generic position: List<Foo>, Option<Foo>,
2107    // Map<K, Foo>, (Foo, Bar), etc.
2108    annotation == type_name
2109        || annotation.contains(&format!("<{}", type_name))
2110        || annotation.contains(&format!("{}>", type_name))
2111        || annotation.contains(&format!(", {}", type_name))
2112        || annotation.contains(&format!("{},", type_name))
2113}
2114
2115/// Check if a name is a user-defined type (sum or product), including modules.
2116pub(crate) fn is_user_type(name: &str, ctx: &CodegenContext) -> bool {
2117    let check_td = |td: &TypeDef| match td {
2118        TypeDef::Sum { name: n, .. } => n == name,
2119        TypeDef::Product { name: n, .. } => n == name,
2120    };
2121    ctx.type_defs.iter().any(check_td)
2122        || ctx.modules.iter().any(|m| m.type_defs.iter().any(check_td))
2123}
2124
2125/// Resolve a module-qualified dotted name to `(module_prefix, local_suffix)`.
2126/// Example: `Models.User.nameById` -> `("Models.User", "nameById")`.
2127pub(crate) fn resolve_module_call<'a>(
2128    dotted_name: &'a str,
2129    ctx: &'a CodegenContext,
2130) -> Option<(&'a str, &'a str)> {
2131    let mut best: Option<&str> = None;
2132    for prefix in &ctx.module_prefixes {
2133        let dotted_prefix = format!("{}.", prefix);
2134        if dotted_name.starts_with(&dotted_prefix) && best.is_none_or(|b| prefix.len() > b.len()) {
2135            best = Some(prefix.as_str());
2136        }
2137    }
2138    best.map(|prefix| (prefix, &dotted_name[prefix.len() + 1..]))
2139}
2140
2141pub(crate) fn module_prefix_to_rust_segments(prefix: &str) -> Vec<String> {
2142    prefix.split('.').map(module_segment_to_rust).collect()
2143}
2144
2145/// Translate an Aver module prefix (`Models.User`, `Combat`) into a relative
2146/// filesystem path stem with `/` separators. Lean's path-as-module convention
2147/// and Dafny's `include "..."` paths both use this — same shape, no
2148/// backend-specific escaping.
2149pub(crate) fn module_prefix_to_filename(prefix: &str) -> String {
2150    prefix.replace('.', "/")
2151}
2152
2153/// Effects declared in fn signatures, preserving the distinction
2154/// between namespace-level and method-level declarations.
2155///
2156/// - `bare_namespaces`: e.g. `! [Console]` ⇒ permits every classified
2157///   `Console.*` method.
2158/// - `methods`: e.g. `! [Console.print]` ⇒ permits only that one
2159///   specific method (not the whole namespace).
2160///
2161/// Aver source allows both forms — we keep them separate so a single
2162/// `! [Random.int]` does not pull every `Random.*` method into the
2163/// trust header (or any other consumer that maps method-by-method).
2164pub(crate) struct DeclaredEffects {
2165    pub bare_namespaces: HashSet<String>,
2166    pub methods: HashSet<String>,
2167}
2168
2169impl DeclaredEffects {
2170    /// True if `c_method` (e.g. `"Random.int"`) is declared either as
2171    /// an explicit method or via its bare namespace (`"Random"`).
2172    pub fn includes(&self, c_method: &str) -> bool {
2173        if self.methods.contains(c_method) {
2174            return true;
2175        }
2176        if let Some((ns, _)) = c_method.split_once('.') {
2177            return self.bare_namespaces.contains(ns);
2178        }
2179        false
2180    }
2181}
2182
2183/// Collect declared effects across `ctx` (entry + dependent modules).
2184/// Single source of truth for the proof-side trust header and the
2185/// runtime-dependency detector in the Rust backend.
2186pub(crate) fn collect_declared_effects(ctx: &CodegenContext) -> DeclaredEffects {
2187    let mut bare_namespaces: HashSet<String> = HashSet::new();
2188    let mut methods: HashSet<String> = HashSet::new();
2189    let mut record = |effect: &str| {
2190        if effect.contains('.') {
2191            methods.insert(effect.to_string());
2192        } else {
2193            bare_namespaces.insert(effect.to_string());
2194        }
2195    };
2196    for item in &ctx.items {
2197        if let TopLevel::FnDef(fd) = item {
2198            for eff in &fd.effects {
2199                record(&eff.node);
2200            }
2201        }
2202    }
2203    for module in &ctx.modules {
2204        for fd in &module.fn_defs {
2205            for eff in &fd.effects {
2206                record(&eff.node);
2207            }
2208        }
2209    }
2210    DeclaredEffects {
2211        bare_namespaces,
2212        methods,
2213    }
2214}
2215
2216/// Basename for the entry file emitted by Lean / Dafny. Prefer the
2217/// source-declared module name (`module Foo` → `Foo`) so the entry
2218/// file's name matches what the user wrote; fall back to a capitalised
2219/// project name when no `module` declaration is present. Lake's
2220/// path-as-module-name convention forces this for Lean — Dafny doesn't
2221/// strictly need it but the same basename keeps the two backends
2222/// aligned (no more `playground.dfy` vs `OracleTrace.lean`).
2223pub fn entry_basename(ctx: &CodegenContext) -> String {
2224    ctx.items
2225        .iter()
2226        .find_map(|item| match item {
2227            TopLevel::Module(m) => Some(m.name.clone()),
2228            _ => None,
2229        })
2230        .unwrap_or_else(|| {
2231            let mut chars = ctx.project_name.chars();
2232            match chars.next() {
2233                None => String::new(),
2234                Some(c) => c.to_uppercase().chain(chars).collect(),
2235            }
2236        })
2237}
2238
2239/// Stable identity key for a verify block's case-index space, shared by the
2240/// Lean emitter's per-block case counters and the CLI's VM ground-truth
2241/// collection (`CodegenContext::sample_expected`).
2242///
2243/// Plain `verify <fn>` blocks share one index space per fn — the VM verify
2244/// runner merges them (`checker::merge_verify_blocks`) and the Lean emitter
2245/// continues the counter across blocks with the same key — while each
2246/// `verify <fn> law <name>` block keeps its own space. Both sides MUST derive
2247/// keys from this one function: index drift between them would associate a
2248/// case with another case's ground-truth value.
2249pub fn verify_block_counter_key(vb: &crate::ast::VerifyBlock) -> String {
2250    match &vb.kind {
2251        crate::ast::VerifyKind::Cases => format!("fn:{}", vb.fn_name),
2252        crate::ast::VerifyKind::Law(law) => format!("law:{}::{}", vb.fn_name, law.name),
2253    }
2254}
2255
2256// Round-6 finding 3: the legacy `fn_owning_scope(ctx) ->
2257// HashMap<String, String>` collided on bare names — two modules
2258// declaring the same-named fn (`AAA.foo` + `BBB.foo`) lost one
2259// scope to last-insert-wins. Replaced by [`fn_owning_scope_for`]
2260// (pointer-eq lookup) at the only remaining callsite. The legacy
2261// helper is removed; if a future caller needs a bulk pre-computed
2262// map, build one keyed by `(prefix, name)` or by the canonical
2263// `Module.fn` string rather than the bare `fd.name`.
2264
2265pub(crate) fn module_prefix_to_rust_path(prefix: &str) -> String {
2266    format!(
2267        "crate::aver_generated::{}",
2268        module_prefix_to_rust_segments(prefix).join("::")
2269    )
2270}
2271
2272fn module_segment_to_rust(segment: &str) -> String {
2273    let chars = segment.chars().collect::<Vec<_>>();
2274    let mut out = String::new();
2275
2276    for (idx, ch) in chars.iter().enumerate() {
2277        if ch.is_ascii_alphanumeric() {
2278            if ch.is_ascii_uppercase() {
2279                let prev_is_lower_or_digit = idx > 0
2280                    && (chars[idx - 1].is_ascii_lowercase() || chars[idx - 1].is_ascii_digit());
2281                let next_is_lower = chars
2282                    .get(idx + 1)
2283                    .is_some_and(|next| next.is_ascii_lowercase());
2284                if idx > 0 && (prev_is_lower_or_digit || next_is_lower) && !out.ends_with('_') {
2285                    out.push('_');
2286                }
2287                out.push(ch.to_ascii_lowercase());
2288            } else {
2289                out.push(ch.to_ascii_lowercase());
2290            }
2291        } else if !out.ends_with('_') {
2292            out.push('_');
2293        }
2294    }
2295
2296    let trimmed = out.trim_matches('_');
2297    let mut normalized = if trimmed.is_empty() {
2298        "module".to_string()
2299    } else {
2300        trimmed.to_string()
2301    };
2302
2303    if matches!(
2304        normalized.as_str(),
2305        "as" | "break"
2306            | "const"
2307            | "continue"
2308            | "crate"
2309            | "else"
2310            | "enum"
2311            | "extern"
2312            | "false"
2313            | "fn"
2314            | "for"
2315            | "if"
2316            | "impl"
2317            | "in"
2318            | "let"
2319            | "loop"
2320            | "match"
2321            | "mod"
2322            | "move"
2323            | "mut"
2324            | "pub"
2325            | "ref"
2326            | "return"
2327            | "self"
2328            | "Self"
2329            | "static"
2330            | "struct"
2331            | "super"
2332            | "trait"
2333            | "true"
2334            | "type"
2335            | "unsafe"
2336            | "use"
2337            | "where"
2338            | "while"
2339    ) {
2340        normalized.push_str("_mod");
2341    }
2342
2343    normalized
2344}
2345
2346/// Split a type annotation string at top-level delimiters (not inside `<>` or `()`).
2347///
2348/// Used by multiple backends to parse Aver type annotation strings like
2349/// `"Map<String, List<Int>>"` or `"(String, Int)"`.
2350pub(crate) fn split_type_params(s: &str, delim: char) -> Vec<String> {
2351    let mut parts = Vec::new();
2352    let mut depth = 0usize;
2353    let mut current = String::new();
2354    for ch in s.chars() {
2355        match ch {
2356            '<' | '(' => {
2357                depth += 1;
2358                current.push(ch);
2359            }
2360            '>' | ')' => {
2361                depth = depth.saturating_sub(1);
2362                current.push(ch);
2363            }
2364            _ if ch == delim && depth == 0 => {
2365                parts.push(current.trim().to_string());
2366                current.clear();
2367            }
2368            _ => current.push(ch),
2369        }
2370    }
2371    let rest = current.trim().to_string();
2372    if !rest.is_empty() {
2373        parts.push(rest);
2374    }
2375    parts
2376}
2377
2378/// Escape a string literal for target languages that use C-style escapes.
2379/// Handles `\\`, `\"`, `\n`, `\r`, `\t`, `\0`,
2380/// and generic control characters as `\xHH` (Lean/Rust) or `\uHHHH` (Dafny).
2381///
2382/// Use `unicode_escapes = true` for Dafny (which needs `\uHHHH`),
2383/// `false` for Lean/Rust (which accept `\xHH`).
2384pub(crate) fn escape_string_literal_ext(s: &str, unicode_escapes: bool) -> String {
2385    let mut out = String::with_capacity(s.len());
2386    for ch in s.chars() {
2387        match ch {
2388            '\\' => out.push_str("\\\\"),
2389            '"' => out.push_str("\\\""),
2390            '\n' => out.push_str("\\n"),
2391            '\r' => out.push_str("\\r"),
2392            '\t' => out.push_str("\\t"),
2393            '\0' => out.push_str("\\0"),
2394            c if c.is_control() => {
2395                if unicode_escapes {
2396                    // Dafny 4+ with Unicode chars enabled: \U{HHHHHH}
2397                    out.push_str(&format!("\\U{{{:06x}}}", c as u32));
2398                } else {
2399                    out.push_str(&format!("\\x{:02x}", c as u32));
2400                }
2401            }
2402            c => out.push(c),
2403        }
2404    }
2405    out
2406}
2407
2408/// Convenience: escape with `\xHH` for control chars (Lean, Rust).
2409pub(crate) fn escape_string_literal(s: &str) -> String {
2410    escape_string_literal_ext(s, false)
2411}
2412
2413/// Convenience: escape with `\u{HHHH}` for control chars (Dafny).
2414pub(crate) fn escape_string_literal_unicode(s: &str) -> String {
2415    escape_string_literal_ext(s, true)
2416}
2417
2418/// Parse an Aver type annotation string into the internal `Type` enum.
2419///
2420/// Thin wrapper around `types::parse_type_str` for use in codegen modules.
2421pub(crate) fn parse_type_annotation(ann: &str) -> Type {
2422    crate::types::parse_type_str(ann)
2423}
2424
2425/// Check if a `Type` represents a set pattern: `Map<T, Unit>`.
2426///
2427/// Aver has no dedicated `Set` type — the idiomatic way to express a set
2428/// is `Map<T, Unit>`. Codegen backends can lower this to the target
2429/// language's native set type (Dafny `set<T>`, Lean `Finset T`, etc.).
2430pub(crate) fn is_set_type(ty: &Type) -> bool {
2431    matches!(ty, Type::Map(_, v) if matches!(v.as_ref(), Type::Unit))
2432}
2433
2434/// Check if a type annotation string represents a set (`Map<T, Unit>`).
2435pub(crate) fn is_set_annotation(ann: &str) -> bool {
2436    is_set_type(&parse_type_annotation(ann))
2437}
2438
2439/// Resolved-form mirror of the historical AST helper. Same Phase-E shape
2440/// (`ResolvedExpr::Literal(Unit)`); used by migrated backends.
2441pub(crate) fn is_unit_expr_resolved(expr: &crate::ir::hir::ResolvedExpr) -> bool {
2442    matches!(
2443        expr,
2444        crate::ir::hir::ResolvedExpr::Literal(crate::ast::Literal::Unit)
2445    )
2446}
2447
2448/// Escape an Aver identifier if it collides with a target language reserved word.
2449///
2450/// `affix` is appended as a suffix (e.g. `"_"` for Dafny, `"'"` for Lean).
2451/// For prefix escaping (e.g. Rust `r#`), use [`escape_reserved_word_prefix`].
2452pub(crate) fn escape_reserved_word(name: &str, reserved: &[&str], suffix: &str) -> String {
2453    if reserved.contains(&name) {
2454        format!("{}{}", name, suffix)
2455    } else {
2456        name.to_string()
2457    }
2458}
2459
2460/// Like [`escape_reserved_word`] but prepends a prefix instead of appending a suffix.
2461/// Used for Rust's `r#keyword` raw identifier syntax.
2462pub(crate) fn escape_reserved_word_prefix(name: &str, reserved: &[&str], prefix: &str) -> String {
2463    if reserved.contains(&name) {
2464        format!("{}{}", prefix, name)
2465    } else {
2466        name.to_string()
2467    }
2468}
2469
2470/// Convert first character of a string to lowercase.
2471///
2472/// Used when converting PascalCase type/variant names to camelCase identifiers.
2473pub(crate) fn to_lower_first(s: &str) -> String {
2474    let mut chars = s.chars();
2475    match chars.next() {
2476        None => String::new(),
2477        Some(c) => c.to_lowercase().to_string() + chars.as_str(),
2478    }
2479}
2480
2481/// Convert an attribute chain into dotted name.
2482/// Example: `Console.print` -> `Some("Console.print")`.
2483pub(crate) fn expr_to_dotted_name(expr: &Expr) -> Option<String> {
2484    crate::ir::expr_to_dotted_name(expr)
2485}
2486
2487/// Oracle v1: how to materialise the oracle argument for an effectful
2488/// fn call in a law body.
2489///
2490/// - `LemmaBinding` — use the lemma-local identifier (`rnd`), matching
2491///   the `given` name. Correct for the universal lemma body.
2492/// - `SampleValue` — use the first Explicit domain value (the stub
2493///   fn's identifier, e.g. `stubConst`). Correct for the concrete
2494///   sample assertions where there's no lemma binding in scope and a
2495///   single domain value.
2496/// - `SampleCaseBinding(case_bindings)` — use the per-case binding
2497///   value (by `given.name`). Correct for sample theorems when the
2498///   domain has multiple values and each case substitutes a
2499///   different one (`given stub: Http.get = [httpDown, httpOk]`).
2500#[derive(Debug, Clone)]
2501pub(crate) enum OracleInjectionMode<'a> {
2502    LemmaBinding,
2503    /// Like `LemmaBinding` but project through the subtype carrier
2504    /// for classified `Generative` / `GenerativeOutput` effect-givens
2505    /// — `g.name` becomes `g.name.val` in the rewritten expression.
2506    /// Used by the Lean backend where lifted theorems quantify over
2507    /// the constrained subtype (`RandomIntInBounds`) instead of the
2508    /// plain function type, so call sites need to peel the carrier.
2509    /// Dafny stays on `LemmaBinding` (no first-class subtype types
2510    /// over functions); the bound is enforced via `requires` on the
2511    /// emitted lemma instead.
2512    LemmaBindingProjected,
2513    #[allow(dead_code)]
2514    SampleValue,
2515    SampleCaseBinding(&'a [(String, crate::ast::Spanned<Expr>)]),
2516}
2517
2518/// Oracle v1: rewrite any call to an effectful fn in a law body so
2519/// it targets the lifted signature — prepend `BranchPath.root()` (for
2520/// generative / gen+output effects) plus one argument per classified
2521/// non-output effect in the callee's signature.
2522///
2523/// Backend-agnostic — operates on AST + `CodegenContext`. Both the
2524/// Dafny and Lean backends call this before emitting the law body so
2525/// the law statement matches the lifted fn shape emitted alongside.
2526pub(crate) fn rewrite_effectful_calls_in_law<'fd, F>(
2527    expr: &crate::ast::Spanned<Expr>,
2528    law: &crate::ast::VerifyLaw,
2529    find_fn_def: F,
2530    mode: OracleInjectionMode,
2531) -> crate::ast::Spanned<Expr>
2532where
2533    F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
2534{
2535    use crate::ast::{Spanned, VerifyGivenDomain};
2536
2537    let injection_by_effect: std::collections::HashMap<String, Spanned<Expr>> = law
2538        .givens
2539        .iter()
2540        .filter_map(|g| {
2541            let arg_expr = match &mode {
2542                OracleInjectionMode::LemmaBinding => {
2543                    Spanned::new(Expr::Ident(g.name.clone()), expr.line)
2544                }
2545                OracleInjectionMode::LemmaBindingProjected => {
2546                    // Inject the bare oracle name; the post-rewrite pass
2547                    // `project_oracle_direct_calls` walks the whole
2548                    // expression once and lifts every reference to a
2549                    // subtype-carried oracle (callee, arg, comparison
2550                    // LHS, ...) through `.val`. Doing the projection
2551                    // here as well would compound — `Attr(Attr(rng,
2552                    // val), val)` for refs the injection wraps.
2553                    Spanned::new(Expr::Ident(g.name.clone()), expr.line)
2554                }
2555                OracleInjectionMode::SampleValue => match &g.domain {
2556                    VerifyGivenDomain::Explicit(vals) => vals.first().cloned()?,
2557                    _ => return None,
2558                },
2559                OracleInjectionMode::SampleCaseBinding(case_bindings) => case_bindings
2560                    .iter()
2561                    .find(|(name, _)| name == &g.name)
2562                    .map(|(_, v)| v.clone())?,
2563            };
2564            Some((g.type_name.clone(), arg_expr))
2565        })
2566        .collect();
2567    let rewritten = rewrite_effectful_call(expr, &injection_by_effect, find_fn_def);
2568
2569    // For `LemmaBindingProjected`, oracle bindings live as subtypes
2570    // (`RandomIntInBounds` etc.); direct calls `rng(path, n, min, max)`
2571    // in the law body need to peel `.val` off the carrier. Walk the
2572    // rewritten expression once more and rewrite direct
2573    // `FnCall(Ident(<oracle_name>), args)` shapes into
2574    // `FnCall(Attr(Ident(<oracle_name>), "val"), args)` for every
2575    // classified Generative-shape given. Other modes leave the body
2576    // alone.
2577    if matches!(mode, OracleInjectionMode::LemmaBindingProjected) {
2578        // Only givens whose effect has a bounded-subtype carrier
2579        // (`RandomIntInBounds` / `RandomFloatInUnit` /
2580        // `TimeUnixMsNonneg`) get `.val` projection. `EffectDimension::
2581        // Generative*` covers more effects than that — e.g.
2582        // `Disk.writeText` is `GenerativeOutput` but the quant param
2583        // stays a plain function in both Lean and Dafny because there's
2584        // no bound to encode. Projecting `.val` on a plain function
2585        // emits `write.val` on something with no `val` field and the
2586        // proof rejects it.
2587        let oracle_names: std::collections::HashSet<String> = law
2588            .givens
2589            .iter()
2590            .filter(|g| crate::types::checker::oracle_subtypes::has_bounded_subtype(&g.type_name))
2591            .map(|g| g.name.clone())
2592            .collect();
2593        if !oracle_names.is_empty() {
2594            return project_oracle_direct_calls(&rewritten, &oracle_names);
2595        }
2596    }
2597    rewritten
2598}
2599
2600/// Rewrite every reference to a subtype-carried oracle so the surrounding
2601/// expression type-checks against the carrier:
2602///
2603/// * Bare ident `rng` → `rng.val` (when `rng` is passed as an argument
2604///   to a helper, or compared with `=` in a domain-premise / `when`
2605///   clause).
2606/// * Direct call `rng(args...)` → `rng.val(args...)` (the underlying
2607///   function call site).
2608///
2609/// Recursive over the whole expression. In nested expressions like
2610/// `Result.Ok(rng(p, n, 1, 6))` or `pairSpec(BranchPath.Root, rng)`,
2611/// each oracle reference is projected exactly once.
2612fn project_oracle_direct_calls(
2613    expr: &crate::ast::Spanned<Expr>,
2614    oracle_names: &std::collections::HashSet<String>,
2615) -> crate::ast::Spanned<Expr> {
2616    use crate::ast::Spanned;
2617    let line = expr.line;
2618    let project_ident = |name: &str, line: usize| -> Spanned<Expr> {
2619        Spanned::new(
2620            Expr::Attr(
2621                Box::new(Spanned::new(Expr::Ident(name.to_string()), line)),
2622                "val".to_string(),
2623            ),
2624            line,
2625        )
2626    };
2627    let new_node = match &expr.node {
2628        // Bare ident reference to a subtype-carried oracle — project.
2629        // Catches helper-call args (`pairSpec(root, rng)`) and any
2630        // other position where the oracle name appears as a value.
2631        Expr::Ident(name) if oracle_names.contains(name) => {
2632            return project_ident(name, line);
2633        }
2634        Expr::FnCall(callee, args) => {
2635            let new_args: Vec<Spanned<Expr>> = args
2636                .iter()
2637                .map(|a| project_oracle_direct_calls(a, oracle_names))
2638                .collect();
2639            // `rng(...)` direct call — project the callee.
2640            let new_callee = if let Expr::Ident(name) = &callee.node
2641                && oracle_names.contains(name)
2642            {
2643                project_ident(name, callee.line)
2644            } else {
2645                project_oracle_direct_calls(callee, oracle_names)
2646            };
2647            Expr::FnCall(Box::new(new_callee), new_args)
2648        }
2649        Expr::Constructor(name, Some(arg)) => Expr::Constructor(
2650            name.clone(),
2651            Some(Box::new(project_oracle_direct_calls(arg, oracle_names))),
2652        ),
2653        Expr::Attr(obj, field) => Expr::Attr(
2654            Box::new(project_oracle_direct_calls(obj, oracle_names)),
2655            field.clone(),
2656        ),
2657        Expr::BinOp(op, l, r) => Expr::BinOp(
2658            *op,
2659            Box::new(project_oracle_direct_calls(l, oracle_names)),
2660            Box::new(project_oracle_direct_calls(r, oracle_names)),
2661        ),
2662        other => other.clone(),
2663    };
2664    Spanned::new(new_node, line)
2665}
2666
2667fn rewrite_effectful_call<'fd, F>(
2668    expr: &crate::ast::Spanned<Expr>,
2669    injection_by_effect: &std::collections::HashMap<String, crate::ast::Spanned<Expr>>,
2670    find_fn_def: F,
2671) -> crate::ast::Spanned<Expr>
2672where
2673    F: Fn(&str) -> Option<&'fd crate::ast::FnDef> + Copy,
2674{
2675    use crate::ast::Spanned;
2676    use crate::types::checker::effect_classification::{EffectDimension, classify};
2677
2678    match &expr.node {
2679        Expr::FnCall(callee, args) => {
2680            let rewritten_args: Vec<Spanned<Expr>> = args
2681                .iter()
2682                .map(|a| rewrite_effectful_call(a, injection_by_effect, find_fn_def))
2683                .collect();
2684            let rewritten_callee = Box::new(rewrite_effectful_call(
2685                callee,
2686                injection_by_effect,
2687                find_fn_def,
2688            ));
2689
2690            let callee_name = match &callee.node {
2691                Expr::Ident(name) => Some(name.clone()),
2692                Expr::Resolved { name, .. } => Some(name.clone()),
2693                _ => None,
2694            };
2695
2696            if let Some(name) = callee_name
2697                && let Some(fd) = find_fn_def(&name)
2698                && !fd.effects.is_empty()
2699                && fd
2700                    .effects
2701                    .iter()
2702                    .all(|e| crate::types::checker::effect_classification::is_classified(&e.node))
2703            {
2704                let mut injected: Vec<Spanned<Expr>> = Vec::new();
2705                let needs_path = fd.effects.iter().any(|e| {
2706                    matches!(
2707                        classify(&e.node).map(|c| c.dimension),
2708                        Some(EffectDimension::Generative | EffectDimension::GenerativeOutput)
2709                    )
2710                });
2711                if needs_path {
2712                    injected.push(Spanned::new(
2713                        // `BranchPath.Root` — nullary value
2714                        // constructor (PascalCase, no parens),
2715                        // symmetric with `Option.None`.
2716                        Expr::Attr(
2717                            Box::new(Spanned::new(
2718                                Expr::Ident("BranchPath".to_string()),
2719                                expr.line,
2720                            )),
2721                            "Root".to_string(),
2722                        ),
2723                        expr.line,
2724                    ));
2725                }
2726                let mut seen = std::collections::HashSet::new();
2727                for e in &fd.effects {
2728                    if !seen.insert(e.node.clone()) {
2729                        continue;
2730                    }
2731                    let Some(c) = classify(&e.node) else { continue };
2732                    if matches!(c.dimension, EffectDimension::Output) {
2733                        continue;
2734                    }
2735                    if let Some(inj) = injection_by_effect.get(&e.node) {
2736                        injected.push(inj.clone());
2737                    }
2738                }
2739                injected.extend(rewritten_args);
2740                return Spanned::new(Expr::FnCall(rewritten_callee, injected), expr.line);
2741            }
2742
2743            Spanned::new(Expr::FnCall(rewritten_callee, rewritten_args), expr.line)
2744        }
2745        Expr::BinOp(op, l, r) => Spanned::new(
2746            Expr::BinOp(
2747                *op,
2748                Box::new(rewrite_effectful_call(l, injection_by_effect, find_fn_def)),
2749                Box::new(rewrite_effectful_call(r, injection_by_effect, find_fn_def)),
2750            ),
2751            expr.line,
2752        ),
2753        Expr::Tuple(items) => Spanned::new(
2754            Expr::Tuple(
2755                items
2756                    .iter()
2757                    .map(|i| rewrite_effectful_call(i, injection_by_effect, find_fn_def))
2758                    .collect(),
2759            ),
2760            expr.line,
2761        ),
2762        _ => expr.clone(),
2763    }
2764}
2765
2766/// Oracle v1: set of user fn names that are reachable from any verify
2767/// block — directly (`verify f ...`) or through the call graph (fn
2768/// body of a reachable fn mentions them). Used by proof backends to
2769/// skip emission of effectful fns that nobody verifies. Dead code in
2770/// a proof output isn't just ugly — a non-terminating effectful fn
2771/// (e.g. a REPL loop) will make Lean reject the whole module because
2772/// it can't prove termination for a fn with no decreasing argument.
2773/// If the user never asked for a proof about that fn, don't force
2774/// the backend to invent one.
2775pub(crate) fn verify_reachable_fn_names(items: &[TopLevel]) -> HashSet<String> {
2776    let mut reachable: HashSet<String> = HashSet::new();
2777    for item in items {
2778        if let TopLevel::Verify(vb) = item {
2779            collect_verify_block_refs(vb, &mut reachable);
2780        }
2781    }
2782    // Fixed-point closure through the call graph.
2783    loop {
2784        let mut changed = false;
2785        for item in items {
2786            if let TopLevel::FnDef(fd) = item
2787                && reachable.contains(&fd.name)
2788            {
2789                let mut called = HashSet::new();
2790                collect_called_idents_in_body(&fd.body, &mut called);
2791                for name in called {
2792                    if reachable.insert(name) {
2793                        changed = true;
2794                    }
2795                }
2796            }
2797        }
2798        if !changed {
2799            break;
2800        }
2801    }
2802    reachable
2803}
2804
2805fn collect_verify_block_refs(vb: &VerifyBlock, out: &mut HashSet<String>) {
2806    out.insert(vb.fn_name.clone());
2807    for (lhs, rhs) in &vb.cases {
2808        collect_called_idents(lhs, out);
2809        collect_called_idents(rhs, out);
2810    }
2811    if let VerifyKind::Law(law) = &vb.kind {
2812        collect_called_idents(&law.lhs, out);
2813        collect_called_idents(&law.rhs, out);
2814        if let Some(when) = &law.when {
2815            collect_called_idents(when, out);
2816        }
2817        for given in &law.givens {
2818            if let VerifyGivenDomain::Explicit(values) = &given.domain {
2819                for v in values {
2820                    collect_called_idents(v, out);
2821                }
2822            }
2823        }
2824    }
2825    for given in &vb.cases_givens {
2826        if let VerifyGivenDomain::Explicit(values) = &given.domain {
2827            for v in values {
2828                collect_called_idents(v, out);
2829            }
2830        }
2831    }
2832}
2833
2834fn collect_called_idents_in_body(body: &FnBody, out: &mut HashSet<String>) {
2835    for stmt in body.stmts() {
2836        match stmt {
2837            Stmt::Binding(_, _, e) | Stmt::Expr(e) => collect_called_idents(e, out),
2838        }
2839    }
2840}
2841
2842fn collect_called_idents(expr: &Spanned<Expr>, out: &mut HashSet<String>) {
2843    match &expr.node {
2844        Expr::FnCall(callee, args) => {
2845            if let Expr::Ident(name) | Expr::Resolved { name, .. } = &callee.node {
2846                out.insert(name.clone());
2847            } else {
2848                collect_called_idents(callee, out);
2849            }
2850            for a in args {
2851                collect_called_idents(a, out);
2852            }
2853        }
2854        Expr::TailCall(boxed) => {
2855            let TailCallData { target, args, .. } = boxed.as_ref();
2856            out.insert(target.clone());
2857            for a in args {
2858                collect_called_idents(a, out);
2859            }
2860        }
2861        Expr::Ident(name) | Expr::Resolved { name, .. } => {
2862            out.insert(name.clone());
2863        }
2864        Expr::BinOp(_, l, r) => {
2865            collect_called_idents(l, out);
2866            collect_called_idents(r, out);
2867        }
2868        Expr::Neg(inner) => collect_called_idents(inner, out),
2869        Expr::Match { subject, arms, .. } => {
2870            collect_called_idents(subject, out);
2871            for arm in arms {
2872                collect_called_idents(&arm.body, out);
2873            }
2874        }
2875        Expr::ErrorProp(inner) | Expr::Attr(inner, _) => {
2876            collect_called_idents(inner, out);
2877        }
2878        Expr::Constructor(_, Some(inner)) => {
2879            collect_called_idents(inner, out);
2880        }
2881        Expr::InterpolatedStr(parts) => {
2882            for part in parts {
2883                if let StrPart::Parsed(inner) = part {
2884                    collect_called_idents(inner, out);
2885                }
2886            }
2887        }
2888        Expr::List(items) | Expr::Tuple(items) | Expr::IndependentProduct(items, _) => {
2889            for i in items {
2890                collect_called_idents(i, out);
2891            }
2892        }
2893        Expr::MapLiteral(entries) => {
2894            for (k, v) in entries {
2895                collect_called_idents(k, out);
2896                collect_called_idents(v, out);
2897            }
2898        }
2899        Expr::RecordCreate { fields, .. } => {
2900            for (_, v) in fields {
2901                collect_called_idents(v, out);
2902            }
2903        }
2904        Expr::RecordUpdate { base, updates, .. } => {
2905            collect_called_idents(base, out);
2906            for (_, v) in updates {
2907                collect_called_idents(v, out);
2908            }
2909        }
2910        Expr::Literal(_) | Expr::Constructor(_, None) => {}
2911    }
2912}
2913
2914/// Sections gathered per emission scope ("" for entry, module prefix
2915/// otherwise). Each backend appends to the bucket for the scope a fn
2916/// (or its SCC component) belongs to.
2917pub(crate) struct PerScopeSections {
2918    pub by_scope: std::collections::HashMap<String, Vec<String>>,
2919}
2920
2921impl PerScopeSections {
2922    pub(crate) fn take(&mut self, scope: &str) -> Vec<String> {
2923        self.by_scope.remove(scope).unwrap_or_default()
2924    }
2925}
2926
2927/// Run SCC analysis on each scope's pure fns independently and route the
2928/// rendered output through the supplied closure. Lean and Dafny share
2929/// this — each scope (entry or dependent module) is SCC-analyzed in
2930/// isolation so a `def foo` in one module and an unrelated `def foo` in
2931/// another module don't get conflated.
2932///
2933/// `is_pure` filters which fns participate; `emit` renders one SCC
2934/// component (>= 1 fn) into the lines to append to that scope's bucket.
2935pub(crate) fn route_pure_components_per_scope<F, G>(
2936    ctx: &CodegenContext,
2937    is_pure: F,
2938    mut emit: G,
2939) -> PerScopeSections
2940where
2941    F: Fn(&FnDef) -> bool,
2942    G: FnMut(&[&FnDef], &str) -> Vec<String>,
2943{
2944    let mut by_scope: std::collections::HashMap<String, Vec<String>> =
2945        std::collections::HashMap::new();
2946
2947    let mut process =
2948        |fns: Vec<&FnDef>,
2949         scope: String,
2950         by_scope: &mut std::collections::HashMap<String, Vec<String>>| {
2951            let comps = crate::call_graph::ordered_fn_components(&fns, &ctx.module_prefixes);
2952            let bucket = by_scope.entry(scope.clone()).or_default();
2953            for comp in comps {
2954                bucket.extend(emit(&comp, scope.as_str()));
2955            }
2956        };
2957
2958    for module in &ctx.modules {
2959        let pure: Vec<&FnDef> = module.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
2960        process(pure, module.prefix.clone(), &mut by_scope);
2961    }
2962    let entry_pure: Vec<&FnDef> = ctx.fn_defs.iter().filter(|fd| is_pure(fd)).collect();
2963    process(entry_pure, String::new(), &mut by_scope);
2964
2965    PerScopeSections { by_scope }
2966}
2967
2968#[cfg(test)]
2969mod tests {
2970    use super::*;
2971    use crate::ast::{Literal, VerifyGiven, VerifyGivenDomain, VerifyLaw};
2972
2973    fn sb(node: Expr) -> Spanned<Expr> {
2974        Spanned::new(node, 1)
2975    }
2976
2977    fn bsb(node: Expr) -> Box<Spanned<Expr>> {
2978        Box::new(sb(node))
2979    }
2980
2981    fn law_with(lhs: Spanned<Expr>, rhs: Spanned<Expr>) -> VerifyLaw {
2982        VerifyLaw {
2983            name: "test".to_string(),
2984            givens: vec![VerifyGiven {
2985                name: "xs".to_string(),
2986                type_name: "List<String>".to_string(),
2987                domain: VerifyGivenDomain::Explicit(vec![sb(Expr::List(vec![]))]),
2988            }],
2989            when: None,
2990            lhs,
2991            rhs,
2992            sample_guards: Vec::new(),
2993        }
2994    }
2995
2996    #[test]
2997    fn law_calls_unclassified_fn_detects_dotted_callee() {
2998        // Review finding 1 (round 2): the recursion classifier emits
2999        // `UnclassifiedFn` messages keyed by bare `fd.name`, so the
3000        // unclassified set the gate consults holds BARE names even
3001        // for fns living in a dep module. A law calling
3002        // `Dep.toSorted(xs)` against a bare-`toSorted` unclassified
3003        // set has to match on the bare suffix too — otherwise the
3004        // gate doesn't fire and the backend emits a non-closing
3005        // `induction t with …`. Also accepts a fully-qualified entry
3006        // for forward compat with a future classifier that emits
3007        // canonical names.
3008        let lhs = sb(Expr::FnCall(
3009            bsb(Expr::Attr(
3010                bsb(Expr::Ident("Dep".to_string())),
3011                "toSorted".to_string(),
3012            )),
3013            vec![sb(Expr::Ident("xs".to_string()))],
3014        ));
3015        let rhs = sb(Expr::Ident("xs".to_string()));
3016        let law = law_with(lhs, rhs);
3017
3018        // Forward-compat: a fully-qualified entry matches a dotted
3019        // callee directly.
3020        let mut canonical = HashSet::new();
3021        canonical.insert("Dep.toSorted".to_string());
3022        assert!(law_calls_unclassified_fn(&law, &canonical));
3023
3024        // Real production shape today: classifier emits bare names;
3025        // the gate must still catch the dotted callsite.
3026        let mut bare_only = HashSet::new();
3027        bare_only.insert("toSorted".to_string());
3028        assert!(
3029            law_calls_unclassified_fn(&law, &bare_only),
3030            "bare unclassified name must catch a dotted callsite via suffix match"
3031        );
3032
3033        // Negative control: an unrelated bare name in the set does
3034        // not match the dotted callsite.
3035        let mut unrelated = HashSet::new();
3036        unrelated.insert("somethingElse".to_string());
3037        assert!(!law_calls_unclassified_fn(&law, &unrelated));
3038    }
3039
3040    #[test]
3041    fn law_lhs_has_trace_projection_skips_user_record_field() {
3042        // Review finding 4: a user-defined record with a `trace`
3043        // field (`record Log { trace: String, ... }`) used to trigger
3044        // the gate as soon as `log.trace` appeared in the LHS. The
3045        // detection now requires BOTH a `.trace` field projection
3046        // AND a trace-API method call (`.event`, `.group`, `.branch`,
3047        // `.length`, `.contains`) — a bare `log.trace` is just a
3048        // record access and stays in normal emit.
3049        let user_field_lhs = sb(Expr::Attr(
3050            bsb(Expr::Ident("log".to_string())),
3051            "trace".to_string(),
3052        ));
3053        assert!(
3054            !law_lhs_has_trace_projection(&user_field_lhs),
3055            "bare user-record `.trace` field must not trigger the gate"
3056        );
3057
3058        // Real Oracle trace projection: `fn().trace.event(0)`.
3059        let runtime_trace_lhs = sb(Expr::FnCall(
3060            bsb(Expr::Attr(
3061                bsb(Expr::Attr(
3062                    bsb(Expr::FnCall(bsb(Expr::Ident("fn".to_string())), vec![])),
3063                    "trace".to_string(),
3064                )),
3065                "event".to_string(),
3066            )),
3067            vec![sb(Expr::Literal(Literal::Int(0)))],
3068        ));
3069        assert!(
3070            law_lhs_has_trace_projection(&runtime_trace_lhs),
3071            "Oracle `.trace.event(0)` projection must trigger the gate"
3072        );
3073    }
3074
3075    #[test]
3076    fn resolve_refined_type_disambiguates_cross_module_same_bare_name() {
3077        // Review finding 3: two modules each declaring a refined
3078        // `Natural` (different predicates) used to silently collide
3079        // under bare keying — `populate_refined_types` skipped the
3080        // second one via `contains_key`, and the backend lookup
3081        // returned whichever predicate landed first. With canonical
3082        // keying (`A.Natural` / `B.Natural`) the slots are separate;
3083        // a fully-qualified lookup matches exactly, a bare lookup
3084        // resolves through module-walk ordering.
3085        use crate::ast::{TypeDef, TypeVariant};
3086        use crate::codegen::ModuleInfo;
3087        use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
3088        use std::collections::HashMap;
3089        let _ = TypeVariant {
3090            name: String::new(),
3091            fields: Vec::new(),
3092        }; // make sure TypeVariant import resolves under all features
3093
3094        let make_module = |prefix: &str| ModuleInfo {
3095            prefix: prefix.to_string(),
3096            depends: Vec::new(),
3097            type_defs: vec![TypeDef::Product {
3098                name: "Natural".to_string(),
3099                fields: vec![("value".to_string(), "Int".to_string())],
3100                line: 1,
3101            }],
3102            fn_defs: Vec::new(),
3103            verify_laws: Vec::new(),
3104            analysis: None,
3105        };
3106        let modules = vec![make_module("A"), make_module("B")];
3107
3108        let make_decl = |predicate_param: &str, witness: i64| RefinedTypeDecl {
3109            name: "Natural".to_string(),
3110            carrier_type: "Int".to_string(),
3111            carrier_field: "value".to_string(),
3112            predicate_param: predicate_param.to_string(),
3113            invariant: Predicate {
3114                free_vars: vec![(
3115                    predicate_param.to_string(),
3116                    QuantifierType::Plain("Int".to_string()),
3117                )],
3118                expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
3119                    Literal::Bool(true),
3120                )),
3121            },
3122            witness: Some(witness.to_string()),
3123            interval: None,
3124            op_classes: Vec::new(),
3125        };
3126        let symbols = crate::ir::SymbolTable::build(&[], &modules);
3127        let a_id = symbols
3128            .type_id_of(&crate::ir::TypeKey::in_module("A", "Natural"))
3129            .expect("A.Natural TypeId");
3130        let b_id = symbols
3131            .type_id_of(&crate::ir::TypeKey::in_module("B", "Natural"))
3132            .expect("B.Natural TypeId");
3133
3134        let mut refined_types: HashMap<crate::ir::TypeId, RefinedTypeDecl> = HashMap::new();
3135        refined_types.insert(a_id, make_decl("a", 0));
3136        refined_types.insert(b_id, make_decl("b", 10));
3137
3138        // Fully-qualified lookups hit the exact slot.
3139        let a = resolve_refined_type_in(&refined_types, &symbols, &modules, "A.Natural")
3140            .expect("A.Natural canonical lookup");
3141        assert_eq!(a.predicate_param, "a");
3142        assert_eq!(a.witness.as_deref(), Some("0"));
3143        let b = resolve_refined_type_in(&refined_types, &symbols, &modules, "B.Natural")
3144            .expect("B.Natural canonical lookup");
3145        assert_eq!(b.predicate_param, "b");
3146        assert_eq!(b.witness.as_deref(), Some("10"));
3147
3148        // Bare lookup finds *something* (module-walk first match) —
3149        // the typechecker prevents mixed usage upstream, so the
3150        // observed ordering is the order modules walk.
3151        let bare = resolve_refined_type_in(&refined_types, &symbols, &modules, "Natural")
3152            .expect("bare Natural resolves via module walk");
3153        assert!(
3154            bare.predicate_param == "a" || bare.predicate_param == "b",
3155            "bare Natural must resolve to one of the canonical decls"
3156        );
3157
3158        // No-module-owner lookup misses — i.e. a bare name that
3159        // wasn't declared in any module is None, not "first in map".
3160        assert!(resolve_refined_type_in(&refined_types, &symbols, &modules, "Unrelated").is_none());
3161    }
3162
3163    #[test]
3164    fn find_refined_type_scoped_prefers_current_module_over_entry_collision() {
3165        // Review finding 2 (round 3): when entry AND a dep module
3166        // both declare a refined record of the same bare name, the
3167        // module's emit pass (passing `scope = Some(prefix)` and a
3168        // bare reference) must resolve to its OWN slot, not entry's.
3169        // The pre-fix order tried direct `refined_types.get(name)`
3170        // first, which matched entry's bare-keyed slot and the
3171        // scope-prefix lookup never ran.
3172        use crate::ast::{TopLevel, TypeDef};
3173        use crate::codegen::{CodegenContext, ModuleInfo};
3174        use crate::ir::proof_ir::{Predicate, QuantifierType, RefinedTypeDecl};
3175        use std::collections::{HashMap, HashSet};
3176
3177        let entry_natural = TypeDef::Product {
3178            name: "Natural".to_string(),
3179            fields: vec![("value".to_string(), "Int".to_string())],
3180            line: 1,
3181        };
3182        let module = ModuleInfo {
3183            prefix: "Mod".to_string(),
3184            depends: Vec::new(),
3185            type_defs: vec![TypeDef::Product {
3186                name: "Natural".to_string(),
3187                fields: vec![("value".to_string(), "Int".to_string())],
3188                line: 1,
3189            }],
3190            fn_defs: Vec::new(),
3191            verify_laws: Vec::new(),
3192            analysis: None,
3193        };
3194
3195        let make_decl = |param: &str, witness: &str| RefinedTypeDecl {
3196            name: "Natural".to_string(),
3197            carrier_type: "Int".to_string(),
3198            carrier_field: "value".to_string(),
3199            predicate_param: param.to_string(),
3200            invariant: Predicate {
3201                free_vars: vec![(param.to_string(), QuantifierType::Plain("Int".to_string()))],
3202                expr: crate::ast::Spanned::bare(crate::ir::hir::ResolvedExpr::Literal(
3203                    Literal::Bool(true),
3204                )),
3205            },
3206            witness: Some(witness.to_string()),
3207            interval: None,
3208            op_classes: Vec::new(),
3209        };
3210
3211        let items = vec![TopLevel::TypeDef(entry_natural)];
3212        let modules = vec![module];
3213        let symbol_table = crate::ir::SymbolTable::build(&items, &modules);
3214        let entry_id = symbol_table
3215            .type_id_of(&crate::ir::TypeKey::entry("Natural"))
3216            .expect("entry Natural id");
3217        let mod_id = symbol_table
3218            .type_id_of(&crate::ir::TypeKey::in_module("Mod", "Natural"))
3219            .expect("Mod.Natural id");
3220
3221        let mut ctx = CodegenContext {
3222            items,
3223            type_defs: Vec::new(),
3224            fn_defs: Vec::new(),
3225            project_name: "scope-test".to_string(),
3226            modules,
3227            module_prefixes: HashSet::new(),
3228            #[cfg(feature = "runtime")]
3229            policy: None,
3230            emit_replay_runtime: false,
3231            runtime_policy_from_env: false,
3232            guest_entry: None,
3233            emit_self_host_support: false,
3234            extra_fn_defs: Vec::new(),
3235            mutual_tco_members: HashSet::new(),
3236            recursive_fns: HashSet::new(),
3237            buffer_build_sinks: HashMap::new(),
3238            buffer_fusion_sites: Vec::new(),
3239            synthesized_buffered_fns: Vec::new(),
3240            proof_ir: crate::ir::ProofIR::default(),
3241            symbol_table,
3242            resolved_fn_defs: Vec::new(),
3243            resolved_module_fn_defs: Vec::new(),
3244            current_module_scope: std::cell::RefCell::new(None),
3245            resolved_program: crate::codegen::program_view::ResolvedProgramView::default(),
3246            program_shape: None,
3247            mir_program: None,
3248            bare_i64: Default::default(),
3249            discovered_lemmas: Vec::new(),
3250            sample_expected: std::collections::HashMap::new(),
3251        };
3252        ctx.proof_ir
3253            .refined_types
3254            .insert(entry_id, make_decl("entry_n", "0"));
3255        ctx.proof_ir
3256            .refined_types
3257            .insert(mod_id, make_decl("mod_n", "10"));
3258
3259        let from_module = find_refined_type_scoped(&ctx, "Natural", Some("Mod"))
3260            .expect("Mod-scoped Natural lookup");
3261        assert_eq!(
3262            from_module.predicate_param, "mod_n",
3263            "scope=Some(\"Mod\") + bare `Natural` must resolve to Mod.Natural, \
3264             not entry's bare-keyed slot"
3265        );
3266
3267        // Entry scope still resolves to entry's slot.
3268        let from_entry =
3269            find_refined_type_scoped(&ctx, "Natural", None).expect("entry Natural lookup");
3270        assert_eq!(from_entry.predicate_param, "entry_n");
3271    }
3272}