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