Skip to main content

aver/ir/
symbol_table.rs

1//! Resolved-identity layer for the compiler.
2//!
3//! Issue #138 phase E. Stable opaque IDs (`FnId`, `TypeId`,
4//! `CtorId`, `ModuleId`) for every named declaration in the
5//! program, paired with bidirectional lookup against the typed
6//! identity keys from [`crate::ir::identity`].
7//!
8//! ## Why
9//!
10//! Six rounds of reviewer audit (PRs #130-#136) and the typed
11//! `FnKey` / `TypeKey` bridge (#137, #139) closed the bug class
12//! *for the IR maps that already cross the boundary*. The cause
13//! underneath was a missing pass: name resolution. Today every
14//! consumer (typechecker, proof lowering, codegen, opaque checks,
15//! recursion analysis) re-derives identity from `fd.name` /
16//! `td.name` at each lookup. The resolver is implicit and
17//! duplicated; the bug class is "two implicit resolvers
18//! disagree".
19//!
20//! The fix is to make resolution **explicit and one-shot**: after
21//! module load, build a [`SymbolTable`] once. Every subsequent
22//! pass takes a `FnId` / `TypeId` and never re-resolves a string.
23//! Backend mangling (`FnId → "Module.fn"` / `FnId →
24//! "Aver_Module.fn"`) is a single function applied at emit time.
25//!
26//! ## Foundation, not migration
27//!
28//! This module is the **foundation only**. It introduces the
29//! types and the build function. No consumer is migrated yet —
30//! `ProofIR.fn_contracts` stays keyed by `FnKey`, `Type::Named`
31//! stays `String`-bodied, backends keep their bare-string
32//! lookups. Migration is staged across follow-up PRs (#138
33//! tracks the phases).
34//!
35//! Today's foundation lets a downstream pass say
36//! `symbols.fn_id_of(&fn_key)` and get a stable opaque handle
37//! back, without disturbing anything that already works.
38
39use std::collections::HashMap;
40
41use crate::ast::{FnDef, TopLevel, TypeDef, TypeVariant};
42use crate::codegen::ModuleInfo;
43use crate::ir::identity::{CtorId, FnId, FnKey, ModuleId, TypeId, TypeKey};
44
45/// One entry in the function table. The `key` field is the public
46/// canonical handle; `module` is the owning scope; `index_in_module`
47/// is the position of this fn in its scope's source-order fn list
48/// (useful for stable display ordering).
49#[derive(Debug, Clone)]
50pub struct FnEntry {
51    pub key: FnKey,
52    pub module: ModuleId,
53    pub index_in_module: u32,
54}
55
56/// One entry in the type table.
57#[derive(Debug, Clone)]
58pub struct TypeEntry {
59    pub key: TypeKey,
60    pub module: ModuleId,
61    pub index_in_module: u32,
62    /// The variants list of a sum type, in source order. Empty
63    /// for record (product) types — those use a single implicit
64    /// constructor whose `CtorId` is registered separately.
65    pub variants: Vec<CtorId>,
66    /// `true` when the type is a product (record). The record has
67    /// one [`CtorId`] entry in the [`SymbolTable::ctors`] table
68    /// even though it has no source-level variant list; this lets
69    /// emit code uniformly route record creation through the
70    /// constructor path.
71    pub is_product: bool,
72}
73
74/// One entry in the constructor table. A constructor belongs to
75/// a single type; sum-type variants get one entry each, product
76/// (record) types get one entry total.
77#[derive(Debug, Clone)]
78pub struct CtorEntry {
79    pub owning_type: TypeId,
80    /// Source name of the variant (`"Ok"`, `"Err"`, `"Some"`,
81    /// `"None"`, `"Circle"`, …). For record types this is the
82    /// type's own name (records share their type's name as the
83    /// constructor name in source syntax: `User { ... }`).
84    pub name: String,
85}
86
87/// One entry in the module table. `ModuleId(0)` is always the
88/// synthetic entry scope; subsequent IDs correspond to
89/// `inputs.dep_modules` in walk order.
90#[derive(Debug, Clone)]
91pub struct ModuleEntry {
92    /// `None` for the entry scope; `Some(prefix)` for dep modules.
93    pub prefix: Option<String>,
94}
95
96/// Resolved-identity table for an Aver program. Built once after
97/// module load + before typecheck (eventually — today no caller
98/// invokes this yet); consumers thereafter look up IDs and never
99/// re-resolve names.
100#[derive(Debug, Clone, Default)]
101pub struct SymbolTable {
102    pub modules: Vec<ModuleEntry>,
103    pub fns: Vec<FnEntry>,
104    pub types: Vec<TypeEntry>,
105    pub ctors: Vec<CtorEntry>,
106
107    fn_index: HashMap<FnKey, FnId>,
108    type_index: HashMap<TypeKey, TypeId>,
109    /// Per-type lookup of `variant_name → CtorId`. The outer
110    /// `TypeId` plus inner `String` lets two unrelated sum types
111    /// share a variant name (`Result.Ok` vs a user's
112    /// `Validation.Ok`) without collision.
113    ctor_index: HashMap<(TypeId, String), CtorId>,
114}
115
116impl SymbolTable {
117    /// Build a `SymbolTable` from entry items + dep modules. The
118    /// build order is fully deterministic: modules in
119    /// `dep_modules` walk order with the entry scope prepended,
120    /// fns in source order within each module, types in source
121    /// order within each module, ctors in variant-source order
122    /// within each type.
123    pub fn build(entry_items: &[TopLevel], dep_modules: &[ModuleInfo]) -> Self {
124        let mut table = SymbolTable::default();
125
126        // ModuleId(0) is always the entry scope.
127        table.modules.push(ModuleEntry { prefix: None });
128        for m in dep_modules {
129            table.modules.push(ModuleEntry {
130                prefix: Some(m.prefix.clone()),
131            });
132        }
133
134        // Helpful pre-traversal: gather (scope, fns, types) tuples
135        // so the actual indexing pass is uniform across entry vs
136        // module scopes.
137        let entry_fns: Vec<&FnDef> = entry_items
138            .iter()
139            .filter_map(|i| match i {
140                TopLevel::FnDef(fd) => Some(fd),
141                _ => None,
142            })
143            .collect();
144        let entry_types: Vec<&TypeDef> = entry_items
145            .iter()
146            .filter_map(|i| match i {
147                TopLevel::TypeDef(td) => Some(td),
148                _ => None,
149            })
150            .collect();
151
152        type ScopeWalk<'a> = (ModuleId, Option<&'a str>, Vec<&'a FnDef>, Vec<&'a TypeDef>);
153        let scopes: Vec<ScopeWalk> =
154            std::iter::once((ModuleId::ENTRY, None, entry_fns, entry_types))
155                .chain(dep_modules.iter().enumerate().map(|(i, m)| {
156                    let module_id = ModuleId((i + 1) as u32);
157                    let fns: Vec<&FnDef> = m.fn_defs.iter().collect();
158                    let types: Vec<&TypeDef> = m.type_defs.iter().collect();
159                    (module_id, Some(m.prefix.as_str()), fns, types)
160                }))
161                .collect();
162
163        for (module_id, prefix, fns, types) in scopes {
164            for (index_in_module, fd) in fns.into_iter().enumerate() {
165                let key = match prefix {
166                    Some(p) => FnKey::in_module(p.to_string(), fd.name.clone()),
167                    None => FnKey::entry(fd.name.clone()),
168                };
169                let id = FnId(table.fns.len() as u32);
170                table.fn_index.insert(key.clone(), id);
171                table.fns.push(FnEntry {
172                    key,
173                    module: module_id,
174                    index_in_module: index_in_module as u32,
175                });
176            }
177            for (index_in_module, td) in types.into_iter().enumerate() {
178                let (type_name, variants, is_product) = match td {
179                    TypeDef::Sum { name, variants, .. } => (name.clone(), variants.clone(), false),
180                    TypeDef::Product { name, .. } => (name.clone(), Vec::new(), true),
181                };
182                let key = match prefix {
183                    Some(p) => TypeKey::in_module(p.to_string(), type_name.clone()),
184                    None => TypeKey::entry(type_name.clone()),
185                };
186                let type_id = TypeId(table.types.len() as u32);
187                table.type_index.insert(key.clone(), type_id);
188                // Register variants (sum) or single constructor
189                // (product) into the ctor table.
190                let ctor_ids: Vec<CtorId> = if is_product {
191                    let cid = CtorId(table.ctors.len() as u32);
192                    table.ctors.push(CtorEntry {
193                        owning_type: type_id,
194                        name: type_name.clone(),
195                    });
196                    table.ctor_index.insert((type_id, type_name.clone()), cid);
197                    vec![cid]
198                } else {
199                    let mut ids = Vec::with_capacity(variants.len());
200                    for v in &variants {
201                        let cid = CtorId(table.ctors.len() as u32);
202                        table.ctors.push(CtorEntry {
203                            owning_type: type_id,
204                            name: v.name.clone(),
205                        });
206                        table.ctor_index.insert((type_id, v.name.clone()), cid);
207                        ids.push(cid);
208                    }
209                    ids
210                };
211                table.types.push(TypeEntry {
212                    key,
213                    module: module_id,
214                    index_in_module: index_in_module as u32,
215                    variants: ctor_ids,
216                    is_product,
217                });
218            }
219        }
220
221        table
222    }
223
224    /// Resolve a `FnKey` to its `FnId`. `None` when the key
225    /// doesn't name any function in the program.
226    pub fn fn_id_of(&self, key: &FnKey) -> Option<FnId> {
227        self.fn_index.get(key).copied()
228    }
229
230    /// Resolve a `TypeKey` to its `TypeId`.
231    pub fn type_id_of(&self, key: &TypeKey) -> Option<TypeId> {
232        self.type_index.get(key).copied()
233    }
234
235    /// Resolve a *bare* type name (no module prefix) against every
236    /// scope in the table — entry first, then each dep module — and
237    /// return the unique match. Returns `None` when the name doesn't
238    /// resolve OR when two or more scopes legitimately share it
239    /// (ambiguous reference; caller must qualify).
240    ///
241    /// Phase-E cross-module ctor resolution: an Aver expression like
242    /// `Val.ValOk(x)` written from a module that doesn't host the
243    /// `Val` type still needs to resolve to the right `TypeId` so the
244    /// resolver can lift the call to `ResolvedCtor::User`. Pre-PR-8
245    /// the rust codegen worked around the gap by matching ctors by
246    /// raw string after the resolver had moved on; PR 8 made the
247    /// resolved-form classification authoritative, which exposed
248    /// this missing lookup. Adding it here keeps identity logic on
249    /// the symbol table where the rest of identity resolution lives.
250    pub fn type_id_by_bare_name(&self, name: &str) -> Option<TypeId> {
251        let mut found: Option<TypeId> = None;
252        for (key, id) in &self.type_index {
253            if key.name == name {
254                if found.is_some() {
255                    // Ambiguous bare reference across scopes; the
256                    // caller must qualify with a module prefix.
257                    return None;
258                }
259                found = Some(*id);
260            }
261        }
262        found
263    }
264
265    /// Resolve a constructor by (owning type, variant name).
266    pub fn ctor_id_of(&self, owning_type: TypeId, variant: &str) -> Option<CtorId> {
267        self.ctor_index
268            .get(&(owning_type, variant.to_string()))
269            .copied()
270    }
271
272    /// Borrow the entry for a `FnId`. Panics on an invalid `FnId`
273    /// — the only way to obtain one is through this table, so an
274    /// invalid `FnId` is a logic error, not a runtime input
275    /// failure.
276    pub fn fn_entry(&self, id: FnId) -> &FnEntry {
277        &self.fns[id.0 as usize]
278    }
279
280    pub fn type_entry(&self, id: TypeId) -> &TypeEntry {
281        &self.types[id.0 as usize]
282    }
283
284    pub fn ctor_entry(&self, id: CtorId) -> &CtorEntry {
285        &self.ctors[id.0 as usize]
286    }
287
288    pub fn module_entry(&self, id: ModuleId) -> &ModuleEntry {
289        &self.modules[id.0 as usize]
290    }
291
292    /// Build-time invariant check: every registered key resolves
293    /// back to a `FnEntry` whose stored key equals the lookup
294    /// key. Helps catch programmer errors in this module — never
295    /// expected to fail at runtime in production callers.
296    #[cfg(test)]
297    pub(crate) fn assert_consistent(&self) {
298        for (i, entry) in self.fns.iter().enumerate() {
299            assert_eq!(
300                self.fn_index.get(&entry.key),
301                Some(&FnId(i as u32)),
302                "fn_index out of sync at index {i}"
303            );
304        }
305        for (i, entry) in self.types.iter().enumerate() {
306            assert_eq!(
307                self.type_index.get(&entry.key),
308                Some(&TypeId(i as u32)),
309                "type_index out of sync at index {i}"
310            );
311        }
312        for (i, entry) in self.ctors.iter().enumerate() {
313            assert_eq!(
314                self.ctor_index
315                    .get(&(entry.owning_type, entry.name.clone())),
316                Some(&CtorId(i as u32)),
317                "ctor_index out of sync at index {i}"
318            );
319        }
320    }
321}
322
323#[allow(dead_code)] // suppresses warning while no production caller uses these helpers
324fn _no_warning_for_unused_variant_field(_v: &TypeVariant) {}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use crate::ast::{TypeDef, TypeVariant};
330
331    fn product(name: &str, line: usize) -> TypeDef {
332        TypeDef::Product {
333            name: name.to_string(),
334            fields: vec![("value".to_string(), "Int".to_string())],
335            line,
336        }
337    }
338
339    fn sum(name: &str, variants: &[&str], line: usize) -> TypeDef {
340        TypeDef::Sum {
341            name: name.to_string(),
342            variants: variants
343                .iter()
344                .map(|v| TypeVariant {
345                    name: v.to_string(),
346                    fields: Vec::new(),
347                })
348                .collect(),
349            line,
350        }
351    }
352
353    fn module(prefix: &str, fns: Vec<FnDef>, types: Vec<TypeDef>) -> ModuleInfo {
354        ModuleInfo {
355            prefix: prefix.to_string(),
356            depends: Vec::new(),
357            type_defs: types,
358            fn_defs: fns,
359            analysis: None,
360        }
361    }
362
363    fn fn_def(name: &str) -> FnDef {
364        use crate::ast::{FnBody, Literal, Spanned};
365        FnDef {
366            name: name.to_string(),
367            line: 1,
368            params: Vec::new(),
369            return_type: "Int".to_string(),
370            effects: Vec::new(),
371            desc: None,
372            body: std::sync::Arc::new(FnBody::from_expr(Spanned::new(
373                crate::ast::Expr::Literal(Literal::Int(0)),
374                1,
375            ))),
376            resolution: None,
377        }
378    }
379
380    #[test]
381    fn entry_only_program_gets_entry_module_and_indexed_fns() {
382        let items = vec![
383            TopLevel::FnDef(fn_def("foo")),
384            TopLevel::FnDef(fn_def("bar")),
385        ];
386        let table = SymbolTable::build(&items, &[]);
387        table.assert_consistent();
388
389        assert_eq!(table.modules.len(), 1, "entry-only build has just ENTRY");
390        assert!(table.modules[0].prefix.is_none());
391
392        let foo_id = table.fn_id_of(&FnKey::entry("foo")).expect("foo missing");
393        let bar_id = table.fn_id_of(&FnKey::entry("bar")).expect("bar missing");
394        assert_ne!(foo_id, bar_id);
395        assert!(table.fn_entry(foo_id).module.is_entry());
396    }
397
398    #[test]
399    fn cross_module_same_bare_name_fns_get_distinct_ids() {
400        // Two modules each declaring `foo`. Pre-symbol-table the
401        // bare-name lookup would collapse them; the table keeps
402        // distinct entries indexed by canonical `FnKey`.
403        let mod_a = module("A", vec![fn_def("foo")], vec![]);
404        let mod_b = module("B", vec![fn_def("foo")], vec![]);
405        let table = SymbolTable::build(&[], &[mod_a, mod_b]);
406        table.assert_consistent();
407
408        let a_foo = table
409            .fn_id_of(&FnKey::in_module("A", "foo"))
410            .expect("A.foo missing");
411        let b_foo = table
412            .fn_id_of(&FnKey::in_module("B", "foo"))
413            .expect("B.foo missing");
414        assert_ne!(a_foo, b_foo);
415
416        // A bare-name lookup should NOT silently resolve to one of
417        // them — the table refuses ambiguity rather than guessing.
418        assert!(
419            table.fn_id_of(&FnKey::entry("foo")).is_none(),
420            "bare-entry `foo` must miss when only module fns named `foo` exist"
421        );
422    }
423
424    #[test]
425    fn product_type_has_one_constructor_under_its_own_name() {
426        let items = vec![TopLevel::TypeDef(product("Natural", 1))];
427        let table = SymbolTable::build(&items, &[]);
428        table.assert_consistent();
429
430        let nat_id = table.type_id_of(&TypeKey::entry("Natural")).unwrap();
431        let entry = table.type_entry(nat_id);
432        assert!(entry.is_product);
433        assert_eq!(entry.variants.len(), 1);
434        let ctor_id = entry.variants[0];
435        assert_eq!(table.ctor_entry(ctor_id).name, "Natural");
436        assert_eq!(table.ctor_entry(ctor_id).owning_type, nat_id);
437        assert_eq!(table.ctor_id_of(nat_id, "Natural"), Some(ctor_id));
438    }
439
440    #[test]
441    fn sum_type_registers_each_variant_under_its_type() {
442        let items = vec![TopLevel::TypeDef(sum("Shape", &["Circle", "Square"], 1))];
443        let table = SymbolTable::build(&items, &[]);
444        table.assert_consistent();
445
446        let shape_id = table.type_id_of(&TypeKey::entry("Shape")).unwrap();
447        let entry = table.type_entry(shape_id);
448        assert!(!entry.is_product);
449        assert_eq!(entry.variants.len(), 2);
450        let circle = table.ctor_id_of(shape_id, "Circle").unwrap();
451        let square = table.ctor_id_of(shape_id, "Square").unwrap();
452        assert_ne!(circle, square);
453        assert_eq!(table.ctor_entry(circle).name, "Circle");
454        assert_eq!(table.ctor_entry(square).name, "Square");
455    }
456
457    #[test]
458    fn variant_name_collision_across_unrelated_sums_is_resolved_per_type() {
459        // Two sum types each with an `Ok` variant — distinct
460        // `CtorId`s because owning_type differs. Mirror of the
461        // real-world Result.Ok vs user's `Validation.Ok` case
462        // the symbol table needs to handle once consumers
463        // migrate.
464        let items = vec![
465            TopLevel::TypeDef(sum("Validation", &["Ok", "Invalid"], 1)),
466            TopLevel::TypeDef(sum("Status", &["Ok", "Failed"], 5)),
467        ];
468        let table = SymbolTable::build(&items, &[]);
469        table.assert_consistent();
470
471        let v_id = table.type_id_of(&TypeKey::entry("Validation")).unwrap();
472        let s_id = table.type_id_of(&TypeKey::entry("Status")).unwrap();
473        let v_ok = table.ctor_id_of(v_id, "Ok").unwrap();
474        let s_ok = table.ctor_id_of(s_id, "Ok").unwrap();
475        assert_ne!(
476            v_ok, s_ok,
477            "same-bare-name variants across unrelated sums must be distinct"
478        );
479    }
480
481    #[test]
482    fn deterministic_indexing_across_repeated_builds() {
483        // Two identical inputs produce identical ID assignments.
484        // Property the `_for_test` consumers downstream rely on
485        // when comparing two builds.
486        let items_a = vec![
487            TopLevel::FnDef(fn_def("a")),
488            TopLevel::FnDef(fn_def("b")),
489            TopLevel::TypeDef(product("R", 1)),
490        ];
491        let items_b = items_a.clone();
492        let t1 = SymbolTable::build(&items_a, &[]);
493        let t2 = SymbolTable::build(&items_b, &[]);
494        assert_eq!(
495            t1.fn_id_of(&FnKey::entry("a")),
496            t2.fn_id_of(&FnKey::entry("a"))
497        );
498        assert_eq!(
499            t1.type_id_of(&TypeKey::entry("R")),
500            t2.type_id_of(&TypeKey::entry("R"))
501        );
502    }
503
504    #[test]
505    fn entry_scope_is_always_module_id_zero() {
506        // Doc invariant — `ModuleId::ENTRY` is the entry scope's
507        // ID regardless of how many dep modules a project has.
508        let mod_a = module("A", vec![fn_def("x")], vec![]);
509        let mod_b = module("B", vec![fn_def("y")], vec![]);
510        let table = SymbolTable::build(&[TopLevel::FnDef(fn_def("entry_fn"))], &[mod_a, mod_b]);
511        table.assert_consistent();
512        let entry_fn = table.fn_id_of(&FnKey::entry("entry_fn")).unwrap();
513        assert_eq!(table.fn_entry(entry_fn).module, ModuleId::ENTRY);
514        assert_eq!(table.module_entry(ModuleId::ENTRY).prefix, None);
515    }
516
517    #[test]
518    fn type_id_by_bare_name_resolves_unique_cross_module_type() {
519        // Phase-E PR 9.4: a callsite in module `A` referring to type
520        // `Val` declared in module `B` (no qualifier in source) gets
521        // the right `TypeId` via this fallback. The rule is: "bare
522        // type name from any scope resolves iff exactly one scope
523        // declares it".
524        let mod_a = module("A", vec![], vec![sum("Color", &["Red", "Blue"], 1)]);
525        let mod_b = module("B", vec![], vec![product("Val", 1)]);
526        let table = SymbolTable::build(&[], &[mod_a, mod_b]);
527        table.assert_consistent();
528
529        let val_id = table.type_id_by_bare_name("Val");
530        let qualified = table.type_id_of(&TypeKey::in_module("B", "Val"));
531        assert_eq!(val_id, qualified, "bare `Val` resolves to B.Val");
532        assert!(val_id.is_some());
533
534        let color_id = table.type_id_by_bare_name("Color");
535        assert_eq!(
536            color_id,
537            table.type_id_of(&TypeKey::in_module("A", "Color"))
538        );
539    }
540
541    #[test]
542    fn type_id_by_bare_name_returns_none_on_ambiguity() {
543        // Two modules each declaring a type called `T` — bare
544        // reference is ambiguous, caller must qualify. The cross-
545        // module ctor-classification path falls back to
546        // `Unresolved` here, which the typechecker then flags as a
547        // user-facing error (or the caller already qualified, in
548        // which case the higher-priority `TypeKey::in_module`
549        // lookup succeeded before this method ran).
550        let mod_a = module("A", vec![], vec![product("T", 1)]);
551        let mod_b = module("B", vec![], vec![product("T", 1)]);
552        let table = SymbolTable::build(&[], &[mod_a, mod_b]);
553        table.assert_consistent();
554
555        assert_eq!(table.type_id_by_bare_name("T"), None);
556
557        // Qualified lookups still resolve.
558        assert!(table.type_id_of(&TypeKey::in_module("A", "T")).is_some());
559        assert!(table.type_id_of(&TypeKey::in_module("B", "T")).is_some());
560    }
561
562    #[test]
563    fn type_id_by_bare_name_finds_entry_scope_type() {
564        // Bare lookup also reaches the entry scope when only the
565        // entry declares the type — same uniqueness rule.
566        let entry_type = product("Cfg", 1);
567        let mod_a = module("A", vec![], vec![sum("Other", &["X"], 1)]);
568        let table = SymbolTable::build(&[TopLevel::TypeDef(entry_type)], &[mod_a]);
569        table.assert_consistent();
570
571        let cfg = table.type_id_by_bare_name("Cfg");
572        assert_eq!(cfg, table.type_id_of(&TypeKey::entry("Cfg")));
573        assert!(cfg.is_some());
574    }
575
576    #[test]
577    fn type_id_by_bare_name_missing_name_is_none() {
578        let mod_a = module("A", vec![], vec![product("X", 1)]);
579        let table = SymbolTable::build(&[], &[mod_a]);
580        table.assert_consistent();
581        assert_eq!(table.type_id_by_bare_name("NotATypeAnywhere"), None);
582    }
583}