Skip to main content

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}