prebindgen_registry/write.rs
1//! Rust file emission for the resolved `Registry`.
2//!
3//! `write_rust` collects every resolved input/output converter (each entry
4//! already carries its full `ItemFn`), every per-item `on_<kind>` output,
5//! and every anonymous const; concatenates them; and hands them to
6//! `Destination::write` (which does prettyplease formatting and
7//! resolves the path against `OUT_DIR`).
8//!
9//! This module is `pub`, so **every `pub` item in it is public API of the
10//! crate**. That is meant to be exactly two — [`write_rust`] and
11//! [`WriteError`] — which is what an out-of-crate adapter calls to emit its
12//! generated file. Anything else added here stays private unless publishing it
13//! is a deliberate decision.
14
15use std::{
16 collections::BTreeMap,
17 path::{Path, PathBuf},
18};
19
20use proc_macro2::TokenStream;
21
22use crate::{
23 destination::Destination,
24 prebindgen::Prebindgen,
25 registry::{Registry, TypeEntry, TypeKey},
26};
27
28/// Errors surfaced by the file-emission phase.
29///
30/// Binding validation is NOT here — it runs once in
31/// [`RegistryBuilder::build`](crate::RegistryBuilder::build)
32/// (see [`Prebindgen::validate_resolved`]), so an invalid binding fails
33/// before a built generator exists and never reaches a writer.
34#[derive(Debug)]
35pub enum WriteError {
36 /// A `TokenStream` produced by an `on_*` trait method failed to parse
37 /// as `syn::Item`s. Indicates a codegen bug in the adapter.
38 BadTokens {
39 phase: &'static str,
40 source: syn::Error,
41 },
42}
43
44impl std::fmt::Display for WriteError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 WriteError::BadTokens { phase, source } => {
48 write!(
49 f,
50 "generated tokens from {} did not parse: {}",
51 phase, source
52 )
53 }
54 }
55 }
56}
57
58impl std::error::Error for WriteError {}
59
60/// Emit the resolved registry to a Rust file.
61///
62/// `out_path` may be relative (resolved against `OUT_DIR` by prebindgen) or
63/// absolute. Returns the path actually written.
64pub fn write_rust<P: AsRef<Path>, E: Prebindgen>(
65 registry: &Registry<E::Metadata>,
66 ext: &E,
67 out_path: P,
68) -> Result<PathBuf, WriteError> {
69 // Validation already ran ONCE in the generator's `build` — a built generator
70 // (the only source of a resolved registry) is valid by construction, so
71 // this writer is a pure emission.
72 // The capability, minted here and nowhere else in this function's reach.
73 // Every callback below is handed a borrow; nothing else in the pipeline is.
74 // See `prebindgen_flat::flat::emit` for what that buys and what it
75 // deliberately does not.
76 let emit = prebindgen_flat::Emit::new();
77 let mut items: Vec<syn::Item> = Vec::new();
78
79 // 0. Adapter prerequisites — runtime-support items (helper structs,
80 // type aliases) the converter bodies depend on. Emitted first so
81 // everything below can reference them.
82 items.extend(ext.prerequisites(registry, &emit));
83
84 // 1. Auto-generated converter wrappers (sorted by ident, deduped).
85 for (_, item_fn) in collect_converter_items(registry) {
86 items.push(syn::Item::Fn(item_fn));
87 }
88
89 // 2. Per-item Rust output from the adapter — only for items the adapter
90 // explicitly declared. Undeclared items were already announced
91 // via `cargo:warning=` by the generator's own unclaimed-item report.
92 let declared = registry.declared();
93 let declared_fns = &declared.functions;
94 let declared_types = &declared.types;
95 let flat = registry.flat();
96 items.extend(parse_items_from_tokens(
97 "on_function",
98 sorted_by_name(flat.functions().map(|f| (&f.name, f)))
99 .into_iter()
100 .filter(|(ident, _)| declared_fns.contains(*ident))
101 .map(|(_, item)| ext.on_function(item, registry, &emit)),
102 )?);
103 items.extend(parse_items_from_tokens(
104 "on_struct",
105 sorted_by_name(flat.types().filter_map(|t| match t {
106 prebindgen_flat::flat::Type::Struct(s) => Some((&s.name, s)),
107 _ => None,
108 }))
109 .into_iter()
110 .filter(|(ident, _)| declared_types.contains_key(&TypeKey::from_ident(ident)))
111 .map(|(_, item)| ext.on_struct(item, registry, &emit)),
112 )?);
113 // Both enum shapes emit through `on_enum` and sort together: they were one
114 // map here before they were two elements. They still SORT together — the
115 // emission order is one sequence — but they dispatch to their own methods
116 // now, because handing an adapter a `Type` it has to re-match is worse than
117 // handing it the element the model already decided on.
118 items.extend(parse_items_from_tokens(
119 "on_enum",
120 sorted_by_name(flat.types().filter_map(|t| match t {
121 prebindgen_flat::flat::Type::Variant(v) => Some((&v.name, t)),
122 prebindgen_flat::flat::Type::Enum(e) => Some((&e.name, t)),
123 _ => None,
124 }))
125 .into_iter()
126 .filter(|(ident, _)| declared_types.contains_key(&TypeKey::from_ident(ident)))
127 .map(|(_, t)| match t {
128 prebindgen_flat::flat::Type::Variant(v) => ext.on_variant(v, registry, &emit),
129 prebindgen_flat::flat::Type::Enum(e) => ext.on_enum(e, registry, &emit),
130 _ => unreachable!("filtered to the two enum shapes above"),
131 }),
132 )?);
133 // Consts: an adapter WITH a const declaration mechanism
134 // (`declared_consts() == Some(set)`) emits declared consts only,
135 // symmetric with functions; an adapter without one (`None`) gets every
136 // const passed through verbatim via the default `on_const`. Prebindgen's
137 // own injected feature guards are not consts at all — see the guards loop.
138 let declared_consts = &declared.consts;
139 items.extend(parse_items_from_tokens(
140 "on_const",
141 sorted_by_name(flat.constants().map(|c| (&c.name, c)))
142 .into_iter()
143 .filter(|(ident, _)| {
144 declared_consts
145 .as_ref()
146 .is_none_or(|set| set.contains(*ident))
147 })
148 .map(|(_, item)| ext.on_const(item, registry, &emit)),
149 )?);
150
151 // 3. Anonymous consts, verbatim. Last, and in stream order. Ungated on
152 // purpose: with no name there is nothing for an adapter to declare, so
153 // the const gate above cannot apply to them.
154 for guard in flat.guards() {
155 items.push(syn::Item::Const(emit.guard(guard)));
156 }
157
158 // 4. Cross-cutting post-process pass. Adapters use this to qualify
159 // bare type references etc. — see Prebindgen::post_process_item.
160 for item in &mut items {
161 ext.post_process_item(item, registry, &emit);
162 }
163
164 let dest: Destination = items.into_iter().collect();
165 Ok(dest.write(out_path))
166}
167
168/// Walk both type tables, dedupe each entry's stored `function` AND each
169/// of its [`crate::prebindgen::Stage`] functions by name, sort
170/// for determinism. Names are read directly off `function.sig.ident` —
171/// the adapter owns the naming.
172///
173/// Private: an internal step of [`write_rust`], not part of the
174/// adapter-facing surface this module exposes.
175fn collect_converter_items<M>(registry: &Registry<M>) -> Vec<(syn::Ident, syn::ItemFn)> {
176 let mut by_name: BTreeMap<String, (syn::Ident, syn::ItemFn)> = BTreeMap::new();
177 let mut collect = |entry: &TypeEntry<M>| {
178 let name = entry.function.sig.ident.clone();
179 by_name
180 .entry(name.to_string())
181 .or_insert_with(|| (name, entry.function.clone()));
182 for stage in &entry.pre_stages {
183 let sname = stage.function.sig.ident.clone();
184 by_name
185 .entry(sname.to_string())
186 .or_insert_with(|| (sname, stage.function.clone()));
187 }
188 };
189 walk_resolved(®istry.input_types, |_, entry| collect(entry));
190 walk_resolved(®istry.output_types, |_, entry| collect(entry));
191 by_name.into_values().collect()
192}
193
194fn walk_resolved<M, F: FnMut(&TypeKey, &TypeEntry<M>)>(
195 table: &std::collections::HashMap<TypeKey, crate::registry::TypeCell<M>>,
196 mut f: F,
197) {
198 let mut keys: Vec<&TypeKey> = table.keys().collect();
199 keys.sort_by(|a, b| a.as_str().cmp(b.as_str()));
200 for key in keys {
201 if let Some(entry) = table.get(key).and_then(|c| c.entry.as_ref()) {
202 f(key, entry);
203 }
204 }
205}
206
207/// Name-sorted, because emission order is part of the generated file and the
208/// model is in source order. Was `sorted_items_by_ident` over the registry's
209/// maps; same ordering, read from the one index.
210fn sorted_by_name<'a, T>(
211 items: impl Iterator<Item = (&'a syn::Ident, &'a T)>,
212) -> Vec<(&'a syn::Ident, &'a T)>
213where
214 T: 'a,
215{
216 let mut items: Vec<(&syn::Ident, &T)> = items.collect();
217 items.sort_by_key(|(left, _)| left.to_string());
218 items
219}
220
221/// Parse a per-item `TokenStream` (which may be empty) as a sequence of
222/// `syn::Item`s. Empty token streams yield zero items.
223fn parse_items_from_tokens<I: IntoIterator<Item = TokenStream>>(
224 phase: &'static str,
225 iter: I,
226) -> Result<Vec<syn::Item>, WriteError> {
227 let mut out = Vec::new();
228 for ts in iter {
229 if ts.is_empty() {
230 continue;
231 }
232 let file: syn::File =
233 syn::parse2(ts.clone()).map_err(|source| WriteError::BadTokens { phase, source })?;
234 out.extend(file.items);
235 }
236 Ok(out)
237}
238
239#[cfg(test)]
240mod tests;