Skip to main content

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}