prebindgen_flat/flat/array_len.rs
1//! The fixed-size-array length subgrammar: one closed representation and one
2//! fallible walk that produces it.
3//!
4//! A length must reduce to a **known number** — a generator runs in `build.rs`,
5//! where it cannot evaluate arbitrary Rust. Two spellings reach one, and nothing
6//! else does: an integer literal, or the bare name of a `#[prebindgen]` const
7//! whose own initializer is an integer literal.
8//!
9//! Both the number and the const identity travel, as an [`ArrayExtent`]: the
10//! value is the semantic length that makes `[u8; A]` and `[u8; 4]` one type, and
11//! the identity is what lets a C header emit `uint8_t x[NAME]`. The *spelling*
12//! travels separately and always — it is in the [`Type::syntax`](super::Type)
13//! slice of the array type itself — so this carries only what a destination
14//! language cannot read off the source.
15//!
16//! Ported from #212, which introduced it for issue #210.
17
18use std::{collections::HashMap, fmt, rc::Rc};
19
20use prebindgen::SourceLocation;
21use quote::ToTokens;
22
23use super::origin::Origin;
24
25/// A length the prebindgen source language does not accept.
26///
27/// Names the offending sub-expression, not just the array: the point of the
28/// single walk is that it knows exactly which part it could not lower.
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct UnsupportedArrayLen {
31 /// The array type as written, for context — `[u8 ; A + 1]`.
32 pub array: String,
33 /// The sub-expression that could not be lowered — `A + 1`.
34 pub offending: String,
35 /// Why it could not be lowered.
36 pub reason: ArrayLenReason,
37}
38
39/// Why an array length was refused.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub enum ArrayLenReason {
42 /// Not a literal and not a plain name: arithmetic, a cast, a call, a block,
43 /// a `match`, a closure — anything with structure the grammar lacks.
44 NotLiteralOrName,
45 /// A literal that is not a non-negative integer.
46 NotAnIntegerLiteral,
47 /// An integer literal too large for `usize`.
48 IntegerOutOfRange,
49 /// A path with more than one segment, a qualified self, or a leading `::` —
50 /// `crate::limits::MAX`, `usize::MAX`, `<Holder>::N`, `::MAX`.
51 ///
52 /// A length names a `#[prebindgen]` const, and those live in one flat,
53 /// uniquely-named namespace, so the bare name is the complete address. Any
54 /// longer path either restates that (`crate::limits::MAX`) or reaches
55 /// somewhere the frontend cannot follow — a module it does not index, an
56 /// associated const it never captured, a foreign crate. Neither can be
57 /// reduced to a number, and guessing between them is how a length silently
58 /// becomes the wrong one.
59 NotABareName,
60 /// A bare name that is not a `#[prebindgen]` const.
61 ///
62 /// The generated crate sees **only** what the macro exposed, so an unmarked
63 /// const is not merely unqualifiable — it does not exist downstream.
64 NotAMarkedConst,
65 /// A `#[prebindgen]` const whose own initializer is not an integer literal.
66 ///
67 /// `build.rs` cannot evaluate it, and a destination language that needs the
68 /// count cannot either. Hoist the arithmetic into the value the const is
69 /// computed FROM, or write the number.
70 ConstIsNotALiteral,
71 /// A `#[prebindgen]` const from a different source crate than the item
72 /// using it.
73 ///
74 /// Uniqueness holds across the *marked* namespace only, so a bare name in
75 /// one source crate can collide with an unmarked name of its own — the
76 /// frontend would silently bind to the other crate's value. Requiring the
77 /// length's const to come from the item's own crate makes that
78 /// unrepresentable.
79 ForeignSourceConst {
80 /// Crate the const was marked in.
81 const_crate: String,
82 /// Crate the item using it came from.
83 item_crate: String,
84 },
85}
86
87impl fmt::Display for UnsupportedArrayLen {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 let what = match &self.reason {
90 ArrayLenReason::NotLiteralOrName => {
91 "is neither an integer literal nor the name of a const".to_string()
92 }
93 ArrayLenReason::NotAnIntegerLiteral => {
94 "is not a non-negative integer literal".to_string()
95 }
96 ArrayLenReason::IntegerOutOfRange => "does not fit in a `usize`".to_string(),
97 ArrayLenReason::NotABareName => {
98 "is a path rather than a bare name; `#[prebindgen]` items live in one flat \
99 namespace, so the bare name is the whole address"
100 .to_string()
101 }
102 ArrayLenReason::NotAMarkedConst => {
103 "names no `#[prebindgen]` const — the generated crate sees only what the macro \
104 exposed, so mark it `#[prebindgen]`"
105 .to_string()
106 }
107 ArrayLenReason::ConstIsNotALiteral => {
108 "names a const whose value is not an integer literal, so `build.rs` cannot \
109 evaluate it"
110 .to_string()
111 }
112 ArrayLenReason::ForeignSourceConst {
113 const_crate,
114 item_crate,
115 } => format!(
116 "names a const marked in `{const_crate}`, but the item using it comes from \
117 `{item_crate}` — a length must name a const from its own source crate"
118 ),
119 };
120 write!(
121 f,
122 "fixed-size array `{}`: the length `{}` {what}. A length must be an integer literal, \
123 or the bare name of a `#[prebindgen]` const that is itself an integer literal \
124 (`pub const N: usize = 4;`) — a generator runs in `build.rs` and cannot evaluate \
125 anything else, and some destination languages need the count as a number.",
126 self.array, self.offending
127 )
128 }
129}
130
131impl std::error::Error for UnsupportedArrayLen {}
132
133/// A fixed-size array's extent: the number, the const identity when the source
134/// named one, and the spelling it was written with.
135///
136/// # Three facts, three consumers — and no `==`
137///
138/// The three answer different questions, and **deliberately no equality is
139/// provided**, because there is no single one that could be right. A consumer
140/// projects the fact it actually needs:
141///
142/// | Question | Projection |
143/// |---|---|
144/// | is this the same type / the same converter? | [`Self::value`] |
145/// | how does a C declaration spell it? | [`Self::origin`]`.syntax`, per occurrence |
146/// | which consts must reach the header as a `#define`? | [`Self::const_id`] |
147///
148/// A blanket `==` mixes them and is wrong under either reading: comparing
149/// `source` makes `[u8; A]` differ from `[u8; 4]` when `A == 4` — one Rust type
150/// reported as two — while ignoring the spelling makes `[u8; 4]` equal
151/// `[u8; 0x04]`, whose retained syntax differs. Neither is type identity and
152/// neither is spelling identity, so the choice belongs to whoever is asking.
153///
154/// # Note for a converter table
155///
156/// `value` being the type identity means several occurrences share one
157/// converter, and their spellings differ. A shared converter therefore needs a
158/// **canonical** Rust spelling chosen on purpose — the evaluated literal is the
159/// obvious one — rather than whichever occurrence happened to populate a
160/// deduplicated entry.
161///
162/// This lives on the **use site** — a field's or parameter's type — and never on
163/// anything keyed by type, for the same reason: two occurrences of one type may
164/// name the length differently, so a type-keyed table could only report
165/// whichever was stored last.
166#[derive(Clone, Debug)]
167pub struct ArrayExtent {
168 /// The evaluated length. The type identity: `[u8; A]` and `[u8; 4]` are one
169 /// Rust type when `A == 4`, and a destination language with no way to name a
170 /// Rust const needs the number.
171 pub value: usize,
172 /// How the length was addressed, so a C header can re-state
173 /// `uint8_t tag[TAG_LEN]` and know `TAG_LEN` must reach it.
174 pub source: ExtentSource,
175 /// The length expression as written — `4`, `0x04`, `TAG_LEN` — and where it
176 /// came from. The spelling of *this* occurrence, which is what a declaration
177 /// re-emits; two occurrences of one type may differ here.
178 pub origin: Origin<syn::Expr>,
179}
180
181/// How an [`ArrayExtent`] was addressed at its use site.
182#[derive(Clone, Debug, PartialEq, Eq)]
183pub enum ExtentSource {
184 /// Written as an integer literal — `[u8; 4]`.
185 Literal,
186 /// Written as the name of a `#[prebindgen]` const — `[u8; TAG_LEN]`.
187 Const(ConstId),
188}
189
190/// A `#[prebindgen]` const, identified the way the flat namespace identifies
191/// everything: by name, plus the crate it was marked in.
192#[derive(Clone, Debug, PartialEq, Eq)]
193pub struct ConstId {
194 pub name: String,
195 /// Crate the const was **declared** in, resolved by looking the name up
196 /// among the captured consts — never assumed from the use site. That is
197 /// what lets an extent refuse a const from another source crate.
198 ///
199 /// A bare crate name, not an [`Origin`]: it describes a *different* item
200 /// than the one being lowered, so it is not that node's provenance.
201 pub crate_name: Option<String>,
202}
203
204impl ArrayExtent {
205 /// The const this extent named, if it named one.
206 pub fn const_id(&self) -> Option<&ConstId> {
207 match &self.source {
208 ExtentSource::Literal => None,
209 ExtentSource::Const(id) => Some(id),
210 }
211 }
212}
213
214/// One `#[prebindgen]` const, as a length sees it.
215struct ConstEntry {
216 /// The literal value, or `None` when the initializer is not one. Present
217 /// either way, so "not a const" and "not a usable const" stay distinct
218 /// diagnostics.
219 value: Option<usize>,
220 /// Crate the const was marked in; `None` for an unstamped stream. Named
221 /// for what it is — a crate, not an [`Origin`], which belongs to the node
222 /// being lowered rather than to some other item it names.
223 crate_name: Option<String>,
224}
225
226/// The `#[prebindgen]` consts a length may name.
227///
228/// Built once per parse, before any type is lowered, so a const may be declared
229/// after the item that uses it. Deliberately holds **only consts**: nothing else
230/// can be a length now that the grammar is a bare name, so there is no item-kind
231/// enumeration here to drift.
232pub(crate) struct ConstIndex {
233 consts: HashMap<String, ConstEntry>,
234}
235
236impl ConstIndex {
237 /// `consts` maps each `#[prebindgen]` const's name to its initializer and
238 /// the crate it was marked in.
239 pub(crate) fn new<I>(consts: I) -> Self
240 where
241 I: IntoIterator<Item = (String, syn::Expr, Option<String>)>,
242 {
243 Self {
244 consts: consts
245 .into_iter()
246 .map(|(name, expr, crate_name)| {
247 let entry = ConstEntry {
248 value: int_literal(&expr),
249 crate_name,
250 };
251 (name, entry)
252 })
253 .collect(),
254 }
255 }
256}
257
258/// The `usize` an expression denotes, if it is plainly an integer literal.
259fn int_literal(expr: &syn::Expr) -> Option<usize> {
260 let syn::Expr::Lit(lit) = expr else {
261 return None;
262 };
263 let syn::Lit::Int(int) = &lit.lit else {
264 return None;
265 };
266 int.base10_parse::<usize>().ok()
267}
268
269/// Lower one array length to its closed representation.
270///
271/// **The contract**: `Ok` means the length was fully understood AND reduced to a
272/// number. There is no separate acceptance check to drift from this — a form
273/// this function does not lower is, by construction, a form the language does
274/// not accept. That is the fix for the validator/rewriter pair this replaces
275/// (issue #210), where eight defects in a row were two walks disagreeing about
276/// one input.
277///
278/// `array` is the array type's rendered form, for diagnostics, and `at` the
279/// origin of the item the length was written in — which both becomes the
280/// extent's own origin and pins which crate a named const may come from.
281pub(crate) fn lower_array_len(
282 len: &syn::Expr,
283 array: &str,
284 at: &Rc<SourceLocation>,
285 consts: &ConstIndex,
286) -> Result<ArrayExtent, UnsupportedArrayLen> {
287 let item_crate = at.crate_name.as_deref();
288 let origin = || Origin::new(len.clone(), Rc::clone(at));
289 let fail = |reason| UnsupportedArrayLen {
290 array: array.to_string(),
291 offending: len.to_token_stream().to_string(),
292 reason,
293 };
294 match len {
295 syn::Expr::Lit(_) => match int_literal(len) {
296 Some(value) => Ok(ArrayExtent {
297 value,
298 source: ExtentSource::Literal,
299 origin: origin(),
300 }),
301 None => Err(fail(match len {
302 // Told apart so an out-of-range integer does not report as
303 // "not an integer".
304 syn::Expr::Lit(l) if matches!(l.lit, syn::Lit::Int(_)) => {
305 ArrayLenReason::IntegerOutOfRange
306 }
307 _ => ArrayLenReason::NotAnIntegerLiteral,
308 })),
309 },
310 syn::Expr::Path(ep) => {
311 // A bare name, and nothing longer. See `NotABareName` for why the
312 // flat namespace makes every longer path either redundant or
313 // unfollowable.
314 if ep.qself.is_some() || ep.path.leading_colon.is_some() || ep.path.segments.len() != 1
315 {
316 return Err(fail(ArrayLenReason::NotABareName));
317 }
318 let name = ep.path.segments[0].ident.to_string();
319 let Some(entry) = consts.consts.get(&name) else {
320 return Err(fail(ArrayLenReason::NotAMarkedConst));
321 };
322 // Provenance before value: a same-named const from another source
323 // may well be a literal, and using it would be the silent wrong
324 // answer rather than an error.
325 if entry.crate_name.as_deref() != item_crate {
326 return Err(fail(ArrayLenReason::ForeignSourceConst {
327 const_crate: entry
328 .crate_name
329 .clone()
330 .unwrap_or_else(|| "<unstamped>".into()),
331 item_crate: item_crate.unwrap_or("<unstamped>").to_string(),
332 }));
333 }
334 let Some(value) = entry.value else {
335 return Err(fail(ArrayLenReason::ConstIsNotALiteral));
336 };
337 Ok(ArrayExtent {
338 value,
339 source: ExtentSource::Const(ConstId {
340 name,
341 crate_name: entry.crate_name.clone(),
342 }),
343 origin: origin(),
344 })
345 }
346 _ => Err(fail(ArrayLenReason::NotLiteralOrName)),
347 }
348}