aver/codegen/program_view.rs
1//! Canonical resolved view of the program for codegen consumers.
2//!
3//! Single source of truth for "what does the program look like, after
4//! name resolution, with module/entry scope preserved". Backends that
5//! used to walk `CodegenContext.{items, fn_defs, type_defs}` (raw
6//! `ast::*`) should iterate this view's resolved fn defs instead. The
7//! AST-shape fields on `CodegenContext` remain available as source
8//! metadata (spans, diagnostics, syntax-discovery), not as the
9//! authoritative backend input.
10//!
11//! # Current Typed HIR boundary (post epic #180)
12//!
13//! Aver's **Typed HIR** is a query layer, not a duplicate IR enum. Its
14//! two structural primitives — what backends mean when they reach for
15//! "the typed program" — are:
16//!
17//! 1. [`ResolvedProgramView`] (this struct). The canonical post-name-
18//! resolution program: every `FnDef` projected through
19//! [`crate::ir::hir::ResolvedFnDef`] with typed
20//! `params: Vec<(String, Type)>` and `return_type: Type`, indexed
21//! by opaque [`crate::ir::FnId`]. Backends ask
22//! "what does this fn look like?" through `fn_by_id` or
23//! `entry_fns()` / `module_fns(prefix)` and get typed answers.
24//! 2. Stamped [`crate::ast::Spanned`]`<`[`crate::ir::hir::ResolvedExpr`]`>`.
25//! Every reachable body expression in a well-typed program has
26//! `.ty().is_some()` after the typechecker stamps and the
27//! resolver propagates. Backends read inferred types per node
28//! without re-running inference.
29//!
30//! Together they answer "what name does this refer to?" (resolved
31//! HIR via `FnId` / `TypeId` / `CtorId`) and "what type does this
32//! expression have?" (typed slot via `Spanned::ty()`).
33//!
34//! Conscious exceptions still walking `ast::*` directly (each
35//! documented at the call site):
36//!
37//! - **`verify_law` helper walkers** ([`crate::verify_law`]). The
38//! helper-law / contextual-helper hint walkers operate on
39//! entry-only AST `FnDef`s through [`crate::verify_law::EntryFnIndex`].
40//! The parser invariant (`verify <name>` only accepts a single
41//! `Ident`) keeps the identity-safe contract; migration to FnId
42//! keying is gated on module-scoped verify shipping.
43//! - **Lean / Dafny proof + law spec emitters**. Several recognisers
44//! (`emit_*_spec_equivalence_law`, `direct_call` AST shape match)
45//! keep walking `Spanned<Expr>` source-shape because the proof IR
46//! is keyed on syntactic surfaces the user wrote, not the resolver-
47//! canonicalised form. `emit_expr_legacy` is the explicit adapter.
48//! - **wasm-gc post-link view** ([`crate::codegen::wasm_gc::view`]).
49//! The wasm-gc backend collapses N modules into one emit unit via
50//! `flatten_multimodule`, then rebuilds its own
51//! [`crate::ir::SymbolTable`] + resolver pass over the flattened
52//! slice. The post-link namespace is the identity layer in that
53//! backend; the documented `backend-link-stage` category covers it.
54//!
55//! Trigger-driven follow-up (NOT scoped to #180):
56//!
57//! - **MIR / Core IR**. Per-expression effects, SSA / CFG / explicit
58//! ownership / effect tracking per instruction belong to a separate
59//! epic. Typed HIR is the substrate they lower from; build the IR
60//! when an inliner / monomorphiser / cross-scope optimiser needs it.
61//! - **`TypedProgramView` wrapper**. Combining `resolved_program`'s
62//! `FnId` index with the typed expression slot into one query
63//! surface (`view.fn_by_id(id).body_expr_type(span)` or similar)
64//! would tighten the contract but adds no semantic value today —
65//! wait for a real consumer.
66//! - **Module-scoped verify → FnId keyed `verify_law`**. The
67//! `EntryFnIndex` newtype is the tripwire that forces the
68//! contributor to address keying when the parser starts accepting
69//! dotted verify targets.
70//!
71//! ## Construction
72//!
73//! Built once at the codegen boundary:
74//! - **Entry slice** projects from `PipelineResult.resolved_items` —
75//! the resolver pass populates this unconditionally, so we just
76//! pick out the `ResolvedTopLevel::FnDef` variants and clone the
77//! resolved fn defs out of them. No re-resolve.
78//! - **Module slice** resolves each dep module's `&[FnDef]` through
79//! `resolve_fn_def_external` under a `ResolveCtx` pinned to that
80//! module's prefix. This is the only producer of resolved module
81//! bodies — `CodegenContext.resolved_module_fn_defs` is a
82//! projection / cache of this view, not an independent source.
83//!
84//! ## Lookups
85//!
86//! The view exposes `fn_by_id(FnId)` so callsites that have an opaque
87//! `FnId` in hand (typical for identity-sensitive emit) can recover
88//! the resolved body without round-tripping through bare names.
89//! Iteration is provided per-scope: `entry_fns()` walks the entry
90//! slice; `module_fns(prefix)` walks one dep module's slice.
91//!
92//! Epic #170 Phase 1: this is the foundation every later phase
93//! builds on. Backends migrate to consuming the view as primary
94//! input in subsequent PRs; this PR only consolidates the producer
95//! side without touching backend signatures.
96
97use std::collections::HashMap;
98
99use crate::codegen::ModuleInfo;
100use crate::ir::SymbolTable;
101use crate::ir::hir::{ResolveCtx, ResolvedFnDef, ResolvedTopLevel, resolve_fn_def_external};
102
103/// Resolved-form mirror of one dep module's fn defs, with the
104/// module's prefix preserved so per-scope iteration is cheap and the
105/// view can build an `FnId`-keyed lookup without losing scope.
106#[derive(Debug, Clone, Default)]
107pub struct ResolvedModuleFns {
108 /// Module prefix (e.g. `"Models.User"`). Same value as
109 /// `ModuleInfo.prefix` on the AST side.
110 pub prefix: String,
111 /// Resolved fn defs in module source order — position-aligned
112 /// with `ModuleInfo.fn_defs` for the rare consumer that needs
113 /// to pair AST and resolved forms side-by-side.
114 pub fn_defs: Vec<ResolvedFnDef>,
115}
116
117/// Canonical resolved-program view consumed by codegen.
118///
119/// Holds the entry-scope items (post-pipeline, post-NameResolve) plus
120/// each dep module's resolved fn defs, with an `FnId` index so
121/// identity-keyed lookups don't walk linearly.
122#[derive(Debug, Clone, Default)]
123pub struct ResolvedProgramView {
124 /// Entry-scope resolved items, exactly as the pipeline produced
125 /// them. Includes `FnDef`, `Module` markers, and `Passthrough`
126 /// variants (verify/decision/typedef nodes that haven't been
127 /// promoted to resolved form yet — see [`ResolvedTopLevel`]).
128 pub entry_items: Vec<ResolvedTopLevel>,
129 /// Per-dep-module resolved fn defs. Order matches the input
130 /// `Vec<ModuleInfo>` so existing position-keyed consumers still
131 /// work during migration.
132 pub modules: Vec<ResolvedModuleFns>,
133 /// `FnId → (scope, ResolvedFnDef index)` index. The opaque
134 /// `FnId` keys (built by `SymbolTable` at pipeline head) are the
135 /// canonical identity primitive — every consumer that has an
136 /// `FnId` in hand should reach this map instead of bare-name
137 /// matching against `entry_items`.
138 fn_index: HashMap<crate::ir::FnId, FnIndexEntry>,
139}
140
141#[derive(Debug, Clone, Copy)]
142struct FnIndexEntry {
143 /// `None` = entry scope; `Some(i)` = `modules[i]`.
144 module: Option<usize>,
145 /// Position inside the owning slice's `fn_defs`.
146 pos: usize,
147}
148
149impl ResolvedProgramView {
150 /// Build the view from a pipeline-produced entry slice and the
151 /// `Vec<ModuleInfo>` carrying dep modules' AST fn defs. The
152 /// caller has already run `pipeline::run` (or equivalent) and is
153 /// passing through the canonical `resolved_items` — we do not
154 /// re-resolve the entry side. Module fn defs are resolved here
155 /// (the pipeline doesn't walk them), pinning
156 /// `ResolveCtx.current_module` to each module's prefix so a
157 /// dotted reference inside one module's body resolves through
158 /// that module's import set first.
159 pub fn build(
160 entry_items: Vec<ResolvedTopLevel>,
161 modules: &[ModuleInfo],
162 symbol_table: &SymbolTable,
163 ) -> Self {
164 let module_views: Vec<ResolvedModuleFns> = modules
165 .iter()
166 .map(|module| {
167 let mut rctx = ResolveCtx::new(symbol_table);
168 rctx.current_module = Some(module.prefix.clone());
169 let fn_defs = module
170 .fn_defs
171 .iter()
172 .filter_map(|fd| resolve_fn_def_external(&rctx, fd))
173 .collect();
174 ResolvedModuleFns {
175 prefix: module.prefix.clone(),
176 fn_defs,
177 }
178 })
179 .collect();
180
181 let mut fn_index: HashMap<crate::ir::FnId, FnIndexEntry> = HashMap::new();
182 for (pos, item) in entry_items.iter().enumerate() {
183 if let ResolvedTopLevel::FnDef(rfd) = item {
184 fn_index.insert(rfd.fn_id, FnIndexEntry { module: None, pos });
185 }
186 }
187 for (i, m) in module_views.iter().enumerate() {
188 for (pos, rfd) in m.fn_defs.iter().enumerate() {
189 fn_index.insert(
190 rfd.fn_id,
191 FnIndexEntry {
192 module: Some(i),
193 pos,
194 },
195 );
196 }
197 }
198
199 Self {
200 entry_items,
201 modules: module_views,
202 fn_index,
203 }
204 }
205
206 /// Resolved entry-scope fn defs in source order — the same set
207 /// `CodegenContext.resolved_fn_defs` projected today, just
208 /// reached through the canonical view.
209 pub fn entry_fns(&self) -> impl Iterator<Item = &ResolvedFnDef> + '_ {
210 self.entry_items.iter().filter_map(|item| match item {
211 ResolvedTopLevel::FnDef(rfd) => Some(rfd),
212 _ => None,
213 })
214 }
215
216 /// Resolved fn defs from one dep module, in source order.
217 /// Returns an empty iterator when no module with that prefix is
218 /// present — callers that need an existence check should use
219 /// [`Self::module`] instead.
220 pub fn module_fns(&self, prefix: &str) -> impl Iterator<Item = &ResolvedFnDef> + '_ {
221 self.modules
222 .iter()
223 .find(|m| m.prefix == prefix)
224 .into_iter()
225 .flat_map(|m| m.fn_defs.iter())
226 }
227
228 /// Whole module slice (prefix + resolved fns) for callsites that
229 /// need the prefix alongside.
230 pub fn module(&self, prefix: &str) -> Option<&ResolvedModuleFns> {
231 self.modules.iter().find(|m| m.prefix == prefix)
232 }
233
234 /// `FnId`-keyed lookup. Returns the resolved fn def regardless
235 /// of scope — identity-sensitive consumers (proof emitter,
236 /// inliner, monomorph) reach this rather than walking entry +
237 /// modules manually.
238 pub fn fn_by_id(&self, id: crate::ir::FnId) -> Option<&ResolvedFnDef> {
239 let entry = *self.fn_index.get(&id)?;
240 match entry.module {
241 None => {
242 let item = self.entry_items.get(entry.pos)?;
243 match item {
244 ResolvedTopLevel::FnDef(rfd) => Some(rfd),
245 _ => None,
246 }
247 }
248 Some(i) => self.modules.get(i)?.fn_defs.get(entry.pos),
249 }
250 }
251
252 /// First fn (entry first, then dep modules in declaration order)
253 /// whose source-level name matches. Stage 5+ of #232 uses this to
254 /// bridge ad-hoc detectors that historically searched by string
255 /// name (`is_directly_recursive` in dafny codegen) into the typed
256 /// `FnId`-keyed `ProgramShape`. Returns `None` if no such fn
257 /// exists in the resolved view.
258 ///
259 /// Note: bare-name lookup is ambiguous when the same name lives
260 /// in two dep modules. Entry-first ordering matches what the
261 /// legacy string-name detectors already assumed.
262 pub fn fn_by_name(&self, name: &str) -> Option<&ResolvedFnDef> {
263 for fd in self.entry_fns() {
264 if fd.name == name {
265 return Some(fd);
266 }
267 }
268 for m in &self.modules {
269 for fd in &m.fn_defs {
270 if fd.name == name {
271 return Some(fd);
272 }
273 }
274 }
275 None
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::ast::{FnBody, FnDef, Module, Spanned, TopLevel};
283 use crate::codegen::ModuleInfo;
284 use crate::ir::SymbolTable;
285
286 fn mk_fn(name: &str) -> FnDef {
287 FnDef {
288 name: name.to_string(),
289 line: 1,
290 params: vec![],
291 return_type: "Int".to_string(),
292 effects: vec![],
293 desc: None,
294 body: std::sync::Arc::new(FnBody::Block(vec![crate::ast::Stmt::Expr(Spanned::bare(
295 crate::ast::Expr::Literal(crate::ast::Literal::Int(0)),
296 ))])),
297 resolution: None,
298 }
299 }
300
301 fn mk_module(prefix: &str, fn_names: &[&str]) -> ModuleInfo {
302 ModuleInfo {
303 prefix: prefix.to_string(),
304 depends: vec![],
305 type_defs: vec![],
306 fn_defs: fn_names.iter().map(|n| mk_fn(n)).collect(),
307 verify_laws: vec![],
308 analysis: None,
309 }
310 }
311
312 #[test]
313 fn view_indexes_entry_fns_by_fn_id() {
314 let entry_items = vec![TopLevel::FnDef(mk_fn("foo")), TopLevel::FnDef(mk_fn("bar"))];
315 let symbol_table = SymbolTable::build(&entry_items, &[]);
316 let resolved = crate::ir::hir::resolve_program(&symbol_table, &entry_items);
317 let view = ResolvedProgramView::build(resolved, &[], &symbol_table);
318
319 assert_eq!(view.entry_fns().count(), 2);
320 let foo_id = symbol_table
321 .fn_id_of(&crate::ir::FnKey::entry("foo"))
322 .expect("foo entry FnId");
323 let bar_id = symbol_table
324 .fn_id_of(&crate::ir::FnKey::entry("bar"))
325 .expect("bar entry FnId");
326 assert_eq!(view.fn_by_id(foo_id).map(|f| f.name.as_str()), Some("foo"));
327 assert_eq!(view.fn_by_id(bar_id).map(|f| f.name.as_str()), Some("bar"));
328 }
329
330 #[test]
331 fn cross_module_same_bare_name_disambiguates_by_fn_id() {
332 // The whole point of this view: two modules with a `walker`
333 // fn must NOT collide. The `FnId` lookup picks the right
334 // resolved body for each.
335 let entry_items: Vec<TopLevel> = vec![TopLevel::Module(Module {
336 name: "Entry".to_string(),
337 line: 1,
338 depends: vec!["A".to_string(), "B".to_string()],
339 exposes: vec![],
340 exposes_opaque: vec![],
341 exposes_line: None,
342 intent: "Test".to_string(),
343 effects: None,
344 effects_line: None,
345 })];
346 let modules = vec![mk_module("A", &["walker"]), mk_module("B", &["walker"])];
347 let symbol_table = SymbolTable::build(&entry_items, &modules);
348 let resolved = crate::ir::hir::resolve_program(&symbol_table, &entry_items);
349 let view = ResolvedProgramView::build(resolved, &modules, &symbol_table);
350
351 let a_id = symbol_table
352 .fn_id_of(&crate::ir::FnKey::in_module("A".to_string(), "walker"))
353 .expect("A.walker FnId");
354 let b_id = symbol_table
355 .fn_id_of(&crate::ir::FnKey::in_module("B".to_string(), "walker"))
356 .expect("B.walker FnId");
357 assert_ne!(a_id, b_id, "FnIds must be distinct across modules");
358
359 let a_walker = view.fn_by_id(a_id).expect("A.walker present");
360 let b_walker = view.fn_by_id(b_id).expect("B.walker present");
361 assert_eq!(a_walker.name, "walker");
362 assert_eq!(b_walker.name, "walker");
363 assert_eq!(a_walker.fn_id, a_id);
364 assert_eq!(b_walker.fn_id, b_id);
365 assert_eq!(view.module_fns("A").count(), 1);
366 assert_eq!(view.module_fns("B").count(), 1);
367 }
368}