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