Skip to main content

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}