Skip to main content

aver/ir/
identity.rs

1//! Identity keys for named declarations.
2//!
3//! Aver source can declare the same bare name in two modules
4//! (`A.foo` and `B.foo` are distinct fns; `A.Natural` and
5//! `B.Natural` are distinct types). The typechecker keeps these
6//! straight at its boundary (`fn_sigs` is keyed by canonical
7//! `Module.name`), but downstream consumers — proof IR, backends,
8//! the VM bytecode lowerer — historically threaded bare `String`s
9//! around and re-derived identity from `fd.name` at every lookup.
10//! That produced a steady drip of "two same-bare-name fns get the
11//! wrong contract / refinement / strategy" bugs (review rounds 1-6
12//! of the proof-export audit).
13//!
14//! `FnKey` / `TypeKey` / `LawKey` are the typed replacements.
15//! Identity is resolved **once** at the IR boundary (today: the
16//! `proof_lower` stage); from there backends consume the key
17//! directly. The compiler refuses to compare a key to a bare
18//! `String`, so the bug class can no longer be expressed.
19//!
20//! These types live in `crate::ir` rather than `crate::ir::proof_ir`
21//! because identity-across-module-boundary is a general IR concern,
22//! not a proof-specific one. The proof flow is the first consumer;
23//! future cross-module identity sites (VM call-site resolution,
24//! Rust codegen multi-module module-fn lookups) will use the same
25//! types.
26
27// ---------------------------------------------------------------------------
28// Opaque IDs
29// ---------------------------------------------------------------------------
30//
31// These live here, not in `symbol_table`, so that `crate::ast::Type` can
32// carry a `TypeId` field without `ast` having to depend on `crate::ir`'s
33// AST-bearing modules. `identity` has no upward deps (only `std`), which
34// keeps the dependency layering clean: `ast` → `ir::identity` →
35// `ir::symbol_table` (consumes ast) → everything else.
36
37/// Opaque, stable identity for a function declaration. Indices are
38/// assigned by [`crate::ir::SymbolTable::build`] in deterministic order
39/// (modules in dep order, then entry; fns in source order within each
40/// scope). Two builds against the same input produce the same IDs.
41///
42/// `FnId` is `Copy` and 4 bytes wide — cheap to thread through
43/// resolved AST and IR nodes.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
45pub struct FnId(pub u32);
46
47/// Opaque, stable identity for a type declaration (record or sum).
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
49pub struct TypeId(pub u32);
50
51/// Opaque, stable identity for a constructor (a single variant of a
52/// sum type, or a single record's nominal constructor — record types
53/// still have exactly one "constructor" in the symbol table sense so
54/// pattern emit + value construction route through one shape).
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
56pub struct CtorId(pub u32);
57
58/// Opaque, stable identity for a module. `ModuleId(0)` is reserved
59/// for the entry scope (matching `FnKey::scope == None`).
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
61pub struct ModuleId(pub u32);
62
63/// Opaque, stable identity for a built-in function (e.g.
64/// `Console.print`, `List.prepend`, `String.fromInt`). Phase 6
65/// wave 11: replaces the previous `MirCallee::Builtin(String)`
66/// shape with a typed id so MIR substrate uses typed identity
67/// everywhere (matching `FnId` / `CtorId` / `TypeId`'s
68/// "consumers never re-parse names" rule). The string name
69/// stays available via `SymbolTable::builtin_entry(id).name`
70/// for diagnostics + the registries that still key off strings
71/// (the VM builtin table, the Rust codegen's
72/// `emit_builtin_call`).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
74pub struct BuiltinId(pub u32);
75
76impl ModuleId {
77    /// The entry scope — top-level items not declared inside any dep
78    /// module. Always assigned `ModuleId(0)`.
79    pub const ENTRY: ModuleId = ModuleId(0);
80
81    pub fn is_entry(self) -> bool {
82        self == Self::ENTRY
83    }
84}
85
86/// Canonical identity for a user-defined function across the IR
87/// boundary. `scope = None` is the entry file; `scope = Some(prefix)`
88/// names a dep module (`"ModuleA"`, `"Models.User"`, …). `name`
89/// is the bare source name. Two modules each declaring a `foo`
90/// produce distinct `FnKey { scope: Some("A"), name: "foo" }` and
91/// `FnKey { scope: Some("B"), name: "foo" }` keys.
92#[derive(Debug, Clone, Eq, PartialEq, Hash)]
93pub struct FnKey {
94    pub scope: Option<String>,
95    pub name: String,
96}
97
98impl FnKey {
99    /// Entry-scope (None) constructor.
100    pub fn entry(name: impl Into<String>) -> Self {
101        Self {
102            scope: None,
103            name: name.into(),
104        }
105    }
106
107    /// Module-scope constructor.
108    pub fn in_module(prefix: impl Into<String>, name: impl Into<String>) -> Self {
109        Self {
110            scope: Some(prefix.into()),
111            name: name.into(),
112        }
113    }
114
115    /// Canonical text form: `Module.name` for module-scoped fns,
116    /// bare `name` for entry. Use this only when crossing an
117    /// interop boundary (Lean qualified imports, Dafny
118    /// `Aver_Module.fn` references) — internal IR / backend logic
119    /// should keep operating on the typed key.
120    pub fn canonical(&self) -> String {
121        match &self.scope {
122            Some(prefix) => format!("{}.{}", prefix, self.name),
123            None => self.name.clone(),
124        }
125    }
126
127    /// Scope as a borrowed `&str` for callers that just need to
128    /// branch on entry vs module without taking ownership.
129    pub fn scope_str(&self) -> Option<&str> {
130        self.scope.as_deref()
131    }
132}
133
134/// Canonical identity for a user-defined type — same shape and
135/// reasoning as [`FnKey`]. Refined records, sum types, and opaque
136/// records all live in the IR keyed by `TypeKey` so a bare AST
137/// reference (`Expr::RecordCreate { type_name: "Natural" }`) is
138/// resolved once at the IR boundary against the caller's owning
139/// scope, never re-resolved per emit site.
140#[derive(Debug, Clone, Eq, PartialEq, Hash)]
141pub struct TypeKey {
142    pub scope: Option<String>,
143    pub name: String,
144}
145
146impl TypeKey {
147    pub fn entry(name: impl Into<String>) -> Self {
148        Self {
149            scope: None,
150            name: name.into(),
151        }
152    }
153
154    pub fn in_module(prefix: impl Into<String>, name: impl Into<String>) -> Self {
155        Self {
156            scope: Some(prefix.into()),
157            name: name.into(),
158        }
159    }
160
161    pub fn canonical(&self) -> String {
162        match &self.scope {
163            Some(prefix) => format!("{}.{}", prefix, self.name),
164            None => self.name.clone(),
165        }
166    }
167
168    pub fn scope_str(&self) -> Option<&str> {
169        self.scope.as_deref()
170    }
171}
172
173/// Canonical identity for a verify-law theorem. Targets a fn by
174/// [`FnKey`]; the `law_name` is local to that fn (Aver's
175/// `verify <fn> law <law_name>` syntax — names only collide within
176/// one fn's verify block).
177#[derive(Debug, Clone, Eq, PartialEq, Hash)]
178pub struct LawKey {
179    pub fn_key: FnKey,
180    pub law_name: String,
181}
182
183impl LawKey {
184    pub fn new(fn_key: FnKey, law_name: impl Into<String>) -> Self {
185        Self {
186            fn_key,
187            law_name: law_name.into(),
188        }
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn fn_key_canonical_distinguishes_entry_from_module() {
198        let entry = FnKey::entry("foo");
199        let in_a = FnKey::in_module("A", "foo");
200        let in_b = FnKey::in_module("B", "foo");
201        assert_eq!(entry.canonical(), "foo");
202        assert_eq!(in_a.canonical(), "A.foo");
203        assert_eq!(in_b.canonical(), "B.foo");
204        // The Hash + Eq contract distinguishes them — that's the
205        // entire point of the type.
206        assert_ne!(entry, in_a);
207        assert_ne!(in_a, in_b);
208    }
209
210    #[test]
211    fn nested_module_prefix_round_trips_through_canonical() {
212        let key = FnKey::in_module("Models.User", "freshId");
213        assert_eq!(key.canonical(), "Models.User.freshId");
214        assert_eq!(key.scope_str(), Some("Models.User"));
215    }
216}