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