Expand description
The flat model itself lives in the separate prebindgen-flat crate —
re-exported here so an adapter names one crate root for the whole
pipeline.
The prebindgen source language: one parser from captured records to
Elements.
Naming:
flatis the source side — the flat API a#[prebindgen]crate may write. The destination adapters (C, JNI) ship as the separateprebindgen-c/prebindgen-jnicrates. They are opposite ends of the pipeline.
Source(s) ──items──> Flat ──Elements──> Registry ──> adapters
raw records parse + indexes classify off `kind`
(syn::Item) validate elements spell with `spell()`FlatBuilder::source folds the first arrow in for the common case, so a build
script names one directory and gets elements; FlatBuilder::items keeps the
arrow itself, for a stream that needs shaping first.
§What an element is
Two things at once, and that pairing is the whole design:
- a closed model —
TypeKind, the field list, which of the two enum shapes an item is — where the type grammar is the accepted Rust syntax and the element structure is the concept above it; - one
Origin, carrying the exact syntax the node was built from and the source it arrived in.
The Origin is uniform: every node has one, at every level — item,
parameter, field, variant, type, array extent. Some levels know less than
others (a field has no line of its own, so it shares its item’s), but the
shape does not change with the level, and no level copies a piece of
provenance down from the one above. That copying is what previously let the
same crate name appear under three field names with two meanings.
So the rule for every consumer is:
Classify off
kind, spell withspell().
And the model enforces it rather than asking. Origin’s syntax is
private, and every route to it — spell(), as_syn() — is visible only
inside this crate. Matching a syn::Type or syn::Expr variant outside
this module is a classifier, and issue #211 says classification lives here
alone; the visibility is what makes that hold rather than a convention
anyone has to remember. Code whose job is producing Rust reaches the
syntax through Emit, which the registry pipeline
(prebindgen-registry’s write_rust) hands only to the emission
callbacks.
§What earns a variant
For a type, a Rust form — and nothing else. TypeKind is the accepted
subset of syn::Type, so two spellings are two variants even when every
destination language would treat them alike. Deciding that &str and
String are both “a string” is a destination’s decision, taken in an
adapter, on a reading the model provides:
| Rust writes | The model says | The reading, where a consumer wants one |
|---|---|---|
String, str | String, Str | the adapter’s, at its own site |
Vec<T>, [T] | Vec, Slice | TypeRef::sequence_elem — one run of T |
Box<T>, Cow<'_, T> | Boxed, Cow | TypeRef::unwrapped — a T either way |
&mut MaybeUninit<T> | Ref over Uninit | TypeRef::borrow_target — the value, not its slot |
no ->, -> () | TypeKind::Unit | the same function |
*const T | rejected | a source crate is idiomatic Rust; the adapter owns pointers |
It buys one property: the syntax is recoverable from the kind (checked, over the whole acceptance corpus, by rebuilding it). Which is the difference between a slice that rides along because it is exact, and one the model cannot do without.
An element is not a type, and there the rule is still the concept:
| Rust writes | The model says | Because |
|---|---|---|
struct S;, struct S {} | zero fields | the delimiters are spelling |
enum E { A(u8) } | Variant | a sum, identified by position |
enum E { A = 7 } | Enum | a named integer, identified by its value |
type X = .., struct X(..) | Extern | named here; contents not modelled |
The two enum shapes are the clearest case of a concept splitting where Rust
has one spelling. Both are enum and both keep a syn::ItemEnum, but a sum’s
alternatives are identified by position — the mirror an adapter builds
carries no repr and numbers its own arms — while a fieldless enum’s members
are identified by the value Rust assigns, which a C header re-states and a
Kotlin enum class entry carries. Neither numbering means anything for the
other shape, so one model covering both would carry a field that is dead in
each direction, and worse than dead: Rust does assign a discriminant to a
sum’s alternatives, and using it would be wrong.
The identities follow the same rule: a nominal type is a TypeId — a
name — not a syn::Path, so nothing downstream has to take a path apart to
learn what a type is. And a name is all it is: a reference carries a
name, the declaration carries the origin, so the same type never compares
unequal to itself because two source crates mentioned it. The one place a
crate name rides with an identity is ConstId, and that is the const’s
declaring crate, resolved by lookup — which is exactly what lets an array
extent refuse a const from another source.
§Why the syntax rides along
The generated Rust glue is itself a destination artifact, and the only one
that needs syntax fidelity: B() must not be re-spelled B, = 0x07 must
not become = 7. Carrying the source’s own slice is how it gets that —
exactly, and at no modelling cost, so a delimiter and a literal’s base need
never become fields.
For a type the slice is no longer where facts go to survive:
TypeKind keeps the lifetime, the wrapper and the argument it once
dropped, and rebuilding the syntax from it is the round-trip that says
so. What is
left is the reason a slice beats a reconstruction anywhere — it is what the
source wrote, and it is already there.
§Where acceptance is enforced
Lowering is total over the accepted grammar: a form with no variant in
TypeKind is a form the language does not accept, so there is no second
acceptance list to drift from it. One rule cannot be stated that way and is
stated in the lowering instead: Uninit is accepted only
directly under a &mut, which is a fact about a position and not about a
form.
Parsing diagnoses; ingestion raises. Those are two different points, and the split is what lets one model serve both.
Parsing never fails on a single item: an item the language cannot express
becomes Element::Unsupported carrying its diagnosis, and
FlatBuilder::build returns the model with it in place. Only whole-stream
rules — a duplicate name in the flat namespace — are ParseErrors, because
no declaration can make two items with one name unambiguous. So a consumer
that wants to inspect what a source crate marked, refusals included, gets
exactly that from Flat::unsupported.
Registry ingestion is where the diagnoses are raised.
Building a registry from this model fails if any element is
Unsupported — all of them at once, so a source crate that needs migrating
sees one list rather than one rebuild per item — and it fails before any
adapter declaration is examined. A binding is built against a model the
frontend could read in full, or it is not built.
No marked item passes through verbatim, because a #[prebindgen] crate
marks the items that cross the boundary and leaves the supporting code to the
consumer. The proc-macro already enforces that — a use, mod, impl or
macro_rules! cannot be marked at all — so the only item kind left that this
module does not model is a union, and it is diagnosed like anything else the
language cannot express.
The one item that is re-emitted verbatim is a Guard — an anonymous
const, which has no address and so cannot be part of an API addressed by name.
Today these are the feature checks Source injects on its own
behalf. Modelled rather than dropped because this module must be total over
what it is handed, and a separate element so nothing that consumes the API has
to remember to skip it.
§Declaring a handle
#[prebindgen] pub type X = path::To<Thing>; declares an Extern: it gives
a foreign or crate-private type a name in the flat API without claiming
anything about its contents. That is what makes the API closable — a handle
enters it deliberately rather than by being mentioned — and it is why a
reference can be required to resolve. A marked tuple struct declares the same
thing, since no adapter has ever crossed its fields.
§Shapes that must be refused rather than approximated
An Element holds what it holds: ordinary parameters, a direct return, no
generic binder. A shape with no slot in that structure cannot be partly
accepted — the missing piece would simply be dropped, and silently:
| Shape | Would become | So |
|---|---|---|
async fn | a function returning () | the future is dropped and the export’s body never runs |
fn f(a: u8, ...) | a function without the tail | the variadic arguments vanish |
struct S<T>, fn f<T>(), struct S<const N: usize> | T as a nominal reference | a parameter is indistinguishable from an item named T |
All three are ItemErrors, carried like any other refusal and raised at
registry ingestion. A
lifetime binder is not among them: lifetimes are spelling, and the
spelling already travels. Nor is impl Trait in argument position — Rust
calls it an anonymous type parameter, but it is not a binder in the syntax,
so the callback form is untouched.
Modules§
Structs§
- Alternative
- One alternative of a
Variant. - Array
Extent - A fixed-size array’s extent: the number, the const identity when the source named one, and the spelling it was written with.
- ConstId
- A
#[prebindgen]const, identified the way the flat namespace identifies everything: by name, plus the crate it was marked in. - Constant
- A
#[prebindgen]constant. - Duplicate
Name - The two items of a
ParseError::DuplicateName. - Enum
- A
#[prebindgen]enum whose every alternative is fieldless — the C-style shape, a named set of integers. - Enum
Value - One named value of an
Enum. - Extern
- A type the flat API names but whose contents it does not model.
- Field
- One field of a
Structor of anAlternative. - Flat
- The flat API: every
#[prebindgen]item from every ingested source, parsed, indexed by name, and with every type reference resolved. - Flat
Builder - Collects what to parse, then hands over the model.
- Function
- A
#[prebindgen]free function. - Guard
- An anonymous const:
const _: T = .., whatever produced it. - Origin
- The syntax a node was built from, plus where that syntax came from.
- Param
- One parameter of a
Function. - Struct
- A
#[prebindgen]struct: a product of fields that cross the boundary. - TypeId
- A nominal type’s identity: a name, and nothing else.
- TypeKey
- Canonical type-shape key: identity is the token string of the
normalized type. Normalization is a closed rule set — group/paren
unwrap, a
crate::/self::/source-module path reduced to its final segment, and a prelude path read as the bare name the language knows it by (std::vec::Vec<Foo>≡Vec<Foo>) — and any spelling it does not cover is kept verbatim. - Type
KeyParse Error - Structured failure of
TypeKey::parse: the offending input plus the underlyingsynparse error. - TypeRef
- A type as the language accepted it, plus the exact syntax it came from.
- Unsupported
- An item the language cannot express.
- Unsupported
Array Len - A length the prebindgen source language does not accept.
- Unsupported
Type - A type the prebindgen source language does not accept.
- Variant
- A
#[prebindgen]enum whose alternatives carry payloads — a sum type.
Enums§
- Array
LenReason - Why an array length was refused.
- Element
- One member of the flat API.
- Extent
Source - How an
ArrayExtentwas addressed at its use site. - Generic
Arg - One generic argument of a
Namedtype, as written. - Item
Error - Why one item could not be expressed in the language.
- Parse
Error - A rule of the language that no single item can satisfy on its own, and that no adapter declaration can excuse.
- Scalar
Kind - The primitives the source language accepts. Mirrors the set every adapter already treats as directly representable.
- Type
- A type the flat API declares.
- Type
Kind - The accepted syntax of a
TypeRef: the subset ofsyn::Typea#[prebindgen]crate may write, and nothing more. - Unsupported
Type Reason - Why a type was refused.
Constants§
- TRANSPARENT_
WRAPPERS - Lower one captured type.
Traits§
- Name
- A name a lookup can be performed with.
Functions§
- canonical_
spelling canonical_typeas tokens — the string form both indexes use as their key.- canonical_
type - A type reduced to the spelling everything keys on: prelude-normalized, so
std::option::Option<T>andOption<T>are one entry. - extract_
fn_ trait_ args - If
tyisimpl Fn(T1, T2, ...) + Send + Sync + 'static, return theFnargument types in declaration order. OtherwiseNone. - peel_
transparent - Strip one transparent wrapper from a spelling,
naming the one removed —
Box<Option<T>>→("Box", Option<T>).