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}