prebindgen_flat/flat/spelling.rs
1//! Spelling: how a captured type is written, reduced to one canonical form.
2//!
3//! Ingest-time machinery, and the frontend's own — it decides what spelling a
4//! type *has* before anything keys on it, which is the same authority that
5//! decides what a type *means*. It lived in `api/core/types_util` until #229's
6//! L2d, next to the classifiers, where it looked like one of them; it is not.
7//! Nothing outside `api/core` ever called it.
8//!
9//! Two things key on [`canonical_type`] — this module's type index and
10//! [`TypeKey`](crate::TypeKey) — and they must agree, which is why the
11//! reduction has exactly one definition and neither spells it out itself.
12//!
13//! The ledger counts *constructing* a watched syn variant as well as matching
14//! one, so [`type_from_ident`] is here for the same reason the rest is: writing
15//! a `syn::Type::Path` is spelling, and spelling belongs to the module that owns
16//! the grammar.
17
18use std::collections::HashMap;
19
20use prebindgen::SourceLocation;
21
22/// Normalize a type to its canonical flat-namespace spelling (issue #95).
23/// The COMPLETE equivalence rule set — any spelling not listed is preserved
24/// verbatim:
25///
26/// 1. `Type::Group` / `Type::Paren` wrappers unwrap (`(Foo)` ≡ `Foo`).
27/// 2. A multi-segment path headed by `crate` / `self` reduces to its final
28/// segment, keeping that segment's generic arguments (`crate::a::Foo<T>`
29/// ≡ `Foo<T>`). Sound because the flat namespace indexes at most one
30/// item per bare ident, and a `crate::` path in a captured item can only
31/// denote the source crate's own item.
32/// 3. A multi-segment path headed by a name in `source_modules` (the
33/// `#[prebindgen]` source crates chained into the registry,
34/// hyphens-as-underscores) reduces the same way (`myflat::Foo` ≡ `Foo`).
35/// Pure callers pass `&[]`.
36/// 4. A **prelude** path reduces to the bare name the language knows it by —
37/// exactly [`Normalization::PRELUDE`], with `core`/`alloc` read as `std`.
38/// Each entry names a *constructor*, so arguments are preserved:
39/// `std::vec::Vec<Foo>` ≡ `Vec<Foo>`.
40///
41/// Nothing else. `std::ffi::CString` stays qualified, and so does a
42/// foreign path (`zenoh::KeyExpr`) **even when an alias names that
43/// type**: a `#[prebindgen] pub type` is a one-way road, bringing a
44/// foreign type into the flat API under a name that is thereafter the
45/// only way to spell it. It declares
46/// an [`Extern`](crate::flat::Extern); it is not an equivalence.
47///
48/// That keeps the rule meaning-preserving, which is the whole contract
49/// here: reduction may choose among spellings of ONE type, never change
50/// what a type is. Treating an alias as an equivalence broke that —
51/// `Vec<u8>` ≡ `Bytes` turns a sequence into an extern — and no
52/// key-shape refinement fixes the category error.
53/// 5. Lifetimes are NOT normalized (`&'a T` ≠ `&T`, `Foo<'static>` ≠ `Foo`)
54/// — a lifetime is part of the spelling a foreign-type declaration relies
55/// on (`ptr_class!(ZKeyExpr<'static>)`), so collapsing it would make two
56/// distinct declarations collide.
57///
58/// Idempotent; recurses through references, slices, tuples, pointers,
59/// generic arguments, and `impl Trait` bounds. Paths with a qualified self
60/// (`<T as Trait>::Assoc`) are left untouched.
61/// What a captured path may be reduced against: the ingested source crates' own
62/// modules, and every name an alias gives to a foreign path.
63///
64/// One value rather than a bare `&[String]`, because reduction has one rule and
65/// two sources of aliases feeding it — see [`normalize_type`]'s rule list.
66/// [`Self::default`] is the prelude alone, which is what a caller normalizing a
67/// lone type (rather than an ingested stream) wants.
68#[derive(Clone, Debug)]
69pub struct Normalization {
70 /// Module name per ingested source, first-seen order. The first doubles as the
71 /// default module for references with no recorded origin.
72 pub source_modules: Vec<String>,
73 /// Constructor path → the bare name the language knows it by, from
74 /// [`Self::PRELUDE`] alone. Matched with the use site's type arguments ignored
75 /// and preserved, because a prelude entry names a constructor:
76 /// `std::vec::Vec` is every `Vec<T>`.
77 ///
78 /// A crate's `#[prebindgen] pub type` is deliberately **not** here — see
79 /// [`normalize_type`]'s rule 4.
80 constructors: HashMap<String, String>,
81}
82
83impl Normalization {
84 /// The names the language **pre-declares**, so no source crate has to write
85 /// them — exactly Rust's own idea of a prelude, a set of `use`s you need not
86 /// write. A crate need not write `use std::vec::Vec`, and need not write
87 /// `#[prebindgen] pub type Vec = std::vec::Vec` either, for the same reason.
88 ///
89 /// Not identical to Rust's prelude: it adds `MaybeUninit`, which the grammar
90 /// recognises for out-parameters, and `Cow`, which it treats as transparent.
91 /// Its entries are exactly the bare names
92 /// [`lower_path`](crate::flat) classifies as builtins and that have a
93 /// std path at all — `str` has none, and neither do the scalars.
94 ///
95 /// Written with the `std` root; `core` and `alloc` are re-exports of the same
96 /// items, so a leading `core`/`alloc` is read as `std` before matching.
97 pub const PRELUDE: &'static [(&'static str, &'static str)] = &[
98 ("std::vec::Vec", "Vec"),
99 ("std::option::Option", "Option"),
100 ("std::result::Result", "Result"),
101 ("std::string::String", "String"),
102 ("std::boxed::Box", "Box"),
103 ("std::mem::MaybeUninit", "MaybeUninit"),
104 ("std::borrow::Cow", "Cow"),
105 ];
106
107 /// The prelude alone: no ingested sources, no declared aliases.
108 pub fn prelude() -> Self {
109 Self {
110 source_modules: Vec::new(),
111 constructors: Self::PRELUDE
112 .iter()
113 .map(|(path, name)| ((*path).to_string(), (*name).to_string()))
114 .collect(),
115 }
116 }
117
118 /// Collect from a captured stream, before anything is normalized.
119 ///
120 /// The single entry point — `FlatBuilder::build` — builds
121 /// this, so they cannot normalize differently. Gathering every module and alias
122 /// first is what makes reduction order-independent: a signature may name a type
123 /// whose alias is declared later, or in another source.
124 pub fn from_items(items: &[(syn::Item, SourceLocation)]) -> Self {
125 let mut out = Self::prelude();
126 for (_, loc) in items {
127 if let Some(crate_name) = &loc.crate_name {
128 let module = crate_name.replace('-', "_");
129 if !out.source_modules.contains(&module) {
130 out.source_modules.push(module);
131 }
132 }
133 }
134 out
135 }
136
137 /// The bare name the language knows this constructor by, arguments ignored.
138 fn constructor_of(&self, path: &syn::Path) -> Option<&str> {
139 self.constructors
140 .get(&constructor_key(path))
141 .map(String::as_str)
142 }
143}
144
145impl Default for Normalization {
146 fn default() -> Self {
147 Self::prelude()
148 }
149}
150
151/// A path as a key: segments joined, arguments dropped, and a leading
152/// `core`/`alloc` read as `std` since they re-export the same items.
153///
154/// Only [`Normalization::constructors`] is keyed this way, and a constructor is
155/// exactly a path without arguments — `std::vec::Vec` matches every `Vec<T>`.
156fn constructor_key(path: &syn::Path) -> String {
157 let mut out = String::new();
158 for (i, seg) in path.segments.iter().enumerate() {
159 if i > 0 {
160 out.push_str("::");
161 }
162 let mut ident = seg.ident.to_string();
163 if i == 0 && (ident == "core" || ident == "alloc") {
164 ident = "std".to_string();
165 }
166 out.push_str(&ident);
167 }
168 out
169}
170
171/// A type reduced to the spelling everything keys on: prelude-normalized, so
172/// `std::option::Option<T>` and `Option<T>` are one entry.
173///
174/// The **single** definition of that reduction. Two things key on it — the
175/// model's type index ([`Flat::type_ref`](crate::flat::Flat::type_ref))
176/// and [`TypeKey`](crate::TypeKey) — and they have to agree, so neither
177/// spells it out itself.
178///
179/// Deliberately `prelude()` rather than a source-module-aware normalization: a
180/// key must mean the same thing before and after ingestion knows what the source
181/// modules are.
182pub fn canonical_type(ty: &syn::Type) -> syn::Type {
183 let mut t = ty.clone();
184 normalize_type(&mut t, &Normalization::prelude());
185 t
186}
187
188/// [`canonical_type`] as tokens — the string form both indexes use as their key.
189pub fn canonical_spelling(ty: &syn::Type) -> String {
190 use quote::ToTokens;
191 canonical_type(ty).to_token_stream().to_string()
192}
193
194pub fn normalize_type(ty: &mut syn::Type, against: &Normalization) {
195 use syn::visit_mut::VisitMut;
196 struct Normalizer<'a> {
197 against: &'a Normalization,
198 }
199 impl VisitMut for Normalizer<'_> {
200 fn visit_type_mut(&mut self, ty: &mut syn::Type) {
201 // Unwrap (possibly nested) group/paren wrappers in place.
202 loop {
203 match ty {
204 syn::Type::Group(g) => *ty = (*g.elem).clone(),
205 syn::Type::Paren(p) => *ty = (*p.elem).clone(),
206 _ => break,
207 }
208 }
209 if let syn::Type::Path(tp) = ty {
210 if tp.qself.is_none() {
211 reduce_flat_path(&mut tp.path, self.against);
212 }
213 }
214 syn::visit_mut::visit_type_mut(self, ty);
215 }
216 }
217 Normalizer { against }.visit_type_mut(ty);
218}
219
220/// Apply [`normalize_type`] to every type position inside an item — fn
221/// signatures, struct fields, enum variants, const types. The ingest-time
222/// pass ([`crate::flat::FlatBuilder::build`]) that makes
223/// captured spellings canonical before any key is formed, so every
224/// downstream `TypeKey::from_type` sees the flat spelling.
225pub fn normalize_item_types(item: &mut syn::Item, against: &Normalization) {
226 use syn::visit_mut::VisitMut;
227
228 struct ItemNormalizer<'a> {
229 against: &'a Normalization,
230 }
231 impl VisitMut for ItemNormalizer<'_> {
232 fn visit_type_mut(&mut self, ty: &mut syn::Type) {
233 // Normalizes the whole subtree; no further descent needed.
234 normalize_type(ty, self.against);
235 }
236 }
237 ItemNormalizer { against }.visit_item_mut(item);
238}
239
240/// The path-reduction step of [`normalize_type`]: collapse a reducible
241/// multi-segment path to its final segment. See the rule list there.
242fn reduce_flat_path(path: &mut syn::Path, against: &Normalization) {
243 if path.segments.len() < 2 {
244 return;
245 }
246
247 // A prelude entry names a CONSTRUCTOR, so arguments are ignored when matching
248 // and preserved when rewriting: `std::vec::Vec<Foo>` is `Vec<Foo>`. A crate's
249 // own alias is NOT consulted — see rule 4.
250 if let Some(name) = against.constructor_of(path) {
251 let mut last = path.segments.last().expect("len checked").clone();
252 last.ident = syn::Ident::new(name, last.ident.span());
253 path.leading_colon = None;
254 path.segments = std::iter::once(last).collect();
255 return;
256 }
257
258 // Otherwise only a prefix into the flat namespace reduces, to the final
259 // segment: this crate's own path, or an ingested source's module.
260 let head = path
261 .segments
262 .first()
263 .expect("len checked")
264 .ident
265 .to_string();
266 let reduce = match head.as_str() {
267 "crate" | "self" => true,
268 other => against.source_modules.iter().any(|m| m == other),
269 };
270 if reduce {
271 let last = path.segments.last().expect("len checked").clone();
272 path.leading_colon = None;
273 path.segments = std::iter::once(last).collect();
274 }
275}