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