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}