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