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