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}