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