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
63impl ModuleId {
64    /// The entry scope — top-level items not declared inside any dep
65    /// module. Always assigned `ModuleId(0)`.
66    pub const ENTRY: ModuleId = ModuleId(0);
67
68    pub fn is_entry(self) -> bool {
69        self == Self::ENTRY
70    }
71}
72
73/// Canonical identity for a user-defined function across the IR
74/// boundary. `scope = None` is the entry file; `scope = Some(prefix)`
75/// names a dep module (`"ModuleA"`, `"Models.User"`, …). `name`
76/// is the bare source name. Two modules each declaring a `foo`
77/// produce distinct `FnKey { scope: Some("A"), name: "foo" }` and
78/// `FnKey { scope: Some("B"), name: "foo" }` keys.
79#[derive(Debug, Clone, Eq, PartialEq, Hash)]
80pub struct FnKey {
81    pub scope: Option<String>,
82    pub name: String,
83}
84
85impl FnKey {
86    /// Entry-scope (None) constructor.
87    pub fn entry(name: impl Into<String>) -> Self {
88        Self {
89            scope: None,
90            name: name.into(),
91        }
92    }
93
94    /// Module-scope constructor.
95    pub fn in_module(prefix: impl Into<String>, name: impl Into<String>) -> Self {
96        Self {
97            scope: Some(prefix.into()),
98            name: name.into(),
99        }
100    }
101
102    /// Canonical text form: `Module.name` for module-scoped fns,
103    /// bare `name` for entry. Use this only when crossing an
104    /// interop boundary (Lean qualified imports, Dafny
105    /// `Aver_Module.fn` references) — internal IR / backend logic
106    /// should keep operating on the typed key.
107    pub fn canonical(&self) -> String {
108        match &self.scope {
109            Some(prefix) => format!("{}.{}", prefix, self.name),
110            None => self.name.clone(),
111        }
112    }
113
114    /// Scope as a borrowed `&str` for callers that just need to
115    /// branch on entry vs module without taking ownership.
116    pub fn scope_str(&self) -> Option<&str> {
117        self.scope.as_deref()
118    }
119}
120
121/// Canonical identity for a user-defined type — same shape and
122/// reasoning as [`FnKey`]. Refined records, sum types, and opaque
123/// records all live in the IR keyed by `TypeKey` so a bare AST
124/// reference (`Expr::RecordCreate { type_name: "Natural" }`) is
125/// resolved once at the IR boundary against the caller's owning
126/// scope, never re-resolved per emit site.
127#[derive(Debug, Clone, Eq, PartialEq, Hash)]
128pub struct TypeKey {
129    pub scope: Option<String>,
130    pub name: String,
131}
132
133impl TypeKey {
134    pub fn entry(name: impl Into<String>) -> Self {
135        Self {
136            scope: None,
137            name: name.into(),
138        }
139    }
140
141    pub fn in_module(prefix: impl Into<String>, name: impl Into<String>) -> Self {
142        Self {
143            scope: Some(prefix.into()),
144            name: name.into(),
145        }
146    }
147
148    pub fn canonical(&self) -> String {
149        match &self.scope {
150            Some(prefix) => format!("{}.{}", prefix, self.name),
151            None => self.name.clone(),
152        }
153    }
154
155    pub fn scope_str(&self) -> Option<&str> {
156        self.scope.as_deref()
157    }
158}
159
160/// Canonical identity for a verify-law theorem. Targets a fn by
161/// [`FnKey`]; the `law_name` is local to that fn (Aver's
162/// `verify <fn> law <law_name>` syntax — names only collide within
163/// one fn's verify block).
164#[derive(Debug, Clone, Eq, PartialEq, Hash)]
165pub struct LawKey {
166    pub fn_key: FnKey,
167    pub law_name: String,
168}
169
170impl LawKey {
171    pub fn new(fn_key: FnKey, law_name: impl Into<String>) -> Self {
172        Self {
173            fn_key,
174            law_name: law_name.into(),
175        }
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    #[test]
184    fn fn_key_canonical_distinguishes_entry_from_module() {
185        let entry = FnKey::entry("foo");
186        let in_a = FnKey::in_module("A", "foo");
187        let in_b = FnKey::in_module("B", "foo");
188        assert_eq!(entry.canonical(), "foo");
189        assert_eq!(in_a.canonical(), "A.foo");
190        assert_eq!(in_b.canonical(), "B.foo");
191        // The Hash + Eq contract distinguishes them — that's the
192        // entire point of the type.
193        assert_ne!(entry, in_a);
194        assert_ne!(in_a, in_b);
195    }
196
197    #[test]
198    fn nested_module_prefix_round_trips_through_canonical() {
199        let key = FnKey::in_module("Models.User", "freshId");
200        assert_eq!(key.canonical(), "Models.User.freshId");
201        assert_eq!(key.scope_str(), Some("Models.User"));
202    }
203}