Skip to main content

aver/codegen/
mod.rs

1/// Aver → target language transpilation.
2///
3/// The codegen module transforms a type-checked Aver AST into source code
4/// for a target language. Current backends: Rust deployment and Lean proof export.
5pub(crate) mod builtin_helpers;
6pub(crate) mod builtin_records;
7pub(crate) mod builtins;
8#[cfg(feature = "wasm-compile")]
9pub mod cert;
10#[cfg(feature = "runtime")]
11pub(crate) mod cite_instantiate;
12pub mod common;
13#[cfg(feature = "runtime")]
14pub mod dafny;
15#[cfg(feature = "runtime")]
16pub mod lean;
17#[cfg(feature = "runtime")]
18pub mod lemma_discovery;
19pub mod program_view;
20pub mod proof_lower;
21#[cfg(feature = "runtime")]
22pub(crate) mod proof_recognize;
23#[cfg(feature = "runtime")]
24pub mod recursion;
25#[cfg(feature = "runtime")]
26pub mod rust;
27pub mod scc;
28#[cfg(feature = "wasip2")]
29pub mod wasip2;
30#[cfg(feature = "wasm-compile")]
31pub mod wasm_gc;
32
33use std::collections::{HashMap, HashSet};
34
35use crate::ast::{FnDef, TopLevel, TypeDef};
36use crate::source::LoadedModule;
37use crate::types::checker::TypeCheckResult;
38
39/// Information about a dependent module loaded for codegen.
40pub struct ModuleInfo {
41    /// Qualified module path, e.g. "Models.User".
42    pub prefix: String,
43    /// Direct `depends [...]` entries from the source module.
44    pub depends: Vec<String>,
45    /// Type definitions from the module.
46    pub type_defs: Vec<TypeDef>,
47    /// Function definitions from the module (excluding `main`).
48    pub fn_defs: Vec<FnDef>,
49    /// `verify … law` blocks of this dep module, in source order.
50    ///
51    /// Carried so the Lean proof backend can (a) emit each proven dep
52    /// law as a `<fn>_law_<name>` theorem inside `namespace M`, and
53    /// (b) admit it into a consumer law's lemma pool under the same
54    /// cone ∪ subject admissibility gate as in-file sibling laws — the
55    /// cross-file law pool. Only `VerifyKind::Law` blocks are kept;
56    /// plain example-style `verify` blocks in a dep are still dropped
57    /// (module-scoped sampling is a separate feature). Read ONLY by the
58    /// Lean proof emit / pool; inert for every other backend.
59    pub verify_laws: Vec<crate::ast::VerifyBlock>,
60    /// IR-level analysis facts produced by the dep module's pipeline run
61    /// (`analyze` stage). `None` for modules loaded via paths that skip
62    /// the analyze stage (none in production today; left optional for
63    /// future ad-hoc loaders). Aver's module DAG invariant makes per-module
64    /// analysis sufficient — see `project_aver_module_dag` memory and
65    /// `src/ir/analyze.rs` for why cross-module SCCs are impossible.
66    pub analysis: Option<crate::ir::AnalysisResult>,
67}
68
69impl ModuleInfo {
70    /// Build a [`ModuleInfo`] from a freshly-parsed [`LoadedModule`].
71    /// Skips the analyze stage — callers that need per-dep analysis
72    /// facts should run the pipeline themselves (see
73    /// `crate::main::commands::load_compile_deps` /
74    /// `playground::loaded_to_module_info`). Used by ad-hoc loaders
75    /// (`vm_profile`, the eval-spec test helpers) that just need the
76    /// dep's symbol layout to feed `SymbolTable::build` /
77    /// `pipeline::run`'s `dep_modules` slot.
78    pub fn from_loaded(loaded: &LoadedModule) -> Self {
79        let depends = loaded
80            .items
81            .iter()
82            .find_map(|i| match i {
83                TopLevel::Module(m) => Some(m.depends.clone()),
84                _ => None,
85            })
86            .unwrap_or_default();
87        let type_defs = loaded
88            .items
89            .iter()
90            .filter_map(|i| match i {
91                TopLevel::TypeDef(td) => Some(td.clone()),
92                _ => None,
93            })
94            .collect();
95        let fn_defs = loaded
96            .items
97            .iter()
98            .filter_map(|i| match i {
99                TopLevel::FnDef(fd) if fd.name != "main" => Some(fd.clone()),
100                _ => None,
101            })
102            .collect();
103        Self {
104            prefix: loaded.dep_name.clone(),
105            depends,
106            type_defs,
107            fn_defs,
108            verify_laws: collect_verify_laws(&loaded.items),
109            analysis: None,
110        }
111    }
112}
113
114/// `verify … law` blocks from a module's top-level items, in source
115/// order. Plain example-style `verify` blocks (`VerifyKind::Example`)
116/// are excluded — only laws are lifted into a dep's [`ModuleInfo`] for
117/// the cross-file law pool.
118///
119/// VISIBILITY gate (cross-file law pool, fail-closed at admission): a
120/// law is lifted ONLY when its SUBJECT fn is EXPOSED by the module —
121/// the same rule [`crate::visibility::collect_module_exports`] /
122/// [`crate::visibility::SymbolRegistry::from_modules`] apply to fns. A
123/// law about a private helper (`_`-prefixed, or absent from a non-empty
124/// `exposes [...]` list) is a module-internal obligation: it is still
125/// proved in the module's OWN export, but it never enters a consumer's
126/// pool, is never emitted into a consumer's build, and is never lowered
127/// for a consumer. A consumer can only cite what its dependency makes
128/// public, exactly as it can only CALL exposed fns.
129pub fn collect_verify_laws(items: &[TopLevel]) -> Vec<crate::ast::VerifyBlock> {
130    use crate::ast::VerifyKind;
131    let module = crate::visibility::module_decl(items);
132    let exposes: Option<&[String]> = module.and_then(|m| {
133        if m.exposes.is_empty() {
134            None
135        } else {
136            Some(m.exposes.as_slice())
137        }
138    });
139    items
140        .iter()
141        .filter_map(|i| match i {
142            TopLevel::Verify(vb)
143                if matches!(vb.kind, VerifyKind::Law(_))
144                    && crate::visibility::is_exposed(&vb.fn_name, exposes) =>
145            {
146                Some(vb.clone())
147            }
148            _ => None,
149        })
150        .collect()
151}
152
153/// Collected context from the Aver program, shared across all backends.
154///
155/// # Invariant (epic #170 Phase 2)
156///
157/// **`resolved_program` is the primary backend input.** Every
158/// identity-sensitive decision (call/ctor/type lookup, fn-by-id
159/// dispatch, mutual-SCC analysis) belongs to that view; the
160/// pipeline produced it once and `build_context` projects it through.
161///
162/// The legacy AST-shape fields below — `items`, `fn_defs`,
163/// `type_defs`, `resolved_fn_defs`, `resolved_module_fn_defs` — are
164/// **source metadata / migration caches**, not independent sources
165/// of truth:
166///
167/// - `items`, `fn_defs`, `type_defs` retain source-shape spans and
168///   diagnostics; backends mid-migration still walk them. They are
169///   NOT the place to add new identity-sensitive logic.
170/// - `resolved_fn_defs` / `resolved_module_fn_defs` are projections
171///   of `resolved_program` kept for callsites that don't yet route
172///   through the `FnId` index. New code should reach
173///   `resolved_program.fn_by_id(fn_id)` instead.
174///
175/// Subsequent epic phases migrate backends (Rust, Lean, Dafny,
176/// wasm-gc) to iterate the view directly. New code in backends
177/// should default to the view. AST consumption requires a clear
178/// category in a code comment: `diagnostic-only`,
179/// `syntax-discovery-only`, `backend-link-stage`, `display-only`,
180/// or `temporary-migration-bridge`.
181pub struct CodegenContext {
182    /// All top-level items (post-TCO transform, post-typecheck).
183    ///
184    /// **Source metadata** — kept for span / diagnostic / syntax
185    /// discovery access. Backends iterating fn bodies should reach
186    /// `resolved_program.entry_fns()` instead.
187    pub items: Vec<TopLevel>,
188    /// User-defined type definitions (for struct/enum generation).
189    ///
190    /// **Source metadata.** Type-id-keyed lookups go through
191    /// `symbol_table` (see [`Self::symbol_table`]); fn bodies that
192    /// need a resolved type reach it via `Type::Named(TypeId, _)`
193    /// after the typechecker stamps. This list stays for ergonomic
194    /// iteration over user-declared types in syntax-discovery sites
195    /// (e.g. cataloguing all `enum` declarations for the proof
196    /// pipeline's refinement detection).
197    pub type_defs: Vec<TypeDef>,
198    /// User-defined function definitions.
199    ///
200    /// **Source metadata.** Backends mid-migration walk this for
201    /// fn-signature shape; new identity-sensitive code reaches
202    /// `resolved_program.entry_fns()` / `fn_by_id(fn_id)` instead.
203    /// Synthesized FnDefs (TCO hoists) appended after
204    /// the pipeline ran live here too; the on-demand resolver
205    /// (`Self::resolve_fn_def`) lifts them through the symbol table.
206    pub fn_defs: Vec<FnDef>,
207    /// Project/binary name.
208    pub project_name: String,
209    /// Dependent modules loaded for inlining.
210    pub modules: Vec<ModuleInfo>,
211    /// Set of module prefixes for qualified name resolution (e.g. "Models.User").
212    pub module_prefixes: HashSet<String>,
213    /// Embedded runtime policy from `aver.toml` for generated code.
214    #[cfg(feature = "runtime")]
215    pub policy: Option<crate::config::ProjectConfig>,
216    /// Emit generated scoped runtime support (replay and/or runtime-loaded policy).
217    pub emit_replay_runtime: bool,
218    /// Load runtime policy from the active module root instead of embedding it.
219    pub runtime_policy_from_env: bool,
220    /// Explicit guest entry boundary for scoped replay/policy.
221    pub guest_entry: Option<String>,
222    /// Emit extra generated helpers needed only by the cached self-host helper.
223    pub emit_self_host_support: bool,
224    /// Extra fn_defs visible during current module emission (not in `fn_defs` or `modules`).
225    /// Set temporarily by the Rust backend when emitting a dependent module so that
226    /// `find_fn_def_by_name` can resolve same-module calls.
227    pub extra_fn_defs: Vec<FnDef>,
228    /// Functions that are part of a mutual-TCO SCC group (emitted as
229    /// trampoline + wrappers). Functions NOT in this set but with
230    /// TailCalls are emitted as plain self-TCO loops. Keyed by opaque
231    /// [`crate::ir::FnId`] from the symbol table — entry-module fns
232    /// and dep-module fns with the same bare name can't accidentally
233    /// merge under bare-name keying.
234    pub mutual_tco_members: HashSet<crate::ir::FnId>,
235    /// Functions that call themselves directly or transitively. Set-
236    /// form union of `entry_analysis.recursive_fns` plus each
237    /// module's `analysis.recursive_fns`. Keyed by opaque
238    /// [`crate::ir::FnId`] — same disambiguation guarantee as
239    /// `mutual_tco_members`. Used by codegen sites that previously
240    /// called `call_graph::find_recursive_fns` ad-hoc.
241    pub recursive_fns: HashSet<crate::ir::FnId>,
242    /// Buffer-build sink fns (`List.prepend`/`reverse` builders consumed
243    /// by `String.join`). The Rust backend emits a `<fn>__buffered`
244    /// variant alongside each entry; the WASM backend rewrites bodies
245    /// to call `rt_buffer_*` helpers. Detection lives in `ir::buffer_build`.
246    pub buffer_build_sinks: HashMap<String, crate::ir::BufferBuildShape>,
247    /// Fusion sites detected for `String.join(<sink>(...), sep)` calls.
248    /// Each entry pairs an enclosing fn + line + sink fn name; the
249    /// emitter rewrites these call expressions to use buffered variants
250    /// in place of the producer + consumer chain.
251    pub buffer_fusion_sites: Vec<crate::ir::FusionSite>,
252    /// Synthesized `<fn>__buffered` variants for every buffer-build
253    /// sink, produced by `ir::synthesize_buffered_variants`. These are
254    /// real `FnDef`s with proper body AST; backends iterate over them
255    /// alongside `fn_defs` so they reach codegen through the same
256    /// pipeline (TCO / no-alloc / mutual-recursion all apply
257    /// identically). Empty when no sinks are detected.
258    pub synthesized_buffered_fns: Vec<FnDef>,
259    /// Proof-export decision IR populated by `proof_lower::lower`
260    /// during `build_context`. Backends (Lean, Dafny) read from
261    /// here to decide refinement-record lift, recursion contracts,
262    /// law-theorem shape, etc. Single source of truth — both
263    /// backends see the same decisions so cross-backend drift
264    /// becomes impossible at the shape level. Step 2: only
265    /// `refined_types` is populated; backends still consume legacy
266    /// `refinement_info_for` for now. Step 3+ migrates backends.
267    #[cfg(feature = "runtime")]
268    pub proof_ir: crate::ir::ProofIR,
269    /// Resolved-identity table (#138 phase E). Always populated:
270    /// `pipeline::run` builds it unconditionally and threads it
271    /// through `build_context`. Consumers (proof IR lookups,
272    /// backend FnId/TypeId resolution) read it directly — no
273    /// `Option` wrapper to unwrap at each callsite.
274    pub symbol_table: crate::ir::SymbolTable,
275    /// Resolved-HIR forms of every entry-scope fn in `fn_defs`,
276    /// in the same source order.
277    ///
278    /// **Compatibility projection of `resolved_program.entry_fns()`**
279    /// (epic #170 Phase 1). Position-aligned with the entry slice of
280    /// `resolved_program.entry_items`. New code should prefer
281    /// `resolved_program.entry_fns()` / `fn_by_id(fn_id)` so the
282    /// `FnId` index is the lookup mechanism. This vec stays for
283    /// callsites that haven't yet been migrated to the view; it will
284    /// be retired once Phase 3-6 migrate all backends.
285    pub resolved_fn_defs: Vec<crate::ir::hir::ResolvedFnDef>,
286    /// Module scope currently active for name resolution. Set by a
287    /// backend dispatcher before emitting a dep-module's fns so that
288    /// legacy resolve-on-demand adapters (e.g. Lean's
289    /// `emit_expr_legacy`) thread the right scope into
290    /// `resolve_expr` / `resolve_stmt` instead of defaulting to entry.
291    /// Empty by default. Set with [`Self::with_module_scope`] in a
292    /// scoped manner.
293    pub current_module_scope: std::cell::RefCell<Option<String>>,
294    /// Per-dep resolved fn defs, parallel to `modules`.
295    ///
296    /// **Compatibility projection of `resolved_program.modules[i].fn_defs`**
297    /// (epic #170 Phase 1). Position-aligned with `modules` for
298    /// callsites that index by `modules[i]`. New code should prefer
299    /// `resolved_program.module_fns(prefix)` or the global
300    /// `fn_by_id(fn_id)` index — that's where cross-module bare-name
301    /// disambiguation happens for free. Retired alongside
302    /// `resolved_fn_defs` once Phase 3-6 migrate the remaining
303    /// backends.
304    pub resolved_module_fn_defs: Vec<Vec<crate::ir::hir::ResolvedFnDef>>,
305    /// Canonical resolved-program view of the whole codegen input —
306    /// entry items (post-pipeline `NameResolve`) + per-dep-module
307    /// resolved fn defs + `FnId`-keyed lookup.
308    ///
309    /// **Epic #170 Phase 1 invariant.** `resolved_program` is the
310    /// primary source of truth for backend codegen — `fn_defs`,
311    /// `type_defs`, `items`, `resolved_fn_defs`, and
312    /// `resolved_module_fn_defs` remain available as projection /
313    /// source metadata / migration cache, but consumers should reach
314    /// the view first when an `FnId` / `TypeId` is in hand. Subsequent
315    /// phases (#170 Phase 3+) migrate backends to iterate the view as
316    /// their primary input; this field is the foundation those PRs
317    /// build on.
318    pub resolved_program: crate::codegen::program_view::ResolvedProgramView,
319    /// Whole-program shape facts — typed Archetype labels + call-graph
320    /// SCC per `FnId`. Computed once per compilation by
321    /// [`analyze_program`](crate::analysis::shape::analyze_program) at
322    /// `build_context` time. Stage 5+ of #232 (0.23 "Shape") migrates
323    /// ad-hoc fn-shape detectors in proof codegen to read this instead
324    /// of rewalking the AST. `None` only for tests that assemble the
325    /// ctx by hand without calling `build_context`; downstream callers
326    /// should treat that as opt-out (preserve legacy detection path).
327    pub program_shape: Option<crate::analysis::shape::ProgramShape>,
328    /// Optimized Core MIR for the whole codegen input (entry + dep
329    /// module fns), `FnId`-keyed. Built once at `build_context` from
330    /// the resolved program — the same lowering + optimizer pass the
331    /// VM / wasm-gc / wasip2 backends run. The Rust backend reads
332    /// `fn_by_id(fn_id)` here to drive its sole codegen path
333    /// (`from_mir::emit_mir_fn_body_routed`): the MIR walker owns all
334    /// runtime codegen after the HIR walker's deletion (W6/Stage-3).
335    /// `None` for hand-assembled test contexts that skip `build_context`.
336    pub mir_program: Option<crate::ir::mir::MirProgram>,
337    /// Per-`(FnId, LocalId)` bare-`i64` representation facts for the Int
338    /// "unboxing" optimization, computed from `mir_program` by
339    /// `bare_i64::analyze`. The Rust backend reads a per-fn slice at
340    /// signature + body emit to select native `i64` vs the default
341    /// `aver_rt::AverInt` for provably-bounded, non-escaping Int values.
342    /// Fail-closed: empty (all-`Boxed`) for hand-assembled test contexts
343    /// and for dependency-module fragments (callers unseen).
344    pub bare_i64: crate::ir::mir::BareI64Facts,
345    /// Kernel-proved lemmas parsed back from a committed
346    /// `DiscoveredLemmas.lean` (the `--discover` artifact), set by the CLI
347    /// on a normal `aver proof` run when the discovery-surface hash still
348    /// matches. The Lean backend embeds each pinned lemma's text before the
349    /// first law theorem that uses it (re-proving it in the same build) and
350    /// `simp`s over its name (`ProofStrategy::SimpOverLemmas`). Empty unless
351    /// the CLI wired it — discovery feedback is strictly opt-in.
352    pub discovered_lemmas: Vec<crate::codegen::lemma_discovery::CommittedLemma>,
353    /// VM-computed ground-truth values for verify cases, keyed by
354    /// `(common::verify_block_counter_key(vb), global_case_index)` →
355    /// `aver_repr_literal` rendering of the case's expected (right-side)
356    /// value. Set by the CLI on `aver proof --backend lean` from a Declared-
357    /// mode `aver verify` run over the entry items; empty everywhere else.
358    /// The Lean emitter literalizes the expected side of bounded sample
359    /// checks from this table (model-vs-ground-truth) so that fuel
360    /// exhaustion — where `panic!` returns `default` and a model-vs-model
361    /// equation becomes vacuously true under `native_decide` — cannot
362    /// kernel-certify a false equation. Entries exist only for cases that
363    /// PASSED `aver verify`; failing/skipped cases keep the source RHS.
364    pub sample_expected: std::collections::HashMap<(String, usize), String>,
365    /// `aver proof --allow-mathlib` (Lean only, opt-in): permit a generic
366    /// Mathlib break-glass closing arm on laws the core strategies cannot
367    /// claim. When `false` (the default) the Lean backend is BYTE-IDENTICAL to
368    /// before — no Mathlib import, no break-glass arm, same tiers. When `true`
369    /// a walling `when`-law is emitted in true-universal form with a domain-
370    /// blind Mathlib tactic portfolio (`aver_mathlib`, keyed on
371    /// `Int.ediv_ediv_of_nonneg` / `pow_add` / `positivity` / `nlinarith` / …)
372    /// under a `first | (trace "AVER_MATHLIB:fn.law"; …) | sorry` floor. The
373    /// post-emit step (`setup_mathlib_for_project`) wires the cached Mathlib
374    /// into the generated lake project; the build-log trace marker drives the
375    /// per-law `mathlib` credit. Axiom whitelist is UNCHANGED — Mathlib lemmas
376    /// are kernel-clean `{propext, Classical.choice, Quot.sound}`.
377    pub allow_mathlib: bool,
378    /// Hand-proof sidecars, keyed by `(fn_name, law_name)` → the proof BODY
379    /// (the tactic text after Lean's `:= by`, or the Dafny lemma body between
380    /// `{` and `}`). Loaded by the CLI from a project's source-controlled
381    /// `proofs/<lean|dafny>/<fn>__<law>.{lean,dfy}` sidecar dir for the active
382    /// backend; empty everywhere else. When an entry exists for a law, the
383    /// codegen splices the body into that law's emitted theorem/lemma and lets
384    /// the kernel (lake / dafny verify) re-check it — a WRONG body fails the
385    /// build loudly and the law is denied universal credit. A law with NO
386    /// sidecar is byte-identical to before. The genuinely-hard lemmas the
387    /// generic engine cannot find (trunc-sticky composition, sticky-plus) live
388    /// here as LABELED, kernel-checked hand proofs (manifest credit `hand`).
389    pub hand_proofs: std::collections::HashMap<(String, String), String>,
390}
391
392/// Output files from a codegen backend.
393pub struct ProjectOutput {
394    /// Files to write: (relative_path, content).
395    pub files: Vec<(String, String)>,
396}
397
398/// Build a CodegenContext from parsed + type-checked items.
399///
400/// `entry_analysis` is the `analyze` stage output for `items` (entry
401/// module). When provided, codegen reads `mutual_tco_members`,
402/// `recursive_fns`, and per-fn `FnAnalysis` from it instead of recomputing.
403/// Each `ModuleInfo` in `modules` carries its own per-module analysis;
404/// codegen unions the per-module sets to build a global view (sound
405/// under Aver's module DAG invariant — no cross-module SCCs possible,
406/// see `src/ir/analyze.rs` doc).
407///
408/// `symbol_table` is the resolved-identity layer built by the
409/// pipeline (`pipeline_result.symbol_table`). Always required:
410/// `pipeline::run` builds it unconditionally so every caller has
411/// one available. The ad-hoc test helpers that drive a stripped
412/// pipeline build their own via `SymbolTable::build(&items,
413/// &modules)` and pass it here.
414#[allow(clippy::too_many_arguments)]
415pub fn build_context(
416    items: Vec<TopLevel>,
417    _tc_result: &TypeCheckResult,
418    entry_analysis: Option<&crate::ir::AnalysisResult>,
419    project_name: String,
420    modules: Vec<ModuleInfo>,
421    symbol_table: crate::ir::SymbolTable,
422    resolved_items: Vec<crate::ir::hir::ResolvedTopLevel>,
423) -> CodegenContext {
424    let type_defs: Vec<TypeDef> = items
425        .iter()
426        .filter_map(|item| {
427            if let TopLevel::TypeDef(td) = item {
428                Some(td.clone())
429            } else {
430                None
431            }
432        })
433        .collect();
434
435    let fn_defs: Vec<FnDef> = items
436        .iter()
437        .filter_map(|item| {
438            if let TopLevel::FnDef(fd) = item {
439                Some(fd.clone())
440            } else {
441                None
442            }
443        })
444        .collect();
445
446    let module_prefixes: HashSet<String> = modules.iter().map(|m| m.prefix.clone()).collect();
447
448    // Mutual-TCO membership unions per-scope sets from the analyze
449    // stage (entry's `entry_analysis` + each dep module's
450    // `module.analysis`); falls back to recomputing per-scope via
451    // `call_graph::tailcall_scc_components` when no analysis ran.
452    // Aver's module DAG invariant guarantees SCCs never span
453    // modules — per-scope union is the correct global view (see
454    // `project_aver_module_dag` memory + `src/ir/analyze.rs`). The
455    // FnId resolution happens inside the `scc` wrappers below.
456    let mut mutual_tco_members: HashSet<crate::ir::FnId> = HashSet::new();
457    match entry_analysis {
458        Some(a) => mutual_tco_members.extend(scc::analysis_set_to_fn_ids(
459            &a.mutual_tco_members,
460            &symbol_table,
461            None,
462        )),
463        None => {
464            // No entry analysis: compute the per-scope SCC set inline
465            // via `call_graph` and project to FnIds. Same effect as
466            // running the analyze stage's mutual-TCO discovery.
467            // **syntax-discovery-only** (epic #170 Phase 8 guardrail):
468            // `entry_fns` is filtered from `fn_defs` — the entry-scope
469            // FnDef vec — so `FnKey::entry(&fd.name)` below is the
470            // correct keying by construction (every `fd` here is
471            // entry-scope).
472            let entry_fns: Vec<&FnDef> = fn_defs.iter().filter(|fd| fd.name != "main").collect();
473            for group in crate::call_graph::tailcall_scc_components(&entry_fns) {
474                if group.len() < 2 {
475                    continue;
476                }
477                for fd in group {
478                    if let Some(id) = symbol_table.fn_id_of(&crate::ir::FnKey::entry(&fd.name)) {
479                        mutual_tco_members.insert(id);
480                    }
481                }
482            }
483        }
484    }
485    for module in &modules {
486        match module.analysis.as_ref() {
487            Some(a) => mutual_tco_members.extend(scc::analysis_set_to_fn_ids(
488                &a.mutual_tco_members,
489                &symbol_table,
490                Some(&module.prefix),
491            )),
492            None => {
493                let mod_fns: Vec<&FnDef> = module.fn_defs.iter().collect();
494                for group in crate::call_graph::tailcall_scc_components(&mod_fns) {
495                    if group.len() < 2 {
496                        continue;
497                    }
498                    for fd in group {
499                        if let Some(id) = symbol_table.fn_id_of(&crate::ir::FnKey::in_module(
500                            module.prefix.clone(),
501                            &fd.name,
502                        )) {
503                            mutual_tco_members.insert(id);
504                        }
505                    }
506                }
507            }
508        }
509    }
510
511    // `recursive_fns` follows the same shape — per-scope union with
512    // analyze-stage fallback. Keyed by opaque `FnId` so entry +
513    // dep-module same-bare-name fns stay distinct.
514    let mut recursive_fns: HashSet<crate::ir::FnId> = HashSet::new();
515    match entry_analysis {
516        Some(a) => recursive_fns.extend(scc::analysis_set_to_fn_ids(
517            &a.recursive_fns,
518            &symbol_table,
519            None,
520        )),
521        None => recursive_fns.extend(scc::bare_names_to_fn_ids(
522            crate::call_graph::find_recursive_fns(&items)
523                .iter()
524                .map(String::as_str),
525            &symbol_table,
526            None,
527        )),
528    }
529    for module in &modules {
530        match module.analysis.as_ref() {
531            Some(a) => recursive_fns.extend(scc::analysis_set_to_fn_ids(
532                &a.recursive_fns,
533                &symbol_table,
534                Some(&module.prefix),
535            )),
536            None => {
537                let mod_items: Vec<TopLevel> = module
538                    .fn_defs
539                    .iter()
540                    .map(|fd| TopLevel::FnDef(fd.clone()))
541                    .collect();
542                recursive_fns.extend(scc::bare_names_to_fn_ids(
543                    crate::call_graph::find_recursive_fns(&mod_items)
544                        .iter()
545                        .map(String::as_str),
546                    &symbol_table,
547                    Some(&module.prefix),
548                ));
549            }
550        }
551    }
552
553    // Detection layer for buffer-build sinks + fusion sites. The
554    // ACTUAL rewrite + synthesis must happen BEFORE the resolver
555    // pass (callers run it via `ir::run_buffer_build_pass` between
556    // TCO and resolver) — the detector matches on `Expr::Ident`
557    // shapes that resolver later rewrites to `Expr::Resolved`. We
558    // rerun detection here against the final items so the resulting
559    // ctx fields reflect what's actually in the AST. With pre-
560    // resolver pass having already run, sinks/sites should be the
561    // same set (sinks are fns, not call sites; fusion sites were
562    // rewritten away so the post-rewrite count is zero in normal flow).
563    let detect_fns: Vec<&FnDef> = fn_defs
564        .iter()
565        .chain(modules.iter().flat_map(|m| m.fn_defs.iter()))
566        .collect();
567    let buffer_build_sinks = crate::ir::compute_buffer_build_sinks(&detect_fns);
568    let buffer_fusion_sites = crate::ir::find_fusion_sites(&detect_fns, &buffer_build_sinks);
569    // The synthesizer already ran in the pre-resolver compile pass
570    // (`ir::run_buffer_build_pass`); the resulting `<fn>__buffered`
571    // variants live in `items` (or in dep `module.fn_defs`) directly,
572    // so we just collect references for the ctx field instead of
573    // re-synthesizing — re-running here would duplicate every fn
574    // and confuse the WASM emitter's fn_indices table.
575    let synthesized_buffered_fns: Vec<FnDef> = fn_defs
576        .iter()
577        .chain(modules.iter().flat_map(|m| m.fn_defs.iter()))
578        .filter(|fd| fd.name.ends_with("__buffered"))
579        .cloned()
580        .collect();
581
582    // Epic #170 Phase 1: build the canonical `ResolvedProgramView`
583    // once, from the pipeline's already-resolved entry items + the
584    // dep modules' AST fn defs. The view does the module-side
585    // resolution (pinning `ResolveCtx.current_module = Some(prefix)`)
586    // — that's the only producer in the codebase. `resolved_fn_defs`
587    // / `resolved_module_fn_defs` then project FROM the view rather
588    // than running an independent second resolve, eliminating the
589    // "two truths" hazard build_context carried since PR 9.
590    let resolved_program = crate::codegen::program_view::ResolvedProgramView::build(
591        resolved_items,
592        &modules,
593        &symbol_table,
594    );
595    let resolved_fn_defs: Vec<crate::ir::hir::ResolvedFnDef> =
596        resolved_program.entry_fns().cloned().collect();
597    let resolved_module_fn_defs: Vec<Vec<crate::ir::hir::ResolvedFnDef>> = resolved_program
598        .modules
599        .iter()
600        .map(|m| m.fn_defs.clone())
601        .collect();
602
603    // Compute program shape before moving items / modules into ctx.
604    // Once-per-compilation analysis substrate (#232 stage 4+); ad-hoc
605    // detectors in codegen (e.g. dafny's `is_directly_recursive`,
606    // future stage 6 adapters for `refinement_info_for`) read from
607    // this instead of rewalking the AST.
608    let program_shape = {
609        let mut all_fns: Vec<&crate::ir::hir::ResolvedFnDef> =
610            resolved_program.entry_fns().collect();
611        for m in &resolved_program.modules {
612            for fd in &m.fn_defs {
613                all_fns.push(fd);
614            }
615        }
616        Some(crate::analysis::shape::analyze_program_with_modules(
617            &all_fns, &items, &modules,
618        ))
619    };
620
621    // Lower the whole resolved program (entry + dep-module fns) to
622    // optimized Core MIR, once, and key it by `FnId`. The Rust
623    // backend reads `fn_by_id` here to render every fn body (its sole
624    // codegen path); building it here (rather than per-fn) keeps the
625    // lowering cost O(program) instead of O(program²). Same
626    // `lower_program` → `optimize` pass the other MIR backends run.
627    let mir_program = {
628        let mut mir_items: Vec<crate::ir::hir::ResolvedTopLevel> = resolved_program
629            .entry_fns()
630            .cloned()
631            .map(crate::ir::hir::ResolvedTopLevel::FnDef)
632            .collect();
633        for m in &resolved_program.modules {
634            for fd in &m.fn_defs {
635                mir_items.push(crate::ir::hir::ResolvedTopLevel::FnDef(fd.clone()));
636            }
637        }
638        Some(crate::ir::mir::optimize(crate::ir::mir::lower_program(
639            &mir_items,
640        )))
641    };
642
643    // Int "unboxing": derive the per-(FnId, LocalId) bare-`i64`
644    // representation facts from the optimized MIR. Read-only — never
645    // mutates the program. Empty (all-`Boxed`) when there is no MIR
646    // (defensive) or for fragments the analysis bails on.
647    //
648    // ETAP-2 SLICE 0+1: this facts path (the VM-side / general read) passes
649    // an EMPTY carrier table, so no carrier slot lowers here — byte-identical
650    // to the pre-slice behavior. Carrier lowering is opt-in at the two
651    // codegen-rewrite entries (`rewrite_for_rust` / `rewrite_for_wasm_gc`),
652    // which build the real table from the refinement-via-opaque inputs.
653    let empty_carrier = crate::ir::mir::bare_i64::CarrierIntervals::new();
654    let bare_i64 = mir_program
655        .as_ref()
656        .map(|p| crate::ir::mir::bare_i64::analyze(p, &empty_carrier))
657        .unwrap_or_default();
658
659    let ctx = CodegenContext {
660        items,
661        type_defs,
662        fn_defs,
663        project_name,
664        modules,
665        module_prefixes,
666        #[cfg(feature = "runtime")]
667        policy: None,
668        emit_replay_runtime: false,
669        runtime_policy_from_env: false,
670        guest_entry: None,
671        emit_self_host_support: false,
672        extra_fn_defs: Vec::new(),
673        mutual_tco_members,
674        recursive_fns,
675        buffer_build_sinks,
676        buffer_fusion_sites,
677        synthesized_buffered_fns,
678        bare_i64,
679        #[cfg(feature = "runtime")]
680        proof_ir: crate::ir::ProofIR::default(),
681        // Symbol table threaded through from the pipeline (or
682        // built locally in fallback). The FnId-keyed `recursive_
683        // fns` / `mutual_tco_members` above used it; backends
684        // (proof_lower / Lean / Rust / Dafny) read it directly off
685        // ctx for opaque-ID lookups.
686        symbol_table,
687        resolved_fn_defs,
688        resolved_module_fn_defs,
689        current_module_scope: std::cell::RefCell::new(None),
690        program_shape,
691        resolved_program,
692        mir_program,
693        discovered_lemmas: Vec::new(),
694        sample_expected: std::collections::HashMap::new(),
695        allow_mathlib: false,
696        hand_proofs: std::collections::HashMap::new(),
697    };
698    // ProofIR no longer populated here. Pipeline owns the lowerings
699    // (`PipelineStage::RefinementLower`, `PipelineStage::ContractLower`);
700    // proof backends opt in via `PipelineConfig.run_refinement_lower` /
701    // `run_contract_lower` and read `pipeline_result.proof_ir` back.
702    // Runtime backends (VM / WASM / Rust) leave both off and skip the
703    // work. Tests that bypass the pipeline assemble the ctx by hand
704    // and call `refresh_facts()` to populate the field — the field
705    // stays `default()` here for those callers until they explicitly
706    // refresh.
707    ctx
708}
709
710impl CodegenContext {
711    /// Set `current_module_scope` for the duration of `f`. Backends
712    /// wrap their per-module emit calls with this so legacy
713    /// resolve-on-demand adapters see the correct prefix.
714    pub fn with_module_scope<R>(&self, scope: Option<&str>, f: impl FnOnce() -> R) -> R {
715        let prev = self
716            .current_module_scope
717            .replace(scope.map(|s| s.to_string()));
718        let out = f();
719        *self.current_module_scope.borrow_mut() = prev;
720        out
721    }
722
723    /// Snapshot of the active module scope. Cloned so callers may
724    /// pass `as_deref()` into resolver/emitter APIs without holding
725    /// the `RefCell` borrow.
726    pub fn active_module_scope(&self) -> Option<String> {
727        self.current_module_scope.borrow().clone()
728    }
729
730    /// Resolve a verify-law's target fn name to its [`FnId`] under the
731    /// active module scope: `FnKey::in_module(scope, name)` when a dep
732    /// module is in scope (cross-file law pool — the dep law's
733    /// `LawTheorem` is keyed by its dep-scope id), else
734    /// `FnKey::entry(name)`. The entry key is tried as a fallback so an
735    /// entry law emitted with a stale scope still resolves. Drives the
736    /// `proof_ir.law_theorems` strategy lookup so a dep law gets the
737    /// SAME auto-proof strategy an entry law of that shape would.
738    pub fn law_target_fn_id(&self, fn_name: &str) -> Option<crate::ir::FnId> {
739        if let Some(scope) = self.active_module_scope()
740            && let Some(id) = self
741                .symbol_table
742                .fn_id_of(&crate::ir::FnKey::in_module(scope, fn_name))
743        {
744            return Some(id);
745        }
746        self.symbol_table
747            .fn_id_of(&crate::ir::FnKey::entry(fn_name))
748    }
749
750    /// Identity-keyed lookup from a bare fn name + scope to the
751    /// matching `&FnDef` in `fn_defs` / `modules[i].fn_defs`. Resolves
752    /// the name through the symbol table to an `FnId` first, then
753    /// recovers the AST `FnDef` via `fn_id_for_decl` pointer-eq scope
754    /// matching — so two same-bare-name fns across modules can't
755    /// cross-resolve.
756    ///
757    /// **Epic #170 Phase 5 helper.** Replaces the
758    /// `ctx.fn_defs.iter().find(|fd| fd.name == name)` pattern that
759    /// proof-mode law / verify rewriters used pre-migration. Backends
760    /// that still need a `&FnDef` (rather than the resolved twin —
761    /// e.g. `rewrite_effectful_calls_in_law` consumes AST shape)
762    /// reach this method instead of walking by bare name.
763    ///
764    /// Returns `None` when the symbol table doesn't know the name
765    /// under the given scope, or when the resolved `FnId` doesn't
766    /// match any `&FnDef` in that scope (synthetic FnDefs added
767    /// post-pipeline fall through here — callers can fallback to a
768    /// bare-name walk over `extra_fn_defs` etc. when that matters).
769    pub fn fn_def_by_name(&self, name: &str, scope: Option<&str>) -> Option<&FnDef> {
770        use crate::ir::FnKey;
771        let key = match scope {
772            Some(prefix) => FnKey::in_module(prefix.to_string(), name),
773            None => FnKey::entry(name),
774        };
775        let fn_id = self.symbol_table.fn_id_of(&key)?;
776        let matches = |fd: &&FnDef| crate::codegen::common::fn_id_for_decl(self, fd) == Some(fn_id);
777        match scope {
778            None => self.fn_defs.iter().find(matches),
779            Some(prefix) => self
780                .modules
781                .iter()
782                .find(|m| m.prefix == prefix)?
783                .fn_defs
784                .iter()
785                .find(matches),
786        }
787    }
788}
789
790impl CodegenContext {
791    /// Test-only bridge: recompute every derived fact
792    /// (`mutual_tco_members`, `recursive_fns`, `proof_ir`,
793    /// `resolved_program`) from the current `items` and `modules`.
794    /// Used exclusively by unit tests that construct a
795    /// `CodegenContext` piecewise — pushing synthetic `FnDef`s
796    /// straight into the items list rather than going through the
797    /// parser and pipeline. Production code never needs this: every
798    /// derived fact is populated by the pipeline stages (analyze,
799    /// proof_lower) and propagated through `build_context`. Calling
800    /// `refresh_facts` on a production-built ctx is redundant work
801    /// that produces the same answer — leave it off the hot path.
802    ///
803    /// **Single-source-of-truth invariant** (epic #170 Phase 1+2):
804    /// rebuilds `resolved_program` once from the freshly-resolved
805    /// items, then derives `resolved_fn_defs` /
806    /// `resolved_module_fn_defs` as projections of that view. There
807    /// is no parallel resolve path here.
808    pub fn refresh_facts(&mut self) {
809        // Synthetic-ctx path must own its symbol table too — FnId-
810        // keyed sets below resolve through it, same shape as the
811        // production `build_context` flow.
812        let symbol_table = crate::ir::SymbolTable::build(&self.items, &self.modules);
813        let entry_fn_id = |name: &str| -> Option<crate::ir::FnId> {
814            symbol_table.fn_id_of(&crate::ir::FnKey::entry(name))
815        };
816        let module_fn_id = |prefix: &str, name: &str| -> Option<crate::ir::FnId> {
817            symbol_table.fn_id_of(&crate::ir::FnKey::in_module(prefix.to_string(), name))
818        };
819
820        let entry_fn_refs: Vec<&FnDef> =
821            self.fn_defs.iter().filter(|fd| fd.name != "main").collect();
822
823        let mut mutual_tco_members: HashSet<crate::ir::FnId> = HashSet::new();
824        for group in crate::call_graph::tailcall_scc_components(&entry_fn_refs) {
825            if group.len() < 2 {
826                continue;
827            }
828            for fd in group {
829                if let Some(id) = entry_fn_id(&fd.name) {
830                    mutual_tco_members.insert(id);
831                }
832            }
833        }
834        for module in &self.modules {
835            let mod_fns: Vec<&FnDef> = module.fn_defs.iter().collect();
836            for group in crate::call_graph::tailcall_scc_components(&mod_fns) {
837                if group.len() < 2 {
838                    continue;
839                }
840                for fd in group {
841                    if let Some(id) = module_fn_id(&module.prefix, &fd.name) {
842                        mutual_tco_members.insert(id);
843                    }
844                }
845            }
846        }
847        self.mutual_tco_members = mutual_tco_members;
848
849        let mut recursive_fns: HashSet<crate::ir::FnId> = scc::bare_names_to_fn_ids(
850            crate::call_graph::find_recursive_fns(&self.items)
851                .iter()
852                .map(String::as_str),
853            &symbol_table,
854            None,
855        );
856        for module in &self.modules {
857            let mod_items: Vec<TopLevel> = module
858                .fn_defs
859                .iter()
860                .map(|fd| TopLevel::FnDef(fd.clone()))
861                .collect();
862            recursive_fns.extend(scc::bare_names_to_fn_ids(
863                crate::call_graph::find_recursive_fns(&mod_items)
864                    .iter()
865                    .map(String::as_str),
866                &symbol_table,
867                Some(&module.prefix),
868            ));
869        }
870        self.recursive_fns = recursive_fns;
871
872        // Reuse the symbol table built at the top of this function
873        // for proof_lower below — it already resolved every FnId we
874        // need for `recursive_fns` / `mutual_tco_members`.
875        self.symbol_table = symbol_table;
876
877        // Rebuild the canonical resolved view from the current items
878        // + modules (post-PR-A: this is the single source for resolved
879        // bodies). Entry-side resolved items are produced by
880        // `resolve_program`, then the view runs the per-dep-module
881        // resolve internally and indexes everything by `FnId`. The
882        // `resolved_fn_defs` / `resolved_module_fn_defs` mirrors below
883        // are projections of this view, kept for callsites that still
884        // walk them directly during the #170 backend-migration arc.
885        let entry_resolved_items = crate::ir::hir::resolve_program(&self.symbol_table, &self.items);
886        self.resolved_program = crate::codegen::program_view::ResolvedProgramView::build(
887            entry_resolved_items,
888            &self.modules,
889            &self.symbol_table,
890        );
891        self.resolved_fn_defs = self.resolved_program.entry_fns().cloned().collect();
892        self.resolved_module_fn_defs = self
893            .resolved_program
894            .modules
895            .iter()
896            .map(|m| m.fn_defs.clone())
897            .collect();
898
899        // ProofIR's `fn_contracts` / `refined_types` are derived from
900        // the just-recomputed item set + the recursion classifier, so
901        // they must stay in step with the rest of the facts. Test
902        // helpers that build the context piecewise and call
903        // `refresh_facts` rely on this to see the same proof decisions
904        // the production pipeline would emit.
905        let inputs = crate::codegen::proof_lower::ProofLowerInputs::from_ctx(self);
906        self.proof_ir = crate::codegen::proof_lower::lower(&inputs);
907    }
908
909    /// Look up the resolved-HIR mirror of a source-shape [`FnDef`]
910    /// previously stashed in [`resolved_fn_defs`] /
911    /// [`resolved_module_fn_defs`]. Falls back to a fresh per-call
912    /// resolver lift against the entry's [`crate::ir::SymbolTable`]
913    /// when neither path covers `fd` — this happens for synthetic
914    /// FnDefs inserted between `build_context` and emit (TCO hoist
915    /// rewrites, test fixtures) which the resolver hasn't lifted
916    /// upfront.
917    ///
918    /// `scope` is the owning module prefix when `fd` came from a
919    /// dependency module's `module.fn_defs`, `None` when `fd` is part
920    /// of the entry's `ctx.fn_defs`. Lookup keys by
921    /// [`crate::ir::FnKey`] through the [`crate::ir::SymbolTable`] so
922    /// two modules that share a bare fn name (e.g. `Util.format` and
923    /// `Other.format`) resolve to their own [`crate::ir::FnId`]
924    /// without bare-name collisions. Pre-PR-9.3a this matched by
925    /// `rfd.name == fd.name` against a flat search of every resolved
926    /// table — fragile the moment flatten changes (or doesn't run)
927    /// and two scopes share a name.
928    ///
929    /// Phase E shared lookup boundary — Rust codegen (PR 8) already
930    /// consumes this through `rust::toplevel::resolved_fn_def_for`;
931    /// wasm-gc / Lean / Dafny / self-host backends pick it up in
932    /// their follow-up PRs.
933    ///
934    /// [`resolved_fn_defs`]: Self::resolved_fn_defs
935    /// [`resolved_module_fn_defs`]: Self::resolved_module_fn_defs
936    pub fn resolve_fn_def<'a>(
937        &'a self,
938        fd: &'a FnDef,
939        scope: Option<&str>,
940    ) -> std::borrow::Cow<'a, crate::ir::hir::ResolvedFnDef> {
941        use crate::ir::FnKey;
942        use crate::ir::hir::{
943            ResolveCtx, ResolvedFnBody, ResolvedFnDef, ResolvedStmt, resolve_fn_def_external,
944        };
945        use std::borrow::Cow;
946
947        // Resolve identity via the symbol table — entry scope vs
948        // dependency module scope is the caller's stated context.
949        let key = match scope {
950            Some(prefix) => FnKey::in_module(prefix.to_string(), fd.name.clone()),
951            None => FnKey::entry(fd.name.clone()),
952        };
953        if let Some(fn_id) = self.symbol_table.fn_id_of(&key) {
954            // Canonical lookup goes through the resolved-program view —
955            // its `fn_by_id` index is the single FnId-keyed source for
956            // the resolved body, replacing the dual-walk over
957            // `resolved_fn_defs` + `resolved_module_fn_defs` that
958            // predated #170 Phase 1.
959            if let Some(rfd) = self.resolved_program.fn_by_id(fn_id) {
960                return Cow::Borrowed(rfd);
961            }
962            // Symbol table knew the key but the view didn't index it.
963            // Falls through to the synthetic-fallback path below; in
964            // production this shouldn't happen.
965        }
966
967        // Synthetic FnDef path — TCO hoist rewrites, test fixtures
968        // the resolver never saw. Lift on demand against the entry's
969        // resolver context.
970        let module_name = self.items.iter().find_map(|i| match i {
971            TopLevel::Module(m) => Some(m.name.clone()),
972            _ => None,
973        });
974        let mut rctx = ResolveCtx::new(&self.symbol_table);
975        rctx.current_module = scope.map(String::from).or(module_name);
976        let lifted = resolve_fn_def_external(&rctx, fd).unwrap_or_else(|| {
977            let stmts: Vec<ResolvedStmt> = match fd.body.as_ref() {
978                crate::ast::FnBody::Block(stmts) => {
979                    stmts.iter().map(|s| self.resolve_stmt(s, scope)).collect()
980                }
981            };
982            ResolvedFnDef {
983                fn_id: crate::ir::FnId(u32::MAX),
984                name: fd.name.clone(),
985                line: fd.line,
986                params: fd
987                    .params
988                    .iter()
989                    .map(|(n, ann)| (n.clone(), crate::types::parse_type_str(ann)))
990                    .collect(),
991                return_type: crate::types::parse_type_str(&fd.return_type),
992                effects: fd.effects.clone(),
993                desc: fd.desc.clone(),
994                body: std::sync::Arc::new(ResolvedFnBody::Block(stmts)),
995                resolution: fd.resolution.clone(),
996            }
997        });
998        Cow::Owned(lifted)
999    }
1000
1001    /// Entry module's name from `items` (the `module X` declaration's
1002    /// X). `None` for ad-hoc test programs without a module decl.
1003    fn entry_module_name(&self) -> Option<String> {
1004        self.items.iter().find_map(|i| match i {
1005            TopLevel::Module(m) => Some(m.name.clone()),
1006            _ => None,
1007        })
1008    }
1009
1010    /// Resolve a source-shape `Spanned<Expr>` on demand using the
1011    /// entry's resolver context. Used by emit helpers that still walk
1012    /// `Expr` (TCO hoisting, mutual TCO, verify blocks, follow-up
1013    /// backends pre-migration) and need to feed the resolved shape
1014    /// into the migrated emitter. The returned `Spanned<ResolvedExpr>`
1015    /// carries the same line + type stamp as the input.
1016    ///
1017    /// `scope` is the owning module prefix when the caller knows
1018    /// which dep module the expression lives in, `None` for entry-
1019    /// scope code. Required for cross-module name resolution — e.g.,
1020    /// a call site in module `A` referring to `Val.ValOk` declared
1021    /// in module `B` only resolves to `ResolvedCtor::User` when the
1022    /// resolver's `current_module` matches the call site's owning
1023    /// scope. Pre-PR-9.4 the helper used the *entry* module name
1024    /// uniformly, which broke cross-module ctor / fn classification
1025    /// for the legacy emit paths (mutual TCO trampolines, TCO hoist
1026    /// — they walked dep-module fn bodies but the resolver context
1027    /// said "you're in the entry module"; the self-host regen
1028    /// surfaced the gap when same-name shadowing across modules was
1029    /// no longer an option).
1030    pub fn resolve_expr(
1031        &self,
1032        expr: &crate::ast::Spanned<crate::ast::Expr>,
1033        scope: Option<&str>,
1034    ) -> crate::ast::Spanned<crate::ir::hir::ResolvedExpr> {
1035        use crate::ir::hir::{ResolveCtx, ResolvedStmt};
1036        let mut rctx = ResolveCtx::new(&self.symbol_table);
1037        rctx.current_module = scope.map(String::from).or_else(|| self.entry_module_name());
1038        let stmt = crate::ast::Stmt::Expr(expr.clone());
1039        match crate::ir::hir::resolve::resolve_stmt_external(&rctx, &stmt) {
1040            ResolvedStmt::Expr(s) => s,
1041            ResolvedStmt::Binding { value, .. } => value,
1042        }
1043    }
1044
1045    /// Same as [`Self::resolve_expr`] but for whole statements
1046    /// (`Binding(name, ty_ann, expr)` or `Expr(expr)`).
1047    pub fn resolve_stmt(
1048        &self,
1049        stmt: &crate::ast::Stmt,
1050        scope: Option<&str>,
1051    ) -> crate::ir::hir::ResolvedStmt {
1052        use crate::ir::hir::ResolveCtx;
1053        let mut rctx = ResolveCtx::new(&self.symbol_table);
1054        rctx.current_module = scope.map(String::from).or_else(|| self.entry_module_name());
1055        crate::ir::hir::resolve::resolve_stmt_external(&rctx, stmt)
1056    }
1057
1058    /// Resolve a source-shape [`crate::ast::Pattern`] to its resolved
1059    /// HIR form. Wraps the pattern in a synthetic match arm + drops
1060    /// it through `resolve_stmt_external`, since the resolver doesn't
1061    /// expose a standalone pattern lifter — same workaround
1062    /// `rust/toplevel.rs` used pre-PR-9.
1063    pub fn resolve_pattern(
1064        &self,
1065        pat: &crate::ast::Pattern,
1066        scope: Option<&str>,
1067    ) -> crate::ir::hir::ResolvedPattern {
1068        use crate::ast::{Expr, Literal, MatchArm, Spanned, Stmt};
1069        use crate::ir::hir::{ResolveCtx, ResolvedExpr, ResolvedStmt};
1070        let mut rctx = ResolveCtx::new(&self.symbol_table);
1071        rctx.current_module = scope.map(String::from).or_else(|| self.entry_module_name());
1072        let synthetic_arm = MatchArm {
1073            pattern: pat.clone(),
1074            body: Box::new(Spanned::bare(Expr::Literal(Literal::Unit))),
1075            binding_slots: std::sync::OnceLock::new(),
1076        };
1077        let stmt = Stmt::Expr(Spanned::bare(Expr::Match {
1078            subject: Box::new(Spanned::bare(Expr::Literal(Literal::Unit))),
1079            arms: vec![synthetic_arm],
1080        }));
1081        let resolved_stmt = crate::ir::hir::resolve::resolve_stmt_external(&rctx, &stmt);
1082        let ResolvedStmt::Expr(spanned) = resolved_stmt else {
1083            unreachable!()
1084        };
1085        let ResolvedExpr::Match { arms, .. } = spanned.node else {
1086            unreachable!()
1087        };
1088        arms.into_iter().next().unwrap().pattern
1089    }
1090}
1091
1092/// Per-key projection of the legacy `fn_sigs` map: routes a source-
1093/// level name through `resolved_program` first (entry + every dep
1094/// module's resolved fns), then walks `TypeDef`s for constructor sigs,
1095/// then handles the synthesised `__buf_*` intrinsics. Lets a
1096/// `CodegenContext` answer `FnSigOracle::fn_sig` without materialising
1097/// the whole `FnSigMap` up front — the verify-law helpers query
1098/// individual names, so per-key resolution is cheaper than per-call
1099/// rebuild.
1100fn codegen_ctx_fn_sig(ctx: &CodegenContext, name: &str) -> Option<crate::verify_law::FnSigInfo> {
1101    use crate::verify_law::FnSigInfo;
1102
1103    if let Some(fn_id) = crate::codegen::common::fn_id_for_dotted_name(ctx, name)
1104        && let Some(rfd) = ctx.resolved_program.fn_by_id(fn_id)
1105    {
1106        return Some(FnSigInfo {
1107            return_type: rfd.return_type.clone(),
1108            is_pure: rfd.effects.is_empty(),
1109        });
1110    }
1111
1112    // Constructor lookup: `Type.Variant` (entry sum), `Module.Type.
1113    // Variant` (module sum), `Box` (entry product), `Module.Box`
1114    // (module product). Walks the same `TypeDef` surfaces the
1115    // legacy fn_sigs population did via SymbolRegistry.
1116    let walk = |td: &crate::ast::TypeDef, scope: Option<&str>| -> Option<FnSigInfo> {
1117        match td {
1118            crate::ast::TypeDef::Sum {
1119                name: parent,
1120                variants,
1121                ..
1122            } => {
1123                let parent_full = match scope {
1124                    Some(prefix) => format!("{prefix}.{parent}"),
1125                    None => parent.clone(),
1126                };
1127                for v in variants {
1128                    let bare = format!("{parent}.{}", v.name);
1129                    let full = format!("{parent_full}.{}", v.name);
1130                    if name == bare || name == full {
1131                        return Some(FnSigInfo {
1132                            return_type: crate::types::Type::named(parent_full.clone()),
1133                            is_pure: true,
1134                        });
1135                    }
1136                }
1137                None
1138            }
1139            crate::ast::TypeDef::Product { name: parent, .. } => {
1140                let parent_full = match scope {
1141                    Some(prefix) => format!("{prefix}.{parent}"),
1142                    None => parent.clone(),
1143                };
1144                if name == parent || name == parent_full {
1145                    return Some(FnSigInfo {
1146                        return_type: crate::types::Type::named(parent_full),
1147                        is_pure: true,
1148                    });
1149                }
1150                None
1151            }
1152        }
1153    };
1154    for item in &ctx.items {
1155        if let TopLevel::TypeDef(td) = item
1156            && let Some(info) = walk(td, None)
1157        {
1158            return Some(info);
1159        }
1160    }
1161    for m in &ctx.modules {
1162        for td in &m.type_defs {
1163            if let Some(info) = walk(td, Some(&m.prefix)) {
1164                return Some(info);
1165            }
1166        }
1167    }
1168
1169    // Synthesised `__buf_*` intrinsics — the deforestation pipeline
1170    // emits these as opaque callables; verify-law walkers may surface
1171    // a reference if a user's law body sketches the buffer pipeline.
1172    match name {
1173        "__buf_new" => Some(FnSigInfo {
1174            return_type: crate::types::Type::named("Buffer"),
1175            is_pure: true,
1176        }),
1177        "__buf_append" | "__buf_append_sep_unless_first" => Some(FnSigInfo {
1178            return_type: crate::types::Type::named("Buffer"),
1179            is_pure: true,
1180        }),
1181        "__buf_finalize" => Some(FnSigInfo {
1182            return_type: crate::types::Type::Str,
1183            is_pure: true,
1184        }),
1185        _ => None,
1186    }
1187}
1188
1189impl crate::verify_law::FnSigOracle for CodegenContext {
1190    fn fn_sig(&self, name: &str) -> Option<crate::verify_law::FnSigInfo> {
1191        codegen_ctx_fn_sig(self, name)
1192    }
1193}