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