From 0de7e41a99b7ed05691acaa773f6518c926d583a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Wed, 29 Jul 2026 18:55:18 +0200 Subject: [PATCH 01/52] Map the Language integration program MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Umbrella document for making every prebindgen component consume `Element`s instead of parsing captured Rust itself: the design and the rule it turns on, the measured size of the problem (202 classification sites, 113 registry map reads), the stage order L0–L5, and the completion criteria restated from #211. This file is the authority on stage state; the umbrella PR body mirrors it. Refs #211. Co-Authored-By: Claude Opus 5 --- docs/language-integration.md | 233 +++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 docs/language-integration.md diff --git a/docs/language-integration.md b/docs/language-integration.md new file mode 100644 index 00000000..57e02c59 --- /dev/null +++ b/docs/language-integration.md @@ -0,0 +1,233 @@ +# Parse once, consume elements everywhere — integration map + +Umbrella for making every component of prebindgen consume +[`core::language`]'s `Element`s instead of parsing captured Rust itself. + +[#211](https://github.com/milyin/prebindgen/issues/211) remains the authority on +the invariants and the frontend/adapter boundary. This document does not restate +them; it records the design this program follows, what has landed, and the order. + +This file is the one place stage state is edited. Change the doc, then re-sync +the umbrella PR body — never the other way round. + +## The design + +``` +Source(s) ──items──> Language ──Elements──> Registry ──> adapters + raw records parse + indexes classify off `kind` + (syn::Item) validate elements spell off `syntax` +``` + +An `Element` is two things at once, and the pairing is the whole point: + +* a **closed classification** — `TypeKind`, `StructFields`, the variant list — + that says what the source *means*, in terms every destination language shares; +* the **exact syntax** each part was built from, sliced down to the parameter, + field, variant and type. + +```rust +pub struct Type { pub kind: TypeKind, pub syntax: syn::Type } +pub struct Param { pub name: syn::Ident, pub ty: Type, pub syntax: syn::PatType } +pub struct Variant { pub tag: i32, pub discriminant: Option, pub fields: Vec, + pub syntax: syn::Variant } +``` + +### Why the syntax rides along + +The predecessor design ([#215](https://github.com/milyin/prebindgen/issues/215)) +built a **syn-free** semantic model and kept hitting one wall: the generated Rust +glue is itself a destination artifact, and it is the only consumer that needs +syntax fidelity. Each time it did, the answer was to model the syntax — +`DiscriminantSource::Explicit(syn::Expr)`, `syn::Member`, `syn::Lifetime`, +`to_syn()`, and finally a `VariantShape` whose only job was to make generated +Rust spell `E::B()` instead of `E::B`. A model that carries no syntax has to +become *lossless* to serve that consumer, which is how a language-neutral IR +turns back into a second `syn`. + +Carrying the original slice costs nothing and removes the pressure, so the +classification stays small and genuinely neutral: + +| Fact | Where it lives | Who reads it | +|---|---|---| +| `B()` vs `B` | `Variant::syntax` (via `Variant::spell`) | generated Rust only | +| `= 0x07` vs `= 7` | `Variant::syntax.discriminant` | a C mirror re-emits it | +| the number 7 | `Variant::discriminant` | Kotlin `NAME(7)`, `jint` decode | +| `Foo<'a, T>` | `Type::syntax` | generated Rust only | +| "it is a `Foo` with one type argument" | `TypeKind::Named` | every adapter | +| `[u8; TAG_LEN]` — spelling / number / const identity | `Type::syntax` / `ArrayExtent::value` / `ExtentSource::Const` | C header / Kotlin / both | + +### The rule + +> **Classify off `kind`, spell off `syntax`.** +> +> Matching a `syn::Type` or `syn::Expr` variant outside `core::language` is a +> classifier, and #211 says classification lives there alone. Passing a `syntax` +> slice into `quote!` is spelling, and spelling the source is exactly what +> generated Rust must do. + +This is mechanically measured, and needed no new mechanism: +`core::language::boundary` (ported from +[#224](https://github.com/milyin/prebindgen/pull/224)) counts *variant mentions* +of watched syn enums per file, so `quote!(#slice)` is invisible to it while +`matches!(ty, syn::Type::Reference(_))` is counted. The committed ledger is the +scoreboard for this whole program. + +## Size of the problem + +Seeded by L0 at **202 classification sites** outside `core::language`, plus +**113** reads of the registry's `syn`-keyed item maps: + +| Area | Ledger sites | Registry map reads | Stage | +|---|---:|---:|---| +| `api/core` (`types_util` 40, `unfold` 15, `registry` 13, `expand` 4) | 72 | 39 | L2 | +| `api/lang/cbindgen` | 25 | 25 | L3 | +| `api/lang/jnigen` | 105 | 49 | L4 | +| **total** | **202** | **113** | | + +Not every site must go: some inspect types the adapter itself *synthesized* — +wire types, converter signatures — which is legitimately the adapter's business. +Separating the two populations is not a document to write up front; it is each +entry's fate as it comes off the ledger, with a stated reason in the PR that +moves it. + +## Stages + +| Stage | Owns | State | +|---|---|---| +| L0 | `Language` + `Element` + the ledger | **done** — [#227](https://github.com/milyin/prebindgen/pull/227) | +| L1 | `Registry` consumes elements | not started | +| L2 | `api/core` stops classifying source syntax | not started | +| L3 | `Cbindgen` consumes elements | not started | +| L4 | `JniGen` consumes elements *(the long pole — 105 sites)* | not started | +| L5 | Close the seam: the public contract stops being `syn` | not started | + +### L0 — the parser — **done** (#227) + +- [x] `Language::parse` over any `(syn::Item, SourceLocation)` stream — the seam + `Registry::from_items` occupies, so multi-source composition is unchanged +- [x] `Element` = `Function | Struct | Enum | Const | Unsupported | Passthrough`, + every element and component carrying its syntax slice +- [x] `Type { kind, syntax }`; lowering total over the accepted grammar +- [x] The array-length subgrammar and `ArrayExtent`, ported from #212 +- [x] Enum tag / discriminant numbering, ported from #226, with `checked_add` +- [x] Round-trip tests: syntax slices are the source's tokens, including the + cases a reconstruction loses (empty delimiters, `0x07`, lifetimes, docs) +- [x] Acceptance matrix: spelling → element, or a diagnosis naming the item + **and** the component +- [x] Boundary ledger ported (#224) and seeded + +**Acceptance is preserved, not expanded.** An item the language cannot express +becomes `Element::Unsupported` carrying its diagnosis, because the pipeline has +always scanned a signature only once an adapter declared it, and a source crate +may mark items no binding uses. Only a duplicate name — which no declaration can +disambiguate — fails the parse. Tuple-struct fields stay unmodelled for the same +reason. + +### L1 — `Registry` consumes elements + +The seam that makes the direction real. Adapters must not need touching. + +- [ ] `Registry::from_elements(Vec)`; `from_items` becomes + `Language::parse` + `from_elements`, so both entry points share one parser +- [ ] The `functions` / `structs` / `enums` / `consts` / `passthrough` maps are + rebuilt from each element's retained `syntax` — a projection, not a second + source of truth +- [ ] `scan_fn_signature`'s receiver / parameter-pattern / `impl Trait` guards + are deleted: the diagnosis is already on `Element::Unsupported`, and + declaring such an item is what raises it +- [ ] `ScanError`'s per-item variants map onto `ItemError`, so one authority + produces the message +- [ ] Elements are indexed by name so L2–L4 can ask for them +- [ ] **Must not move**: every generated artifact byte-identical + (`examples/regen-check.sh`) + +### L2 — `api/core` stops classifying source syntax + +- [ ] `types_util` — 40 sites, the largest single file. `normalize_type`, + `immediate_pattern_children`, `match_pattern`, the `is_*` predicates +- [ ] `registry::immediate_subtype_positions` — near-duplicate of + `immediate_pattern_children`, and the two already diverge on `Type::Path` +- [ ] `unfold` (15) and `expand` (4) read element types +- [ ] `TypeKey` derivable from a `Type` so a lookup stops routing through a + spelling +- [ ] Ledger down by the migrated count; every entry that *stays* is justified in + the PR as adapter-synthesized + +### L3 — `Cbindgen` consumes elements + +- [ ] `builder` (8), `trait_impl` (6), `emit` (5), `mod` (5), `convert` (1) +- [ ] Variant patterns and constructors come from `Variant::spell`, not from + re-deriving delimiters +- [ ] A discriminant is re-emitted from `Variant::syntax`, and the number comes + from `Variant::discriminant` +- [ ] Generated C artifacts byte-identical + +### L4 — `JniGen` consumes elements + +The long pole. Split by area, each PR independently green. + +- [ ] `emit/names` (17), `jni/builder` (13), `jni/trait_impl` (11), + `emit/wrapper` (11), `emit/flat_input` (10), `render` (8), `selector` (7), + and the rest +- [ ] `classify.rs` — a whole classifier with **zero** watched sites, so the + ledger cannot see it: it must be migrated on its own merit +- [ ] `prim_array_of` reads `ArrayExtent` instead of re-matching `Type::Array` +- [ ] Generated Rust and Kotlin byte-identical + +### L5 — close the seam + +The public contract stops being `syn`, which is what stops the population from +growing back. + +- [ ] `Registry`'s public item maps stop being the adapter-facing contract — + relates to [#92](https://github.com/milyin/prebindgen/issues/92) +- [ ] `Prebindgen::post_process_item(&mut syn::Item)` — the hook that let + qualification live in an adapter in the first place +- [ ] `ConverterImpl::function` / `TypeEntry::function` as `syn::ItemFn`; + `prerequisites` / `local_functions` returning raw items +- [ ] `Niches { value: syn::Expr, matches: syn::Expr }` — a semantic fact carried + as raw expression syntax +- [ ] Extend the ledger's `WATCHED` beyond `Type` / `Expr` — `Item`, `Fields`, + `FnArg`, `ReturnType`, `GenericArgument`, `Pat` — one enum at a time, each + addition a regenerated ledger whose diff *is* the decision +- [ ] Close or accept the blind spots the ledger header lists (token-string + classification, ident-name classification, helper delegation) + +## Completion criteria + +#211's, restated for this design: + +- One documented entry point from captured records to elements — `Language::parse`. +- Both `Cbindgen` and `JniGen` take every **source** fact from an element. +- No component re-derives a source fact by matching captured syntax; the ledger + has reached the irreducible set, and every remaining entry is documented as + inspecting adapter-synthesized types. +- The accepted Rust subset is covered by the acceptance matrix with precise + diagnostics naming item and component. +- Spelling generated Rust is done by re-emitting a `syntax` slice, never by + reconstructing one from a classification. + +## Relationship to #215 + +#215 is superseded. Its four merged PRs are not lost: L0 ports the +array-length subgrammar (#212), the type grammar and its acceptance tests, the +enum tag/discriminant numbering (#226) and the boundary ledger (#224). What is +dropped is the syn-free model itself — `SourceType::to_syn`, +`DiscriminantSource`, `VariantShape`, `NamedArg::Lifetime` — because carrying the +source's own slice does that job without a modelling cost. + +The `source-frontend` branch stays in place as the reference. Nothing depends on +it, and it is not a base for anything here: every stage of this program targets +`main`. + +## Review protocol + +Each stage PR states its own exit: + +- **Must not move** — byte-identical artifacts, enforced by + `examples/regen-check.sh`. A diff is a bug. +- **Reviewed diff** — expected to change, cause stated up front. A diff outside + that cause is a bug. +- **Asserted** — the invariant the stage adds, and the ledger delta it claims. + +[`core::language`]: ../prebindgen/src/api/core/language/mod.rs From 09016516b242ee5740ae5a47751bc3d522ff331b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 30 Jul 2026 00:56:40 +0200 Subject: [PATCH 02/52] Parse the record stream into elements that keep their syntax (#211) (#227) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * jnigen: derive a return expansion from a value form (#213) (#221) * jnigen: derive a return expansion from a value form (#213 Gap A) `expand_return!(T).fields(fields!(t_to_struct))` takes T's output fields from its value form — the struct gathering its own accessors — instead of restating them. Two of zenoh-flat-jni's five hand-written lists had already drifted from the struct they mirror; a derived list cannot. `.fields()` is `.field()` applied to each struct field, so it keeps the same rule: a field crosses by ITS OWN type's default output boundary. A field type with an `expand_return!` splices it (a KeyExpr field still crosses as its string, not as a handle), a declared data class inlines, a field behind Option/Vec stays one leaf. Adopting it therefore preserves the boundary shape a hand-written list already had. Per-field adjustments live on the `FieldsDecl`, keyed on the Rust field ident like `FunctionDecl::expand_param`: `.field(name, expand_return!(..))` replaces one field's decomposition, `.name(name, "kt")` renames its leaf. Naming a field the struct lacks is a hard error — that is the drift this declarator exists to catch. Core changes: - `UnfoldLeaf.path` becomes `Vec` (`Call` / `Field`, each carrying its own optionality) so one path can mix accessor calls and field reads. Behaviour-preserving for every existing producer. - `DeconRecord::Fields` + `FieldRecord`; the adapter walks the struct (it knows which are declared classes), core decides per field whether to splice, and rides the existing visited/Cycle guard. - `UnfoldPlan.root_call` hoists the value-form call to one local, so the struct is built once per delivery rather than once per field. - `Prebindgen::deconstructors` now takes `&Registry`, matching `value_struct_decons` — a value form's fields come off the indexed struct. Sum-typed fields (ReplyStruct.result) are not covered yet. Co-Authored-By: Claude Opus 5 * jnigen: a sum-typed field of a value form (#213 Gap A, sums) `ReplyStruct { result: ReplyResult, .. }` — a `sealed_class!` field of a value form now decomposes in place into its selector and one leaf group per alternative. A sum has no whole-value converter by construction, so this is the only shape in which it can cross at all. The user-facing callback still receives ONE typed `ZOutcome`: the tag and group slots collapse into a single parameter rebuilt by an inlined `when`, reusing the `GroupDesc` collapsing that a fixed-builder arg already uses. Handing the raw slots over would have defeated the `sealed_class!`. Generalizations, both behaviour-preserving for a sum in the whole-return position (its 20 existing tests are unchanged): - the selector leaf carries the sum's own type as its `out_ty`, so the emitter finds the enum to match on from the leaf rather than from `plan.source` — which names the CONTAINING value once a sum is a field; - `encode_sum_leaves` becomes `encode_sum_group`, taking one sum's leaf segment plus the expression to match on. `encode_plan_leaves` segments the leaf list and emits one match per sum instead of the whole plan being handed to the sum emitter; a whole-return sum is the degenerate case of one segment covering everything. `Vec` and `Option` fields are refused by name: the first has variable arity, the second would need a present flag beside its tag that an output leaf list cannot carry (the `fromParts` bridge's `PlanFieldKind::Sum` can, which is why a data-class field may be `Option`). Also restores examples/example-cbindgen goldens, which the previous commit picked up from an --all-features regeneration. The generator output is unchanged; only the committed artifact was wrong. Co-Authored-By: Claude Opus 5 * examples: restore example-cbindgen goldens to the plain-build variant An earlier `git add -A` in this branch swept in an --all-features regeneration, whose FEATURES guard reads "example-flat/internal example-flat/unstable" instead of "". `examples/regen-check.sh` builds with default features, so the committed artifact has to be the default-feature one — this is what CI checks. The generator output is unchanged either way; only the committed file was wrong. Co-Authored-By: Claude Opus 5 * covertest: exercise the derived value-form boundary on the JVM (#213) Library tests alone do not count as coverage in this repo, so `.fields()` gets a real round trip: `perftest_flat::ext::Report` is a handle whose output boundary is declared from its value form, with each field landing on a different rule of the expansion — summary a type with its own expand_return! ⇒ spliced into (count, total), NOT handed over as a handle taken Option ⇒ one leaf origin a non-optional data class ⇒ inlined into its fields outcome a sealed_class! ⇒ selector + one group per alternative, carrying a handle label a plain leaf `Test.kt`'s new section is itself the assertion: the callback signature would not compile if any field had been derived wrongly. It also pins the ownership contract for a handle reached through a value form and a sum group — live inside the callback, still live after, the receiver's to close. 47 sections pass on a real JVM. Adds the Gap B unit test the issue asked for: a handle-payload sum in DATA-CLASS FIELD position, the one position return/callback coverage did not reach. It works — and the test pins two consequences that were previously unstated: the container is NOT AutoCloseable (a sum payload is the receiver's to close, unlike a plain handle field, which cascades), and a sum field pushes its parent onto the whole-value fromParts bridge. Co-Authored-By: Claude Opus 5 * jnigen: address review on #221 — three value-form defects P1 — a single-leaf value form passed a borrow to an owned converter. One leaf makes core pick `Delivery::Return`, whose reach is composed separately in `emit/wrapper.rs`'s `is_convert` path. That path rendered a `Field` step as `&(expr).field` and returned it, so a plain field leaf — whose `out_ty` is the field type as written — got `&F` where its converter takes `F`, and a non-`Copy` field additionally borrowed out of the temporary the value-form call returned. It now clones the reached place, the same treatment `encode_plan_leaves` gives a `LeafSource::Field` leaf; an identity leaf stays borrowed, since its converter IS the borrowed-opaque clone. P2 — a per-field override did not validate its declared type. `.field("key_expr", expand_return!(ZBytes)...)` was accepted for a `ZKeyExpr` field whenever both were declared handles, and an override silently outlived an upstream field-type change — the exact drift `.fields()` exists to catch. The declared key is now compared against the peeled field type and names both, matching the target checks on the per-function expansion APIs. P2 — nested value forms were not hoisted. `root_call` only searched the declaration's top-level records, so a field splicing a child whose own boundary is also derived rebuilt that child once per child leaf, breaking the stated "called once per delivery" contract. Replaced by `UnfoldPlan.hoists: Vec>` — the path prefixes to bind once, recorded where `flatten` descends and therefore outermost-first. Each is composed from the longest already-bound prefix of itself, and each leaf reaches off the innermost hoist it sits under: let __vf0 = z_outer_to_struct(&arg); let __vf1 = z_inner_to_struct(&(&__vf0).inner); This also removes the single-value-form special case rather than adding a second one beside it. Three regression tests, one per finding. The only generated-output change is the `__vf` -> `__vf0` rename. Co-Authored-By: Claude Opus 5 * jnigen: validate nested value-form field shapes * jnigen: consuming value forms — move the fields instead of cloning them `.fields(fields!(f))` now accepts a value form that takes its receiver BY VALUE. Such a form destroys the object into its parts, so the generated code moves the value in and moves each field OUT into its leaf — the clones the borrowing form pays disappear entirely. This is what the hot receive path wants and what zenoh itself recommends: `From for SampleFields` exists, in zenoh's words, because it "allows deconstructing a sample to fields without cloning, which is more efficient than using getter methods". Every callback hands its value over owned (`impl Fn(Sample)`), so there is nothing to preserve — the borrowing form clones fields out of a value it is about to drop. Measured on covertest's `Report`: six clones removed from the callback body, `report_into_struct(__cb_arg0)` moved in, every field moved out. Consuming-ness is INFERRED from the accessor's signature, so it cannot drift from it, and both forms stay usable side by side. Because a consuming form moves the value, two shapes are refused at declaration time rather than emitted as Rust that cannot compile downstream: a sibling record (`.field_self()` or another `.field()` would read a moved value), and a form reached through another value form (it would move a field out from under the parent's other leaves). A `&T`-returning function clones once up front and consumes the clone, so one declaration still serves owned and borrowed returns alike. Two supporting changes: - The reach derivation is now SHARED (`reach_leaf_flat`) between the multi-leaf encoder and the single-leaf `Delivery::Return` shortcut in emit/wrapper.rs. Deriving it twice is what let them drift into the P1 defect; the shortcut also now refuses an optional intermediate step explicitly instead of composing code that cannot type-check. - Reaches project the leading run of plain field steps DIRECTLY (`&v.a.b`) instead of through a borrow of the base (`&(&v).a.b`). The two name the same value, but the second borrows the base as a whole, which the borrow checker rejects once a sibling leaf has moved another field out — so without this, field moves compiled only while the borrowing leaves happened to be declared first. Co-Authored-By: Claude Opus 5 * jnigen: `.fields_into()` — declare the consuming value form, and let it nest `6133f91` taught `.fields(fields!(f))` to accept a by-value accessor and INFERRED consuming-ness from its signature. That reads the decision off the wrong thing. Giving the value away is a boundary decision — the same one `.field_self()` makes, which is exactly why the two cannot coexist — not a property of which function happened to be named. So the collision surfaced as a resolve-time error phrased as a restriction on `.fields()`, when it is really two declarators to pick between. Now the decl says which it wants: .field_self() the value itself, whole .fields(fields!(to_struct)) a copy of its parts .fields_into(fields!(into_)) the value itself, as its parts `.fields_into(..)` must be the decl's only record — a `.field_self()` or a sibling `.field(..)` would read a value that is gone — and that is now a panic in the declarator, in BOTH orders, rather than an `UnfoldError` found a resolve later. The declared flag and the accessor's receiver are cross-checked when the records are flattened, so intent still cannot drift from the signature; naming the wrong one of a `to_struct`/`into_struct` pair is an error that says which declarator the accessor belongs to. The nesting refusal is GONE. Its stated reason — "it would move a field out from under the parent's other leaves" — does not hold: a hoisted value form is an owned struct, its fields are disjoint, and `project_leading_fields` (same commit) already stopped leaves from borrowing the base as a whole. So a nested consuming form is handed the parent's field BY MOVE: let __vf0 = z_outer_to_struct(&__cb_arg0); let __vf1 = z_inner_into_struct(__vf0.inner); // moved, not cloned … __vf0.tag … // sibling leaf, still fine `compose_step` borrows (`&(e).f`), so the field run to that field is projected in the hoist loop instead of going through it. A nested form reached through an accessor CALL holds a borrow with nothing to give up, so it clones once and consumes the clone — the same fallback a borrowed root already takes. That was the one place an available `_into_struct` went unused for no reason. Verified: 438 lib tests (three retargeted, five new — both collision orders, both signature-mismatch directions, and the nested move under a borrowing AND a consuming parent), covertest-kotlin's 47 JVM sections, regen-check byte-clean. The generated output for covertest is unchanged — same accessor, same moves; only the declaration that names it moved. Co-Authored-By: Claude Opus 5 * jnigen: address review on #221 — consuming ownership in two more places Two review findings, both cases where `.fields_into(..)` promised a move and the emitter did not deliver one. [P1] The single-leaf `Delivery::Return` shortcut never consulted the plan's hoists. It composed its reach straight off the raw value, so a one-field value form declared with `.fields_into(..)` emitted (&(myflat::z_one_into_struct(&__cvsrc)).label).clone() — `&ZOne` handed to a by-value receiver, ill-typed in the consumer's crate before you even reach the pointless clone. Every consuming test so far produced a MULTI-leaf callback plan and went through `encode_plan_leaves`, so nothing covered it. The hoist loop is now `bind_hoists`, shared by both paths, and `reach_leaf_flat` takes the rebased path plus its hoist's `consuming` flag. The shortcut binds the same `__vfN` locals as the multi-leaf encoder and reaches the leaf off the innermost one. That is the same fix that was applied to the reach itself in `6133f91` and for the same reason: two derivations of one question drift. [P2] The identity branch computed `consuming` and then returned before using it. Only a handle at the owned ROOT (empty path) moved; a handle FIELD always took the clone-via-converter arm: ZChild_to_jlong_...(&mut env, &__vf0.child) despite the parent form having given its value away — a preserved clone, and a `Clone` bound the handle type need not have. The branch now computes the owned PLACE (the root, or a plain-field run under a consuming hoist) and boxes it, `Box::into_raw(Box::new(__vf0.child))`. Both regressions reproduce the reviewer's exact shapes and both fail without the corresponding fix (verified by stashing each). Sum payloads, which the P2 comment also flagged, are NOT fixed here: filed as #228. `encode_sum_group` matches by reference and clones every payload kind through one chain, so moving means reworking that emitter's ownership model — the selector reads the same matched value, and an owned handle payload wants the identity branch's box rather than the borrowed-opaque converter. Not an addendum to this PR. Verified: 440 lib tests, covertest-kotlin's 47 JVM sections, regen-check byte-clean (neither shape occurs in covertest, which is why its goldens do not move — the unit regressions are what pin them). Co-Authored-By: Claude Opus 5 * jnigen: decide leaf ownership in the plan, not in each emitter Two more review findings on #221, both the same defect wearing a different hat: an identity (handle) leaf under a consuming value form was still reached as a borrow, so the borrowed-opaque converter cloned it — and demanded a `Clone` the handle type need not have. * A value form whose SOLE field is a handle takes the single-leaf `Delivery::Return` shortcut. `bind_hoists` called the by-value accessor correctly, then the shortcut returned `&__vf0.child`, because its consuming case only covered `LeafSource::Field`. * An `Option` field was excluded by the previous fix's plain-field test, leaving `match &(&__vf0).child { Some(__n0) => …clone… }` — an ordinary optional handle field, not the sum limitation of #228, and the commonest shape there is (`SampleStruct.attachment`). Patching each emitter would have been a third special case for one question. The question belongs to the PLAN: `place_is_owned` now decides, where an identity leaf's `out_ty` is chosen, whether the value at that path is the plan's to give away — the root of an owned plan, or a field of a form that CONSUMED its value, reached by a movable run of steps. An owned `out_ty` IS that statement, and it already selects the owning converter, so every emitter follows one decision instead of re-deriving it. `steps_are_movable` (plan.rs) is that run: field reads only, with an `Option` allowed on the LAST one — a `None` arm still hands the whole `Option` over by value, while an `Option` in the middle must be unwrapped and so can only be borrowed through. The resolver and both emitters read the same predicate; two readings would drift, and the disagreement is a borrow handed to an owning converter. Emitters then just project the place: * `reach_leaf_flat` moves whenever the leaf owns its `out_ty` — field and identity leaves alike. It keeps requiring a plain-field run, since return delivery has no `None` arm for a trailing `Option`. * The nullable identity branch matches the `Option` BY VALUE and boxes the `Some` payload, instead of matching a borrow of it. Both regressions reproduce the reviewer's shapes and fail without the fix (verified by stashing it). 442 lib tests, covertest's 47 JVM sections, regen-check byte-clean. Co-Authored-By: Claude Opus 5 * jnigen: a nullable sole leaf is a callback delivery, not a return `single_return` chose `Delivery::Return` on leaf COUNT alone. A value form whose only field is an `Option` therefore landed on the flat return path, which has no `None` arm and whose `convert_out_ty` names the leaf's own type rather than an optional of it — so it composed &(&__vf0).child into `ZChild_to_jlong(.., __out)`, typed for `ZChild`. The downstream crate does not compile. Making `out_ty` owned in 421531e addressed move-vs-clone; it says who frees the handle, not whether there is one. Absence is a DELIVERY question. Callback delivery already has the arm — the leaf crosses as a boxed `Long` or JVM null — so a nullable leaf goes there, which is one condition on `single_return` rather than teaching the shortcut to match and map a trailing option it has no way to represent in its return type. Nullability here only ever comes from an `Option` with something DECOMPOSED below it (a `.field_self()` handle, a nested value form); a plain leaf's own `Option` rides its converter and leaves the leaf non-nullable. So no shape that returns today stops returning — regen-check is byte-identical and covertest's 47 sections are unchanged. Regression reproduces the reviewer's shape and fails without the fix. Co-Authored-By: Claude Opus 5 * jnigen: an owned root identity moves on the flat return path too The flat return path asked the wrong question. It tied the move to the rebased hoist's `consuming` flag, but "a consuming form gave it to me" is only ONE of the two ways a leaf owns what it reaches. A plain `-> ZChild` return under the type-level `expand_return!(ZChild).field_self()` — the declaration that exists so the same boundary can be spliced as a value-form field — has no hoist at all, so `consuming` was false and the path emitted let __cvsrc = myflat::z_root_child_make(); { &__cvsrc } into the OWNING `ZChild_to_jlong`, whose argument is `ZChild`. Same mismatch inside the `map` closure of an `Option` return. For an identity leaf the plan already states ownership — that is what `place_is_owned` decides and what selected the owning converter — so the emitter reads it off `out_ty` instead of re-deriving it. A field leaf keeps asking the enclosing form, since its `out_ty` is the field type as written and owned either way. That predates this PR: the previous shape of this path composed `&base` for an empty path regardless. The callback emitter has always treated the owned root as an owned place; now both do. Regression covers the plain and the `Option` return and fails without the fix. 444 lib tests, covertest's 47 JVM sections, regen-check byte-clean. Co-Authored-By: Claude Opus 5 * jnigen: rename `.fields_into()` to `.fields_self_into()` Puts the declarator squarely in the `field_self` family it belongs to, which is the whole point of it being its own declarator: `.field_self()` hands the value over whole, `.fields_self_into(..)` hands *the value itself* over as its parts, and `.fields(..)` hands over a copy of its parts. `self` is what the first two share and what makes them mutually exclusive. Mechanical: the method, the two panic messages, the doc links, the covertest declaration and its coverage-table row. Generated output is unchanged — regen-check byte-clean. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 * Parse the record stream into elements that keep their syntax `Language` turns a captured `(syn::Item, SourceLocation)` stream into `Element`s: a closed, destination-neutral classification paired, at every level, with the exact syntax it was built from — the item, each parameter, field, variant and type. The pairing is the point. Issue #211 asks that adapters stop re-reading captured Rust, and the natural reading of that — a syn-free semantic model — makes the model responsible for reconstructing Rust too, because the generated glue is itself a destination artifact. That pressure is what turns a language-neutral IR back into a second `syn`: a delimiter, a lifetime and a literal's base all have to be modelled so they can be re-emitted. Keeping the original slice costs nothing and removes the pressure, so the classification stays small: Element::Enum → Variant { tag, discriminant: Option, fields, syntax } `B()` is a unit *group* and still spells `E::B()`, because `Variant::spell` reads the delimiters off `syntax`. `= 0x07` reaches a C header as `0x07` while Kotlin gets the number 7. Neither is a modelled fact. The rule for consumers is therefore: **classify off `kind`, spell off `syntax`.** #224's boundary ledger measures exactly that without adaptation — it counts variant mentions of `syn::Type` / `syn::Expr`, so `quote!(#slice)` is invisible to it and `matches!(ty, syn::Type::Reference(_))` is not. It is ported here and seeded at 202 sites, the population the adapter migrations pay down. Acceptance is preserved, not expanded. An item the language cannot express becomes `Element::Unsupported`, carrying its diagnosis: the pipeline has always scanned a signature only once an adapter declares it, and a source crate may mark items no binding uses. Only a duplicate name — which no declaration can disambiguate — fails the parse. Nothing consumes elements yet; `Registry::from_elements` is the next step. Ported from the #215 branch: the array-length subgrammar (#212), the type grammar and its acceptance tests, enum tag/discriminant numbering (#226), the ledger (#224). Refs #211. Co-Authored-By: Claude Opus 5 * Make the element model logical, not Rust-shaped `TypeKind` still named Rust type constructors where it should have named concepts, and the identity of a nominal type was a `syn::Path` sitting inside the classification — a position the boundary ledger cannot see. The test a variant has to pass is whether a *destination* language would act on the distinction; if only Rust can tell, it is spelling, and the syntax slice already carries it. Twelve variants become ten: * `Slice` folds into `Sequence`. `Vec` and `[T]` are one concept — a run of `T` — and ownership is already the `Ref` layer's fact, so a second variant encoded it twice. This is what the pipeline does anyway: one `Shape::Iterable` covers both, and jnigen rewrites a `&[T]` input into the `Vec<_>` pattern. * `Boxed` goes. `Box` **is** `T`: owned either way, and nothing outside Rust can tell. It classifies as what it wraps, and the `Box` survives where it matters — in the syntax generated Rust spells. * `Ptr` goes. No source crate writes a raw pointer, neither adapter has a selection arm for one, and accepting it *widened* acceptance, which this stage was not supposed to do. * `Str` covers `str`, so `&str` is a borrowed string rather than a reference to a nominal type nothing can resolve. It is the most common non-scalar parameter in the whole ecosystem, and both adapters already special-case it by name. * `Named` carries a `TypeId` — a name — instead of a `syn::Path`. The same test applied to the elements: a function's return is a `Type`, unit when elided, because no consumer distinguishes that from `-> ()` (eight of them normalize one to the other on the spot). A struct's fields are `Option>` — a product, or opaque — because named/unnamed/unit were three Rust shapes where `Variant` already modelled the same idea as a field list plus delimiters read off the syntax. `spell.rs` now holds everything that turns an element back into Rust tokens, so `element.rs` describes structure alone, and `Struct::spell` joins `Variant::spell` as the dual that makes the shapes unnecessary. Two things move to where they belong: `Language::parse` normalizes before lowering (`ty.rs` already assumed it had), and the callback grammar `extract_fn_trait_args` lives in the language rather than the registry — one ledger site paid down, 202 to 201. Co-Authored-By: Claude Opus 5 * Delete the passthrough element A `#[prebindgen]` crate marks the items that cross the boundary; the supporting code around them belongs to the consumer. The proc-macro already enforces that — marking a `use`, `mod`, `impl` or `macro_rules!` is a compile error at the mark site — so the variant's own doc listed items that could never reach it. What actually reached it was one thing: the `const _` feature guard, which is not a source item at all. `CfgFilter` synthesizes it and prepends it to the stream, so `Passthrough` existed to carry an item prebindgen itself wrote. It is a const, so it is modelled as one, and `Element::name` returns `None` for `_` — which is the real fact, and the one that lets several sources' guards coexist in the flat namespace. `write.rs` already had that rule for consts (`*ident == "_"` bypasses the declaration gate), dead until now because `const _` never reached the consts map. That leaves `union` and a type alias, the two kinds the macro accepts and the frontend does not model. Neither is written by any source crate in the ecosystem. They become `Unsupported` with a diagnosis naming the kind, rather than being copied verbatim into generated code that would reference source types by bare name — so the mark site and the frontend now disagree about exactly two kinds, and disagree loudly instead of silently. `Unsupported::name` becomes optional, since an item kind may have no identifier. Co-Authored-By: Claude Opus 5 * Give every node one Origin: its syntax, and where that syntax came from The classification is now logical, but its other half was still ad-hoc. `syntax` sat on nine node types as nine separate fields; `location` sat on the five item types only, because a captured record is per-item and a component has none of its own. That asymmetry had a cost. The one semantically load-bearing part of a location — the crate name — was reachable at item level only, so it got copied downward by hand, under a third field name, with drifting meaning: `ConstId.origin` is the crate a const was *declared* in, while `TypeId.origin` was the crate of the item *using* the type. The latter was also part of `TypeId`'s derived `Eq`, so `Sample` referenced from two source crates compared unequal — one type with two identities, three lines under a doc calling the name "the whole address". The two facts are orthogonal and neither derives from the other. `syn` tokens normally carry spans, but the proc-macro serializes each item as a string into JSONL and `build.rs` re-parses it, so every span in a slice points into an anonymous buffer; `SourceLocation::from_span` captures file/line/column while real rustc spans still exist, precisely because they cannot survive the trip. So every node now carries `Origin { syntax: S, location: Rc }` — item, parameter, field, variant, type, and the array extent, which had no syntax at all and now spells its own length. Generic, so the typed slices survive; `Rc` because the model holds `syn` and is `!Send` regardless, the call `TypeKey` already made. One captured record is one item, so an item and every node lowered out of it share one allocation, which is both the honest answer to "where is this field" and the cheap one. With provenance arriving on its own, `item_crate: Option<&str>` stops being threaded through six lowering functions, `TypeId` is a name alone, and `ConstId.origin` becomes `ConstId.crate_name` — a crate that belongs to a *different* item, not this node's provenance. The rule, now stated where it can be read: a reference carries a name, the declaration carries the origin. Co-Authored-By: Claude Opus 5 * A variant's position is an index, not a tag `Variant.tag: i32` and `Field.index: usize` were one fact under two names: the ordinal of a child within its parent's ordered list. Sum versus product is already carried by *which* list it is — `Enum::variants` or `Struct::fields` — not by the number. The defence for keeping them apart was that a tag is transmitted while an index is only used to address a field. That defence was made of adapter behaviour: `i32` because cbindgen writes `c_int` and jnigen writes `jint`. Deciding a frontend field's shape from two generators' wire types is exactly the coupling this module exists to prevent, and it is the same test that stripped `Boxed` and `Slice` — a fact earns its shape from what the source means, not from what one adapter does with it. Transmitting the position to say which alternative is live is one destination's choice; another may send a name. The signedness had no defence at all: a declaration-order position is `0..N-1`. So `Variant.index: usize`, matching `Field.index`, and both documented as the same fact for the same reason — a node handed out on its own still knows where it sits. What remains genuinely distinct is `Variant::discriminant`: a position is where the source *put* a variant, a discriminant is the value Rust *assigns* it, and the two are independent. Co-Authored-By: Claude Opus 5 * Address review: extent identity, callback returns, i64::MIN Three correctness fixes before this becomes the model later stages consume. **`ArrayExtent` had an equality that was neither identity it could have been.** It compared `value` and `source`, so `[u8; A]` differed from `[u8; 4]` when `A == 4` — one Rust type reported as two — while `[u8; 4]` equalled `[u8; 0x04]`, whose retained syntax differs. So it was not type identity and not spelling identity, and its own doc claimed the first while the code did neither. There is no single equality that could be right, because the extent answers three different questions, so it now provides none and each consumer projects what it needs: `value` for type and converter identity, `origin.syntax` for a C declaration's spelling at that occurrence, `const_id()` for which consts must reach the header. A regression pins all three apart — same value with different const dependency, same value with different spelling, same value with different const. The doc also records what a converter table will need: `value` being the identity means occurrences share one converter with differing spellings, so a canonical spelling must be chosen deliberately rather than inherited from whichever occurrence populated the entry. **The callback grammar silently dropped a return type.** `extract_fn_trait_args` read `ParenthesizedGenericArguments::inputs` and never `output`, so `impl Fn() -> u8 + Send + Sync + 'static` was accepted as `Callback { args: [] }`. `TypeKind::Callback` has no slot for a return and the grammar's own error text says a callback returns `()`, so the fact was lost — silently, which is worse than refusing. A non-unit return is now refused, a written `-> ()` still accepted, both with tests. No source crate in the ecosystem writes a returning callback, so nothing real narrows. The helper predates this PR, but making it the authoritative frontend classifier is what would have made the loss irreversible for every later consumer. **`i64::MIN` was not a discriminant.** `int_literal` parsed the magnitude as `i64` before applying the sign, so `-9223372036854775808` — valid Rust — failed at the digits. The magnitude is now parsed as `i128` and range-checked after negation, with a regression at the bottom of the range and one step past it. Along the way, `is_unit_type` becomes the language's one answer to "is this `()`", used by both the type lowering and the callback check. `types_util::is_unit` could not serve: it is gated behind `unstable-cbindgen`. Co-Authored-By: Claude Opus 5 * Address review: async, variadic, generic binders, and a ledger hole Three more shapes the frontend accepted but could not represent, and one hole in the check that is supposed to catch exactly this class of thing. **`async fn` was the dangerous one.** `Function` has a direct return, so `pub async fn ping() {}` lowered as a function returning `()` — a generated wrapper would call it, drop the future, and export a function whose body never runs. A **C-variadic** tail was dropped from the signature just as quietly. Both are now `ItemError`s. **A type or const generic parameter is refused.** The elements have no generic binder, so a `T` in a field or parameter lowered as `TypeKind::Named` — an ordinary reference into the flat namespace, indistinguishable from a real item called `T`, which loses the scoping every downstream resolver needs. Modelling binders and substitution is the other option; refusing is the right one, because no destination language can express an uninstantiated parameter, and the source crates already write concrete types per instantiation. The diagnosis says so. Two things are deliberately *not* generic binders, both tested. A lifetime parameter: lifetimes are spelling and the spelling already travels, the same call `lower_type` makes for a lifetime argument. And `impl Trait` in argument position — Rust calls it an anonymous type parameter, but `syn` does not desugar it into the binder list, so the callback form every callback-taking source function uses is untouched. **The boundary ledger could be evaded.** `is_cfg_test` treated any predicate containing the ident `test` as test-only, so a classifier under `#[cfg(not(test))]` or `#[cfg(any(test, feature = "x"))]` was skipped — in a production build. It now matches the exact predicate `cfg(test)` and counts everything it cannot prove test-only, which is the safe direction for a check whose job is to stop a classifier hiding. `cfg(all(test, ..))` is genuinely test-only and is counted anyway; nothing in the tree writes one, and widening it later should be a deliberate edit with a ledger diff attached. The count does not move: every `cfg` on an item in the tree is either exactly `cfg(test)` or mentions no `test` at all. Co-Authored-By: Claude Opus 5 * Let Language read a source directory, not just a stream A build script's whole prebindgen preamble was two steps and a binding it did not otherwise want: let source = prebindgen::Source::new(zenoh_flat::PREBINDGEN_OUT_DIR); let registry = Registry::from_items(source.items_all())?; `Language` now folds the first step in, so naming the directory is enough: let elements = Language::new() .source(zenoh_flat::PREBINDGEN_OUT_DIR) .parse()?; That is five of the six consumer build scripts — zenoh-flat-jni, zenoh-flat-c, perftest-c, perftest-kotlin, example-cbindgen — which use nothing of `Source` but `new` and `items_all`. Reading a stream is kept, as the general case rather than the only one: `items()` takes any `(syn::Item, SourceLocation)` iterator, so everything a `Source` can express still composes — a group selection, a renamed dependency (covertest-kotlin's `crate_name` override, the sixth build script), several sources at once. `source()` is sugar over it. The other four knobs on `Source`'s builder — group selection and feature/target filtering — are reachable this way and were not mirrored, because no build script in the workspace calls them. The feeders accumulate and `parse` consumes, rather than each input being parsed as it arrives. That is forced, not stylistic: the rules that make a parse fail are whole-stream — one flat namespace, one const index an array length may reach into, one set of source modules to normalize against — so every input must be in hand before any of it is classified. A test now pins both directions of that: a length in one feeder resolving a const from another, and a duplicate name across feeders still failing. `Language` and `Element` join `Registry` in the `core` facade, since they are what a build script names; the rest of the element model stays in `core::language`, where an adapter reaches for it. The four doc examples on `Language` are now real doctests rather than `ignore` blocks — `Source::init_doctest_simulate` was already there to make that possible. Co-Authored-By: Claude Opus 5 * Split the two enum shapes: a Variant is not an Enum `Element::Enum` covered both a payload-carrying enum and a fieldless one, on the theory that the second is the degenerate first. They are two entities, and the evidence is in how they are numbered. A sum's alternatives are identified by **position**: cbindgen states it outright — "the mirror carries no explicit discriminants, so its tags are declaration order `0..N`" — and jnigen's sum emission mentions `discriminant` exactly zero times against eleven uses of the position. A fieldless enum's members are identified by the **value Rust assigns**: a C header re-states each `= expr`, and a Kotlin `enum class` entry is `NAME(7)`, with position only a fallback when the discriminant is not a literal. So one model covering both carried a field dead in each direction — and worse than dead on the sum side, because Rust *does* assign a discriminant to a payload alternative and using it would be wrong. The unified model invited exactly that mistake. Element::Variant(Variant { alternatives: Vec }) // a sum Element::Enum(Enum { values: Vec }) // C-style `Alternative` carries `index` and `fields` and no discriminant; `EnumValue` carries `index` and `discriminant` and no fields. `discriminant_values` belongs to `Enum` alone now. `is_unit` and `first_payload_variant` are gone: the first was the classification, which `lower_enum` now makes once, and the second existed to name an offender to an adapter that only accepts fieldless enums — such an adapter matches `Element::Enum` and never sees the other shape. Both shapes still spell delimiters off their own syntax, because `A`, `B()` and `C {}` are fieldless alike and Rust demands the delimiters wherever the last two are named — so `spell` is on `Alternative` and `EnumValue`, over the one `spell::fields`. `enum E {}` and an all-empty-group enum are `Enum`; one field anywhere makes the item a `Variant`, and a sum may still mix empty and payload-carrying alternatives. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- docs/sum-types.md | 4 +- examples/covertest-kotlin/build.rs | 24 +- examples/covertest-kotlin/kotlin/REPORT.md | 2 + .../generated/io/prebindgen/covertest.kt | 2 + .../io/prebindgen/covertest/model.kt | 84 + .../kotlin/io/prebindgen/covertest/Test.kt | 53 + .../src/generated_bindings.rs | 388 ++++ examples/perftest-flat/src/ext.rs | 103 ++ prebindgen/src/api/core/language/array_len.rs | 348 ++++ .../src/api/core/language/boundary.ledger | 73 + prebindgen/src/api/core/language/boundary.rs | 495 +++++ prebindgen/src/api/core/language/element.rs | 292 +++ prebindgen/src/api/core/language/mod.rs | 836 +++++++++ prebindgen/src/api/core/language/origin.rs | 88 + prebindgen/src/api/core/language/spell.rs | 83 + .../src/api/core/language/tests/acceptance.rs | 891 +++++++++ prebindgen/src/api/core/language/tests/mod.rs | 118 ++ .../src/api/core/language/tests/roundtrip.rs | 466 +++++ prebindgen/src/api/core/language/ty.rs | 521 ++++++ prebindgen/src/api/core/mod.rs | 2 + prebindgen/src/api/core/prebindgen.rs | 11 +- prebindgen/src/api/core/registry.rs | 42 +- prebindgen/src/api/core/unfold.rs | 386 +++- prebindgen/src/api/core/unfold/plan.rs | 122 +- prebindgen/src/api/core/unfold/tests.rs | 36 +- prebindgen/src/api/lang/jnigen/jni/builder.rs | 369 +++- prebindgen/src/api/lang/jnigen/jni/decl.rs | 213 +++ .../src/api/lang/jnigen/jni/emit/delivery.rs | 437 ++++- .../api/lang/jnigen/jni/emit/struct_out.rs | 9 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 48 +- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 22 +- prebindgen/src/api/lang/jnigen/jni/iface.rs | 59 +- .../src/api/lang/jnigen/jni/tests/mod.rs | 1 + .../src/api/lang/jnigen/jni/tests/sealed.rs | 109 ++ .../api/lang/jnigen/jni/tests/value_form.rs | 1626 +++++++++++++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 7 +- prebindgen/src/api/lang/jnigen/mod.rs | 5 +- prebindgen/src/lib.rs | 20 +- 38 files changed, 8153 insertions(+), 242 deletions(-) create mode 100644 prebindgen/src/api/core/language/array_len.rs create mode 100644 prebindgen/src/api/core/language/boundary.ledger create mode 100644 prebindgen/src/api/core/language/boundary.rs create mode 100644 prebindgen/src/api/core/language/element.rs create mode 100644 prebindgen/src/api/core/language/mod.rs create mode 100644 prebindgen/src/api/core/language/origin.rs create mode 100644 prebindgen/src/api/core/language/spell.rs create mode 100644 prebindgen/src/api/core/language/tests/acceptance.rs create mode 100644 prebindgen/src/api/core/language/tests/mod.rs create mode 100644 prebindgen/src/api/core/language/tests/roundtrip.rs create mode 100644 prebindgen/src/api/core/language/ty.rs create mode 100644 prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs diff --git a/docs/sum-types.md b/docs/sum-types.md index eb427589..86a10a08 100644 --- a/docs/sum-types.md +++ b/docs/sum-types.md @@ -40,8 +40,8 @@ pub struct ReplyStruct { A caller who sets both `RecoveryConfig` fields gets `heartbeat` ignored with no diagnostic; a caller who checks only `ReplyStruct::sample` reads an error reply as an empty success. These are -[zenoh-flat #31](https://github.com/ZettaScaleLabs/zenoh-flat/issues/31) and -[#30](https://github.com/ZettaScaleLabs/zenoh-flat/issues/30). flat's README already forbids bending +[zenoh-flat #31](https://github.com/eclipse-zenoh/zenoh-flat/issues/31) and +[#30](https://github.com/eclipse-zenoh/zenoh-flat/issues/30). flat's README already forbids bending its shapes to generator limits (§*Bindings choose; flat stays neutral*), so the fix belongs here. Sum types are also not exotic: Kotlin, Swift, Rust, TypeScript and Python (tagged unions) all express diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index e210f31b..6aa69154 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -34,6 +34,8 @@ //! | split × builder-delivered return (#87) | `summaryMerge` — cartesian split + generic `` wrapper; every overload re-declares `` | //! | JNI native-symbol escaping (#86) | `esc_pkg.Esc_Probe` — underscored subpackage + class (escaped `freePtr` symbol) + hook-mangled `escape_probe_value` harness extern | //! | `expand_return!` `.field()` (+`_self`) | `Summary` fields + `StorageError` `message` + self (error handle → `onError`) | +//! | `expand_return!` `.fields(fields!(…))` (#213) | `Report` — boundary DERIVED from the value form instead of restated; covers every per-field rule (spliced `Summary`, inlined `Stamp`, `Option`, a sum with a handle payload, a plain leaf) | +//! | `expand_return!` `.fields_self_into(fields!(…))` | `report_into_struct(r: Report)` — the CONSUMING value form: the value is given away and its fields MOVED out, so the clones the borrowing `report_to_struct` pays are not emitted at all | //! | `PackageDecl::fun` / `FunctionDecl::name`| every free function; `.name` renames `millis_add` → `addMillis` | //! | `Generation::report()` (C7) | `kotlin/REPORT.md` — the resolved surface, committed next to the regen | //! | contextual method names | method hook strips `storage`/`stamp` class prefixes; `summary_new`→`.name("of")` still overrides | @@ -97,8 +99,8 @@ use prebindgen::{ constant, convert, core::Registry, data_class, enum_class, expand_param, expand_return, expr, - from, fun, into, lang::JniGen, matching, package, path, ptr_class, sealed_class, sig, try_from, - ty, variant, + fields, from, fun, into, lang::JniGen, matching, package, path, ptr_class, sealed_class, sig, + try_from, ty, variant, }; fn strip_flat_class_prefix(class: &str, name: &str) -> String { @@ -239,6 +241,11 @@ fn main() { // resources: one alternative carries an opaque handle, one // carries nothing at all. .class(sealed_class!(Lookup)) + // `Report`'s output boundary is DERIVED from its value form + // (`.fields_self_into(fields!(report_into_struct))` below) instead of + // being restated field by field — #213. The form it names is + // the CONSUMING one, so the fields are moved, not cloned. + .class(ptr_class!(Report)) // `Hold`'s payload is a CONVERTED type, so its leaf crosses // through the `convert!(Duration)` chain; `HoldPolicy` puts // that same payload in the data-class-field position. @@ -378,6 +385,14 @@ fn main() { .field(fun!(summary_count)) .field(fun!(summary_total)), ) + // `Report` default output DERIVED from its value form (#213): the + // leaves come from `ReportStruct`'s fields, so the list cannot drift + // from the struct the way a restated one does. Each field still crosses + // by ITS OWN type's boundary — `summary` splices `Summary`'s decl above + // into `(count, total)` rather than becoming a handle, `origin` inlines + // its `Stamp` fields, `taken` stays one `Stamp?` leaf, and `outcome` + // decomposes into a selector plus one group per alternative. + .expand(expand_return!(Report).fields_self_into(fields!(report_into_struct))) // ── Base-package handle type: `Storage` + scalar members ──────────── // Back in the base package so the typed handle classes live alongside // `Payload`. @@ -490,6 +505,11 @@ fn main() { // handle-carrying sum arriving through a CALLBACK, and a sum // returned BORROWED (`&E` / `Option<&E>`). .fun(fun!(lookup_each)) + // #213: the output boundary DERIVED from the type's value form + // rather than restated. `report_each` delivers the decomposed + // `Report` in one crossing; the leaf list comes from + // `ReportStruct`'s fields, so it cannot drift from it. + .fun(fun!(report_each)) .fun(fun!(archive_set_reading)) .fun(fun!(archive_reading)) .fun(fun!(archive_reading_maybe)) diff --git a/examples/covertest-kotlin/kotlin/REPORT.md b/examples/covertest-kotlin/kotlin/REPORT.md index bd25b651..6a3eeecc 100644 --- a/examples/covertest-kotlin/kotlin/REPORT.md +++ b/examples/covertest-kotlin/kotlin/REPORT.md @@ -96,6 +96,7 @@ Base package: `io.prebindgen.covertest` - shaped by: return `Reading` decomposed → [tag, exact_v0, range_low, range_high, tagged_v0, tagged_v1, companion_v0] (Callback delivery) - `reading_series` — `fun readingSeries(n: Int, onError: JniErrorHandler>): List` - shaped by: return `Reading` decomposed → [tag, exact_v0, range_low, range_high, tagged_v0, tagged_v1, companion_v0] (Callback delivery) +- `report_each` — `fun reportEach(n: Long, sink: ReportCallback, onError: JniErrorHandler)` - `stamp_new` — `fun stampNew(secs: Long, nanos: Long, onError: JniErrorHandler): Stamp` - shaped by: return `Stamp` decomposed → [secs, nanos] (Callback delivery) - `stamp_series` — `fun stampSeries(count: Long, onError: JniErrorHandler>): List` @@ -202,6 +203,7 @@ Base package: `io.prebindgen.covertest` - `Priority`: enum_class → `io.prebindgen.covertest.model.Priority` (wire `jni :: sys :: jint`) - `Reading`: sealed_class → `io.prebindgen.covertest.model.Reading` (wire `?`) - `RepliesConfig`: data_class → `io.prebindgen.covertest.model.RepliesConfig` (wire `jni :: objects :: JObject`) +- `Report`: ptr_class → `io.prebindgen.covertest.model.Report` (wire `jni :: sys :: jlong`) - `Stamp`: data_class → `io.prebindgen.covertest.model.Stamp` (wire `jni :: objects :: JObject`) - `Storage`: ptr_class → `io.prebindgen.covertest.Storage` (wire `jni :: sys :: jlong`) - `StorageError`: ptr_class → `io.prebindgen.covertest.errors.StorageError` (wire `jni :: sys :: jlong`) diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt index 03f4a89c..fa8b1bd8 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt @@ -832,6 +832,8 @@ internal object CovNative { external fun readingSeries(n: Int, acc: Any?, fold: Any, errorSink: Any): Any? + external fun reportEach(n: Long, sink: Any, errorSink: Any) + external fun stampNanos(sSecs: Long, sNanos: Long, errorSink: Any): Long external fun stampNew(secs: Long, nanos: Long, build: Any, errorSink: Any): Any? diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt index fdcb894c..61d80a5f 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt @@ -5,6 +5,7 @@ import io.prebindgen.covertest.CovNative import io.prebindgen.covertest.DurationCallback import io.prebindgen.covertest.JniErrorHandler import io.prebindgen.covertest.JniErrorHandlerCapture +import io.prebindgen.covertest.NativeHandle import io.prebindgen.covertest.Payload import io.prebindgen.covertest.Ranked import io.prebindgen.covertest.__u64FolderRawHolder @@ -858,6 +859,30 @@ public data class Unsigned(val byte: Int, val short: Int, val int: Long, val lon } } +/** Typed handle for a native Zenoh `Report`. */ +public class Report(initialPtr: Long) : NativeHandle(initialPtr) { + @Synchronized + override fun close() { + val p = ptr + if (p != 0L && (p and 1L) == 0L) { + ptr = p or 1L + freePtr(p) + } + } + + @Synchronized + public fun take(): Report { + val p = ptr + ptr = p or 1L + return Report(p) + } + + public companion object { + @JvmStatic + external fun freePtr(ptr: Long) + } +} + public fun interface LookupCallback { public fun run(lookup: Lookup) } @@ -906,6 +931,54 @@ public fun ReadingCallback.asRaw(): ReadingCallbackRaw = ) } +public fun interface ReportCallback { + public fun run( + summary__count: Long, + summary__total: Double, + taken: Stamp?, + origin__secs: Long, + origin__nanos: Long, + outcome: Lookup, + label: String, + ) +} + +public fun interface ReportCallbackRaw { + public fun run( + summary__count: Long, + summary__total: Double, + taken: Stamp?, + origin__secs: Long, + origin__nanos: Long, + outcome__tag: Int, + outcome__found_v0: Long, + outcome__failed_v0: String?, + label: String, + ) +} + +public fun ReportCallback.asRaw(): ReportCallbackRaw = + ReportCallbackRaw { + summary__count, + summary__total, + taken, + origin__secs, + origin__nanos, + outcome__tag, + outcome__found_v0, + outcome__failed_v0, + label -> + run( + summary__count, + summary__total, + taken, + origin__secs, + origin__nanos, + when (outcome__tag) { 0 -> Lookup.Absent; 1 -> Lookup.Found(Summary(outcome__found_v0)); 2 -> Lookup.Failed(outcome__failed_v0!!); else -> throw IllegalArgumentException("Lookup: invalid tag $outcome__tag") }, + label + ) + } + public fun interface ArraysBuilder { public fun run( bytes: ByteArray, @@ -1479,6 +1552,17 @@ public fun lookupEach(n: Long, total: Double, sink: LookupCallback, onError: Jni if (__bcap.failed) return onError.run(__bcap.ze0) } +/** + * Deliver a [`Report`] to a callback — the decomposed value form arriving in + * ONE crossing, which is the whole point of deriving the boundary rather than + * handing over a handle the receiver must then query field by field. + */ +public fun reportEach(n: Long, sink: ReportCallback, onError: JniErrorHandler) { + val __bcap = JniErrorHandlerCapture.acquire() + CovNative.reportEach(n, sink.asRaw(), __bcap) + if (__bcap.failed) return onError.run(__bcap.ze0) +} + /** * Store the `which` alternative as the archive's own reading, and the same one * as its optional fallback. A **negative** `which` clears the fallback and diff --git a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt index f6bd381f..464ae0a7 100644 --- a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt +++ b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt @@ -67,6 +67,7 @@ import io.prebindgen.covertest.model.Observation import io.prebindgen.covertest.model.observationNew import io.prebindgen.covertest.model.observationWhich import io.prebindgen.covertest.model.lookupEach +import io.prebindgen.covertest.model.reportEach import io.prebindgen.covertest.model.lookupOf import io.prebindgen.covertest.model.archiveReading import io.prebindgen.covertest.model.archiveReadingMaybe @@ -571,6 +572,58 @@ fun main() { check(s.isClosed()) } + // An output boundary DERIVED from the type's value form + // (`expand_return!(Report).fields(fields!(report_to_struct))`) instead of a + // restated field list — #213. The point is that deriving changes NOTHING + // about the wire: each field still crosses by its own type's boundary, all + // in ONE crossing, so a binding can swap a hand-written list (which drifts + // when the struct gains a field) for the derived one and keep its shape. + // + // Each parameter below lands on a different rule, and the signature itself + // is the assertion — it would not compile if a field had been derived + // wrongly: + // summary__count/total the field's type has its own expand_return! and + // is spliced by it — NOT handed over as a handle + // taken Option stays ONE leaf + // origin__secs/nanos a non-optional data class INLINES + // outcome a sum, typed here, tag + groups on the wire + // label a plain leaf + section("output boundary derived from a value form") { + val rows = mutableListOf() + val kept = mutableListOf() + reportEach(3L, { sCount, sTotal, taken, oSecs, oNanos, outcome, label -> + // The value form is called ONCE per delivery, so every leaf below + // comes from the same snapshot. + check(oSecs == 1L && oNanos == 2L) + val stamped = if (taken != null) "@${taken.secs}" else "-" + val which = when (outcome) { + is Lookup.Failed -> "failed" + Lookup.Absent -> "absent" + is Lookup.Found -> { + val s = outcome.v0 + // A handle carried by a sum group, reached through a value + // form: live, and the receiver's to close. + check(!s.isClosed()) + kept.add(s) + "found:${s.count(boom)}" + } + } + rows.add("$label|$sCount|$sTotal|$stamped|$which") + }, boom) + + check( + rows == listOf( + "r0|0|0.0|@7|failed", + "r1|0|10.0|-|absent", + "r2|1|20.0|@7|found:1", + ) + ) { "derived value-form leaves: $rows" } + + check(kept.size == 1) + kept[0].close() + check(kept[0].isClosed()) + } + // A sum returned BORROWED (`&Reading` / `Option<&Reading>`). The value stays // owned by the archive; the encoder matches THROUGH the reference and clones // what each live group needs, so Kotlin gets an ordinary value with no diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index a6edac35..17767e52 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -211,6 +211,22 @@ const _: () = { }; #[no_mangle] #[allow(non_snake_case, unused_variables)] +pub(crate) unsafe extern "C" fn Java_io_prebindgen_covertest_model_Report_freePtr( + _env: jni::JNIEnv, + _class: jni::objects::JClass, + ptr: jni::sys::jlong, +) { + if ptr != 0 && (ptr & 1) == 0 { + drop(Box::from_raw(ptr as *mut perftest_flat::Report)); + } +} +const _: () = { + if ::core::mem::align_of::() < 2 { + panic!("opaque handle types must have alignment >= 2 (bit 0 is the closed tag)"); + } +}; +#[no_mangle] +#[allow(non_snake_case, unused_variables)] pub(crate) unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_payloadVecFree( _env: jni::JNIEnv, _class: jni::objects::JClass, @@ -3869,6 +3885,255 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Reading_Send_Sync_static_5964f1fc<'env, clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn JObject_to_impl_Fn_Report_Send_Sync_static_eb5ca515<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result< + impl Fn(perftest_flat::Report) + Send + Sync + 'static, + __JniErr, +> { + Ok({ + use std::sync::Arc; + let java_vm = Arc::new( + env + .get_java_vm() + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Unable to retrieve JVM: {}", e)))?, + ); + let callback_global_ref = env + .new_global_ref(&v) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Unable to global-ref callback: {}", e)))?; + let __invoke_class = env + .get_object_class(&v) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from( + format!("Unable to get callback class for {}: {}", "Fn(Report)", e), + ))?; + let __invoke_id = env + .get_method_id( + &__invoke_class, + "run", + "(JDLio/prebindgen/covertest/model/Stamp;JJIJLjava/lang/String;Ljava/lang/String;)V", + ) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Unable to resolve run for {}: {}", "Fn(Report)", e)))?; + Box::new(move |__cb_arg0: perftest_flat::Report| { + let _ = (|| -> ::core::result::Result<(), __JniErr> { + let mut env = java_vm + .attach_current_thread_as_daemon() + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Attach thread for {}: {}", "Fn(Report)", e)))?; + env.push_local_frame(24) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("push local frame for {}: {}", "Fn(Report)", e)))?; + let __frame_res = (|| -> ::core::result::Result<(), __JniErr> { + let __vf0 = perftest_flat::report_into_struct(__cb_arg0); + let __cb0_obj5: jni::sys::jvalue; + let __cb0_obj6: jni::sys::jvalue; + let __cb0_obj7: jni::objects::JObject; + match &__vf0.outcome { + perftest_flat::Lookup::Absent => { + __cb0_obj5 = jni::sys::jvalue { i: 0 }; + __cb0_obj6 = jni::sys::jvalue { j: 0i64 }; + __cb0_obj7 = jni::objects::JObject::null(); + } + perftest_flat::Lookup::Found(__sv0) => { + let __enc___cb0_obj6 = match Summary_to_jlong_3cb103b9( + &mut env, + __sv0.clone(), + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + __cb0_obj6 = jni::sys::jvalue { + j: __enc___cb0_obj6, + }; + __cb0_obj5 = jni::sys::jvalue { i: 1 }; + __cb0_obj7 = jni::objects::JObject::null(); + } + perftest_flat::Lookup::Failed(__sv0) => { + let __enc___cb0_obj7 = match String_to_JString_c7f3ca43( + &mut env, + __sv0.clone(), + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + __cb0_obj7 = __enc___cb0_obj7.into(); + __cb0_obj5 = jni::sys::jvalue { i: 2 }; + __cb0_obj6 = jni::sys::jvalue { j: 0i64 }; + } + } + let __cb0_obj0: jni::sys::jvalue = { + let __enc0 = match i64_to_jlong_fbf9a9bc( + &mut env, + perftest_flat::summary_count(&__vf0.summary), + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + jni::sys::jvalue { j: __enc0 } + }; + let __cb0_obj1: jni::sys::jvalue = { + let __enc1 = match f64_to_jdouble_9e4a8f70( + &mut env, + perftest_flat::summary_total(&__vf0.summary), + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + jni::sys::jvalue { d: __enc1 } + }; + let __cb0_obj2: jni::objects::JObject = { + let __enc2 = match Option_Stamp_to_JObject_6375b503( + &mut env, + __vf0.taken, + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + __enc2 + }; + let __cb0_obj3: jni::sys::jvalue = { + let __enc3 = match i64_to_jlong_fbf9a9bc( + &mut env, + __vf0.origin.secs, + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + jni::sys::jvalue { j: __enc3 } + }; + let __cb0_obj4: jni::sys::jvalue = { + let __enc4 = match i64_to_jlong_fbf9a9bc( + &mut env, + __vf0.origin.nanos, + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + jni::sys::jvalue { j: __enc4 } + }; + let __cb0_obj8: jni::objects::JObject = { + let __enc8 = match String_to_JString_c7f3ca43( + &mut env, + __vf0.label, + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()), + ); + } + }; + __enc8.into() + }; + let __call_res: ::core::result::Result<(), __JniErr> = unsafe { + env.call_method_unchecked( + &callback_global_ref, + __invoke_id, + jni::signature::ReturnType::Primitive( + jni::signature::Primitive::Void, + ), + &[ + __cb0_obj0, + __cb0_obj1, + jni::sys::jvalue { + l: __cb0_obj2.as_raw(), + }, + __cb0_obj3, + __cb0_obj4, + __cb0_obj5, + __cb0_obj6, + jni::sys::jvalue { + l: __cb0_obj7.as_raw(), + }, + jni::sys::jvalue { + l: __cb0_obj8.as_raw(), + }, + ], + ) + } + .map(|_| ()) + .map_err(|e| { + let _ = env.exception_describe(); + <__JniErr as ::core::convert::From< + String, + >>::from(e.to_string()) + }); + __call_res?; + Ok(()) + })(); + let _ = unsafe { env.pop_local_frame(&jni::objects::JObject::null()) }; + __frame_res?; + Ok(()) + })() + .map_err(|e| tracing::error!("{} callback error: {e}", "Fn(Report)")); + }) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + dead_code, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn JObject_to_impl_Fn_Storage_Send_Sync_static_2f26edcf<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, @@ -6787,6 +7052,28 @@ pub(crate) unsafe fn Option_Priority_to_JObject_ad5cbb32<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn Option_Stamp_to_JObject_6375b503<'a>( + env: &mut jni::JNIEnv<'a>, + v: Option, +) -> ::core::result::Result, __JniErr> { + Ok({ + match v { + Some(value) => Stamp_to_JObject_f6b1e942(env, value)?, + None => jni::objects::JObject::null().into(), + } + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + dead_code, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn Option_Summary_to_jlong_828826f3<'a>( env: &mut jni::JNIEnv<'a>, v: Option<&perftest_flat::Summary>, @@ -7112,6 +7399,23 @@ pub(crate) unsafe fn RepliesConfig_to_JObject_eb8e9079<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn Report_to_jlong_eaed4ba1<'a>( + env: &mut jni::JNIEnv<'a>, + v: perftest_flat::Report, +) -> ::core::result::Result { + Ok(std::boxed::Box::into_raw(std::boxed::Box::new(v)) as i64) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + dead_code, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn Result_Storage_StorageError_to_Storage_7ccce404<'a>( env: &mut jni::JNIEnv<'a>, v: Result, @@ -8220,6 +8524,30 @@ pub(crate) unsafe fn jlong_to_PayloadVecHandler_b32d2812<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn jlong_to_Report_eaed4ba1<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::sys::jlong, +) -> ::core::result::Result, __JniErr> { + if *v == 0 || (*v & 1) == 1 { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from("Operation on a closed native handle.".to_string()), + ); + } + Ok(unsafe { OwnedObject::from_raw(*v as *const perftest_flat::Report) }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + dead_code, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn jlong_to_StorageError_26b2d298<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::sys::jlong, @@ -14200,6 +14528,66 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_readingSeries<'a } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_reportEach<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + n: jni::sys::jlong, + sink: jni::objects::JObject<'a>, + __error_sink: jni::objects::JObject<'a>, +) -> () { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let n = match jlong_to_i64_fbf9a9bc(&mut env, &n) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return (); + } + }; + let sink = match JObject_to_impl_Fn_Report_Send_Sync_static_eb5ca515( + &mut env, + &sink, + ) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return (); + } + }; + let __out = perftest_flat::report_each(n, sink); + match unit_to_unit_9ecccf8e(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + () + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_stampNanos<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index f429ae8c..fbcb2cbe 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -1382,6 +1382,109 @@ pub fn escape_probe_value(p: &EscapeProbe) -> i64 { p.value } +// ── Value form: a type's own accessors gathered into one struct (#213) ────── + +/// An opaque handle whose output boundary is declared from its **value form** +/// ([`report_to_struct`]) instead of a restated field list — the +/// `expand_return!(Report).fields(fields!(report_to_struct))` exercise. +/// +/// Its fields are chosen so each one lands on a different rule of the +/// expansion, and so the derived boundary is the same one a hand-written +/// `.field()` list would have produced: +/// +/// | field | rule | +/// |---|---| +/// | `summary` | its type has its own `expand_return!` ⇒ spliced into `(count, total)`, NOT handed over as a handle | +/// | `taken` | `Option` ⇒ stays ONE leaf, its converter builds the object | +/// | `origin` | a non-optional declared `data class` ⇒ INLINES into its own fields | +/// | `outcome` | a `sealed_class!` ⇒ its selector plus one group per alternative, with a handle payload | +/// | `label` | a plain leaf | +pub struct Report { + summary: Summary, + taken: Option, + origin: Stamp, + outcome: Lookup, + label: String, +} + +/// The value form of [`Report`]: its fields as data, handles staying handles. +#[prebindgen] +pub struct ReportStruct { + /// Decomposed by `Summary`'s own boundary decl, not delivered as a handle. + pub summary: Summary, + /// Absent when the report was never stamped. + pub taken: Option, + /// Always present, so it inlines into `origin_secs` / `origin_nanos`. + pub origin: Stamp, + /// A tag-gated group set, one alternative live, carrying a handle. + pub outcome: Lookup, + /// A plain string leaf beside the rest. + pub label: String, +} + +/// Build a [`Report`]. `count < 0` makes the outcome a failure, `0` absent. +#[prebindgen] +pub fn report_new(count: i64, total: f64, taken: bool, label: String) -> Report { + Report { + summary: summary_new(count.max(0), total), + taken: taken.then(|| stamp_new(7, 8)), + origin: stamp_new(1, 2), + outcome: lookup_of(count, total), + label, + } +} + +/// Decompose a [`Report`] into its value form — the accessor +/// `expand_return!(Report).fields(fields!(...))` names. Cloning the fields is +/// what makes this a *value* form; the generated code calls it ONCE per +/// delivery and reads every leaf off that one result. +#[prebindgen] +pub fn report_to_struct(r: &Report) -> ReportStruct { + ReportStruct { + summary: r.summary.clone(), + taken: r.taken, + origin: r.origin, + outcome: r.outcome.clone(), + label: r.label.clone(), + } +} + +/// The **consuming** value form of [`Report`] — the same fields, reached by +/// destroying the report instead of cloning out of a borrow. +/// +/// This is the shape a hot receive path wants. Every callback hands its value +/// over **owned** (`impl Fn(Report)`), so there is nothing to preserve: moving +/// the fields out costs nothing, while [`report_to_struct`] pays a clone per +/// handle field for a value it is about to drop. The binding picks whichever +/// form it declares; `expand_return!(Report).fields(fields!(report_into_struct))` +/// selects this one and the generated code then **moves** each field into its +/// leaf rather than cloning it. +#[prebindgen] +pub fn report_into_struct(r: Report) -> ReportStruct { + ReportStruct { + summary: r.summary, + taken: r.taken, + origin: r.origin, + outcome: r.outcome, + label: r.label, + } +} + +/// Deliver a [`Report`] to a callback — the decomposed value form arriving in +/// ONE crossing, which is the whole point of deriving the boundary rather than +/// handing over a handle the receiver must then query field by field. +#[prebindgen] +pub fn report_each(n: i64, sink: impl Fn(Report) + Send + Sync + 'static) { + for i in 0..n { + sink(report_new( + i - 1, + 10.0 * i as f64, + i % 2 == 0, + format!("r{i}"), + )); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/prebindgen/src/api/core/language/array_len.rs b/prebindgen/src/api/core/language/array_len.rs new file mode 100644 index 00000000..1c1ff3c9 --- /dev/null +++ b/prebindgen/src/api/core/language/array_len.rs @@ -0,0 +1,348 @@ +//! The fixed-size-array length subgrammar: one closed representation and one +//! fallible walk that produces it. +//! +//! A length must reduce to a **known number** — a generator runs in `build.rs`, +//! where it cannot evaluate arbitrary Rust. Two spellings reach one, and nothing +//! else does: an integer literal, or the bare name of a `#[prebindgen]` const +//! whose own initializer is an integer literal. +//! +//! Both the number and the const identity travel, as an [`ArrayExtent`]: the +//! value is the semantic length that makes `[u8; A]` and `[u8; 4]` one type, and +//! the identity is what lets a C header emit `uint8_t x[NAME]`. The *spelling* +//! travels separately and always — it is in the [`Type::syntax`](super::Type) +//! slice of the array type itself — so this carries only what a destination +//! language cannot read off the source. +//! +//! Ported from #212, which introduced it for issue #210. + +use std::{collections::HashMap, fmt, rc::Rc}; + +use quote::ToTokens; + +use super::origin::Origin; +use crate::SourceLocation; + +/// A length the prebindgen source language does not accept. +/// +/// Names the offending sub-expression, not just the array: the point of the +/// single walk is that it knows exactly which part it could not lower. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UnsupportedArrayLen { + /// The array type as written, for context — `[u8 ; A + 1]`. + pub array: String, + /// The sub-expression that could not be lowered — `A + 1`. + pub offending: String, + /// Why it could not be lowered. + pub reason: ArrayLenReason, +} + +/// Why [`lower_array_len`] refused a length. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ArrayLenReason { + /// Not a literal and not a plain name: arithmetic, a cast, a call, a block, + /// a `match`, a closure — anything with structure the grammar lacks. + NotLiteralOrName, + /// A literal that is not a non-negative integer. + NotAnIntegerLiteral, + /// An integer literal too large for `usize`. + IntegerOutOfRange, + /// A path with more than one segment, a qualified self, or a leading `::` — + /// `crate::limits::MAX`, `usize::MAX`, `::N`, `::MAX`. + /// + /// A length names a `#[prebindgen]` const, and those live in one flat, + /// uniquely-named namespace, so the bare name is the complete address. Any + /// longer path either restates that (`crate::limits::MAX`) or reaches + /// somewhere the frontend cannot follow — a module it does not index, an + /// associated const it never captured, a foreign crate. Neither can be + /// reduced to a number, and guessing between them is how a length silently + /// becomes the wrong one. + NotABareName, + /// A bare name that is not a `#[prebindgen]` const. + /// + /// The generated crate sees **only** what the macro exposed, so an unmarked + /// const is not merely unqualifiable — it does not exist downstream. + NotAMarkedConst, + /// A `#[prebindgen]` const whose own initializer is not an integer literal. + /// + /// `build.rs` cannot evaluate it, and a destination language that needs the + /// count cannot either. Hoist the arithmetic into the value the const is + /// computed FROM, or write the number. + ConstIsNotALiteral, + /// A `#[prebindgen]` const from a different source crate than the item + /// using it. + /// + /// Uniqueness holds across the *marked* namespace only, so a bare name in + /// one source crate can collide with an unmarked name of its own — the + /// frontend would silently bind to the other crate's value. Requiring the + /// length's const to come from the item's own crate makes that + /// unrepresentable. + ForeignSourceConst { + /// Crate the const was marked in. + const_crate: String, + /// Crate the item using it came from. + item_crate: String, + }, +} + +impl fmt::Display for UnsupportedArrayLen { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let what = match &self.reason { + ArrayLenReason::NotLiteralOrName => { + "is neither an integer literal nor the name of a const".to_string() + } + ArrayLenReason::NotAnIntegerLiteral => { + "is not a non-negative integer literal".to_string() + } + ArrayLenReason::IntegerOutOfRange => "does not fit in a `usize`".to_string(), + ArrayLenReason::NotABareName => { + "is a path rather than a bare name; `#[prebindgen]` items live in one flat \ + namespace, so the bare name is the whole address" + .to_string() + } + ArrayLenReason::NotAMarkedConst => { + "names no `#[prebindgen]` const — the generated crate sees only what the macro \ + exposed, so mark it `#[prebindgen]`" + .to_string() + } + ArrayLenReason::ConstIsNotALiteral => { + "names a const whose value is not an integer literal, so `build.rs` cannot \ + evaluate it" + .to_string() + } + ArrayLenReason::ForeignSourceConst { + const_crate, + item_crate, + } => format!( + "names a const marked in `{const_crate}`, but the item using it comes from \ + `{item_crate}` — a length must name a const from its own source crate" + ), + }; + write!( + f, + "fixed-size array `{}`: the length `{}` {what}. A length must be an integer literal, \ + or the bare name of a `#[prebindgen]` const that is itself an integer literal \ + (`pub const N: usize = 4;`) — a generator runs in `build.rs` and cannot evaluate \ + anything else, and some destination languages need the count as a number.", + self.array, self.offending + ) + } +} + +impl std::error::Error for UnsupportedArrayLen {} + +/// A fixed-size array's extent: the number, the const identity when the source +/// named one, and the spelling it was written with. +/// +/// # Three facts, three consumers — and no `==` +/// +/// The three answer different questions, and **deliberately no equality is +/// provided**, because there is no single one that could be right. A consumer +/// projects the fact it actually needs: +/// +/// | Question | Projection | +/// |---|---| +/// | is this the same type / the same converter? | [`Self::value`] | +/// | how does a C declaration spell it? | [`Self::origin`]`.syntax`, per occurrence | +/// | which consts must reach the header as a `#define`? | [`Self::const_id`] | +/// +/// A blanket `==` mixes them and is wrong under either reading: comparing +/// `source` makes `[u8; A]` differ from `[u8; 4]` when `A == 4` — one Rust type +/// reported as two — while ignoring the spelling makes `[u8; 4]` equal +/// `[u8; 0x04]`, whose retained syntax differs. Neither is type identity and +/// neither is spelling identity, so the choice belongs to whoever is asking. +/// +/// # Note for a converter table +/// +/// `value` being the type identity means several occurrences share one +/// converter, and their spellings differ. A shared converter therefore needs a +/// **canonical** Rust spelling chosen on purpose — the evaluated literal is the +/// obvious one — rather than whichever occurrence happened to populate a +/// deduplicated entry. +/// +/// This lives on the **use site** — a field's or parameter's type — and never on +/// anything keyed by type, for the same reason: two occurrences of one type may +/// name the length differently, so a type-keyed table could only report +/// whichever was stored last. +#[derive(Clone, Debug)] +pub struct ArrayExtent { + /// The evaluated length. The type identity: `[u8; A]` and `[u8; 4]` are one + /// Rust type when `A == 4`, and a destination language with no way to name a + /// Rust const needs the number. + pub value: usize, + /// How the length was addressed, so a C header can re-state + /// `uint8_t tag[TAG_LEN]` and know `TAG_LEN` must reach it. + pub source: ExtentSource, + /// The length expression as written — `4`, `0x04`, `TAG_LEN` — and where it + /// came from. The spelling of *this* occurrence, which is what a declaration + /// re-emits; two occurrences of one type may differ here. + pub origin: Origin, +} + +/// How an [`ArrayExtent`] was addressed at its use site. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum ExtentSource { + /// Written as an integer literal — `[u8; 4]`. + Literal, + /// Written as the name of a `#[prebindgen]` const — `[u8; TAG_LEN]`. + Const(ConstId), +} + +/// A `#[prebindgen]` const, identified the way the flat namespace identifies +/// everything: by name, plus the crate it was marked in. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ConstId { + pub name: String, + /// Crate the const was **declared** in, resolved by looking the name up + /// among the captured consts — never assumed from the use site. That is + /// what lets an extent refuse a const from another source crate. + /// + /// A bare crate name, not an [`Origin`]: it describes a *different* item + /// than the one being lowered, so it is not that node's provenance. + pub crate_name: Option, +} + +impl ArrayExtent { + /// The const this extent named, if it named one. + pub fn const_id(&self) -> Option<&ConstId> { + match &self.source { + ExtentSource::Literal => None, + ExtentSource::Const(id) => Some(id), + } + } +} + +/// One `#[prebindgen]` const, as a length sees it. +struct ConstEntry { + /// The literal value, or `None` when the initializer is not one. Present + /// either way, so "not a const" and "not a usable const" stay distinct + /// diagnostics. + value: Option, + /// Crate the const was marked in; `None` for an unstamped stream. Named + /// for what it is — a crate, not an [`Origin`], which belongs to the node + /// being lowered rather than to some other item it names. + crate_name: Option, +} + +/// The `#[prebindgen]` consts a length may name. +/// +/// Built once per parse, before any type is lowered, so a const may be declared +/// after the item that uses it. Deliberately holds **only consts**: nothing else +/// can be a length now that the grammar is a bare name, so there is no item-kind +/// enumeration here to drift. +pub(crate) struct ConstIndex { + consts: HashMap, +} + +impl ConstIndex { + /// `consts` maps each `#[prebindgen]` const's name to its initializer and + /// the crate it was marked in. + pub(crate) fn new(consts: I) -> Self + where + I: IntoIterator)>, + { + Self { + consts: consts + .into_iter() + .map(|(name, expr, crate_name)| { + let entry = ConstEntry { + value: int_literal(&expr), + crate_name, + }; + (name, entry) + }) + .collect(), + } + } +} + +/// The `usize` an expression denotes, if it is plainly an integer literal. +fn int_literal(expr: &syn::Expr) -> Option { + let syn::Expr::Lit(lit) = expr else { + return None; + }; + let syn::Lit::Int(int) = &lit.lit else { + return None; + }; + int.base10_parse::().ok() +} + +/// Lower one array length to its closed representation. +/// +/// **The contract**: `Ok` means the length was fully understood AND reduced to a +/// number. There is no separate acceptance check to drift from this — a form +/// this function does not lower is, by construction, a form the language does +/// not accept. That is the fix for the validator/rewriter pair this replaces +/// (issue #210), where eight defects in a row were two walks disagreeing about +/// one input. +/// +/// `array` is the array type's rendered form, for diagnostics, and `at` the +/// origin of the item the length was written in — which both becomes the +/// extent's own origin and pins which crate a named const may come from. +pub(crate) fn lower_array_len( + len: &syn::Expr, + array: &str, + at: &Rc, + consts: &ConstIndex, +) -> Result { + let item_crate = at.crate_name.as_deref(); + let origin = || Origin::new(len.clone(), Rc::clone(at)); + let fail = |reason| UnsupportedArrayLen { + array: array.to_string(), + offending: len.to_token_stream().to_string(), + reason, + }; + match len { + syn::Expr::Lit(_) => match int_literal(len) { + Some(value) => Ok(ArrayExtent { + value, + source: ExtentSource::Literal, + origin: origin(), + }), + None => Err(fail(match len { + // Told apart so an out-of-range integer does not report as + // "not an integer". + syn::Expr::Lit(l) if matches!(l.lit, syn::Lit::Int(_)) => { + ArrayLenReason::IntegerOutOfRange + } + _ => ArrayLenReason::NotAnIntegerLiteral, + })), + }, + syn::Expr::Path(ep) => { + // A bare name, and nothing longer. See `NotABareName` for why the + // flat namespace makes every longer path either redundant or + // unfollowable. + if ep.qself.is_some() || ep.path.leading_colon.is_some() || ep.path.segments.len() != 1 + { + return Err(fail(ArrayLenReason::NotABareName)); + } + let name = ep.path.segments[0].ident.to_string(); + let Some(entry) = consts.consts.get(&name) else { + return Err(fail(ArrayLenReason::NotAMarkedConst)); + }; + // Provenance before value: a same-named const from another source + // may well be a literal, and using it would be the silent wrong + // answer rather than an error. + if entry.crate_name.as_deref() != item_crate { + return Err(fail(ArrayLenReason::ForeignSourceConst { + const_crate: entry + .crate_name + .clone() + .unwrap_or_else(|| "".into()), + item_crate: item_crate.unwrap_or("").to_string(), + })); + } + let Some(value) = entry.value else { + return Err(fail(ArrayLenReason::ConstIsNotALiteral)); + }; + Ok(ArrayExtent { + value, + source: ExtentSource::Const(ConstId { + name, + crate_name: entry.crate_name.clone(), + }), + origin: origin(), + }) + } + _ => Err(fail(ArrayLenReason::NotLiteralOrName)), + } +} diff --git a/prebindgen/src/api/core/language/boundary.ledger b/prebindgen/src/api/core/language/boundary.ledger new file mode 100644 index 00000000..3a3b8fed --- /dev/null +++ b/prebindgen/src/api/core/language/boundary.ledger @@ -0,0 +1,73 @@ +# prebindgen source-syntax boundary ledger — issue #211. +# +# One line per file OUTSIDE `api/core/language/`, counting how many times a +# variant of a watched syn syntax enum is named in production code: +# +# syn::Type::Reference(r) => ... <- one site +# matches!(ty, syn::Expr::Lit(_)) <- one site +# +# Watched enums: syn::Type, syn::Expr. Test code is excluded. +# +# Every one of these is a place that independently decides what captured Rust +# MEANS. #211 says only `core::language` may do that, so this file freezes the +# population: any change fails `cargo test -p prebindgen boundary_ledger`. +# +# This is the CLASSIFY half of the rule, and only that half. Spelling the source +# — handing an element's `syntax` slice to `quote!` — names no variant and is +# not counted, because re-emitting what the source wrote is what generated Rust +# is for. Deciding what the source MEANT from that syntax is what it is not for. +# +# To change it deliberately: +# +# UPDATE_BOUNDARY_LEDGER=1 cargo test -p prebindgen boundary_ledger +# git diff prebindgen/src/api/core/language/boundary.ledger +# +# A count going DOWN is the goal (a classifier reads elements instead) and is +# still a ledger edit, so the win shows up in the diff. +# +# KNOWN BLIND SPOTS — classification this check cannot see, because it never +# writes a `syn::` path. Each is a candidate addition to WATCHED: +# +# * token-string classification, e.g. `core/domain.rs` matching +# `ty.to_token_stream().to_string()` against "i8" / "f32"; +# * ident-name classification, e.g. `seg.ident == "Option"`; +# * helper delegation — `jnigen/jni/classify.rs` is a whole classifier with +# zero watched sites; +# * syn enums outside WATCHED: Item, Fields, FnArg, ReturnType, +# GenericArgument, Pat, ...; +# * `types_util::match_pattern` unification against `parse_quote!(_)` +# patterns, which adds a shape rule with no watched site at all. +# +# A check that silently under-reports is worse than no check, which is why the +# gaps are listed here rather than implied away. + +4 api/core/expand.rs +12 api/core/registry.rs +40 api/core/types_util.rs +16 api/core/unfold.rs +8 api/lang/cbindgen/builder.rs +1 api/lang/cbindgen/convert.rs +5 api/lang/cbindgen/emit.rs +5 api/lang/cbindgen/mod.rs +6 api/lang/cbindgen/trait_impl.rs +13 api/lang/jnigen/jni/builder.rs +1 api/lang/jnigen/jni/emit/callback.rs +4 api/lang/jnigen/jni/emit/convert.rs +2 api/lang/jnigen/jni/emit/delivery.rs +10 api/lang/jnigen/jni/emit/flat_input.rs +17 api/lang/jnigen/jni/emit/names.rs +2 api/lang/jnigen/jni/emit/vec_build.rs +11 api/lang/jnigen/jni/emit/wrapper.rs +3 api/lang/jnigen/jni/fold.rs +5 api/lang/jnigen/jni/iface.rs +2 api/lang/jnigen/jni/kotlin_emit.rs +2 api/lang/jnigen/jni/overloads.rs +1 api/lang/jnigen/jni/prim.rs +3 api/lang/jnigen/jni/prim_array.rs +8 api/lang/jnigen/jni/render.rs +7 api/lang/jnigen/jni/selector.rs +11 api/lang/jnigen/jni/trait_impl.rs +2 api/lang/jnigen/jni/wire_access.rs +2 api/lang/jnigen/util.rs + +# total: 203 diff --git a/prebindgen/src/api/core/language/boundary.rs b/prebindgen/src/api/core/language/boundary.rs new file mode 100644 index 00000000..e2bc6215 --- /dev/null +++ b/prebindgen/src/api/core/language/boundary.rs @@ -0,0 +1,495 @@ +//! The mechanical boundary check: a committed ledger of every place outside +//! this module that classifies captured Rust syntax. +//! +//! Issue #211's sixth completion criterion is that a mechanical check must +//! prevent new source-syntax classifiers from appearing outside the frontend. +//! This is that check. It is test-only; it ships no production code. +//! +//! It measures exactly the rule the [element model](super) states — **classify +//! off `kind`, spell off `syntax`** — and it can, because it counts *variant +//! mentions* of a syn syntax enum rather than uses of syn values. Handing a +//! `syntax` slice to `quote!` names no variant and is invisible here; asking +//! `matches!(ty, syn::Type::Reference(_))` names one and is counted. So the +//! check needed no adaptation to the design: spelling was never what it saw. +//! +//! ## What a site is +//! +//! A place that looks at captured Rust syntax and asks *what shape is this*: +//! +//! ```ignore +//! syn::Type::Reference(r) => vec![(*r.elem).clone()], // core/types_util.rs +//! if !matches!(arg_ty, syn::Type::Reference(_)) => { .. } // jnigen emit/wrapper.rs +//! let syn::Type::Slice(s) = &*r.elem else { .. }; // cbindgen builder.rs +//! ``` +//! +//! Each is an independent decision about what the source Rust *means* — the +//! decisions #211 says belong to this module alone. #210 was two such places, +//! in one file, disagreeing about `[u8; ::N]`. +//! +//! So [`scan_tree`] counts, per file, how many times a variant of a [`WATCHED`] +//! syn syntax enum is named, and [`boundary_ledger`] fails if any count moved. +//! Up means a new classifier landed outside the language. Down means one was +//! migrated — the goal, but it still edits the ledger, so the progress of the +//! adapter migrations shows as a diff instead of being invisible. +//! +//! The seed count is therefore high: nothing consumes elements yet, so this +//! freezes the population as it stands and every later PR pays it down. +//! +//! The number's job is to not move, not to be a precise census: a few counted +//! occurrences *build* syntax rather than classify it (`cbindgen/builder.rs` +//! returns a `syn::Type::Path`, which is emission). Separating build from match +//! mechanically costs real code, and those sites are few and stable. +//! +//! ## Why the count is read off disk +//! +//! The crate has no default features and `unstable-cbindgen` gates the whole +//! cbindgen suite, so CI runs both `cargo test` and `cargo test --all +//! --all-features`. A check that inspected the *compiled* crate would count a +//! different population in each, i.e. give two answers in one CI run — the +//! failure mode of #219. Reading the source files makes the count +//! feature-independent, so both invocations agree. +//! +//! ## Why tokens, not a grep and not an AST visit +//! +//! A `grep -c` counts lines rather than occurrences, counts the inline +//! `#[cfg(test)] mod` blocks (so *adding a test* would fail the check, which is +//! how these ledgers die), and is defeated by one line: `use syn::Type;` then +//! `Type::Reference(_)` greps as zero. A `syn::visit::Visit` walk fixes those +//! and opens a worse hole — it cannot see inside macro invocations, and a large +//! share of the sites live in `matches!(..)`. A token walk sees patterns, +//! expressions, types and macro bodies alike, and is immune to line wrapping. + +use std::{ + collections::BTreeMap, + fs, + path::{Path, PathBuf}, + str::FromStr, +}; + +use proc_macro2::{Delimiter, TokenStream, TokenTree}; + +/// The syn syntax enums whose variants count as a classification site. +/// +/// Deliberately minimal. It is the population #211 and `docs/source-frontend.md` +/// already track, and the low-noise one: emitters construct `syn::Item::Fn` +/// constantly but rarely construct a `syn::Type` or `syn::Expr`, so adding +/// `Item` here would churn the ledger on every emission change. +/// +/// Extending this list is one line, plus a regenerated ledger whose diff is the +/// classification decision. +const WATCHED: &[&str] = &["Type", "Expr"]; + +/// Ledger location, relative to `src/`. +const LEDGER: &str = "api/core/language/boundary.ledger"; + +/// Regenerated verbatim on every write, so the contract cannot drift from the +/// numbers underneath it. +const HEADER: &str = "\ +# prebindgen source-syntax boundary ledger — issue #211. +# +# One line per file OUTSIDE `api/core/language/`, counting how many times a +# variant of a watched syn syntax enum is named in production code: +# +# syn::Type::Reference(r) => ... <- one site +# matches!(ty, syn::Expr::Lit(_)) <- one site +# +# Watched enums: syn::Type, syn::Expr. Test code is excluded. +# +# Every one of these is a place that independently decides what captured Rust +# MEANS. #211 says only `core::language` may do that, so this file freezes the +# population: any change fails `cargo test -p prebindgen boundary_ledger`. +# +# This is the CLASSIFY half of the rule, and only that half. Spelling the source +# — handing an element's `syntax` slice to `quote!` — names no variant and is +# not counted, because re-emitting what the source wrote is what generated Rust +# is for. Deciding what the source MEANT from that syntax is what it is not for. +# +# To change it deliberately: +# +# UPDATE_BOUNDARY_LEDGER=1 cargo test -p prebindgen boundary_ledger +# git diff prebindgen/src/api/core/language/boundary.ledger +# +# A count going DOWN is the goal (a classifier reads elements instead) and is +# still a ledger edit, so the win shows up in the diff. +# +# KNOWN BLIND SPOTS — classification this check cannot see, because it never +# writes a `syn::` path. Each is a candidate addition to WATCHED: +# +# * token-string classification, e.g. `core/domain.rs` matching +# `ty.to_token_stream().to_string()` against \"i8\" / \"f32\"; +# * ident-name classification, e.g. `seg.ident == \"Option\"`; +# * helper delegation — `jnigen/jni/classify.rs` is a whole classifier with +# zero watched sites; +# * syn enums outside WATCHED: Item, Fields, FnArg, ReturnType, +# GenericArgument, Pat, ...; +# * `types_util::match_pattern` unification against `parse_quote!(_)` +# patterns, which adds a shape rule with no watched site at all. +# +# A check that silently under-reports is worse than no check, which is why the +# gaps are listed here rather than implied away. +"; + +/// Every classification site in the crate's own sources, keyed by path relative +/// to `src/` with `/` separators so the ledger is identical on every platform. +/// +/// Excluded: this module's own directory (the language is where classification +/// is *supposed* to live), and test code — `tests.rs`, anything under a `tests/` +/// directory, and any item carrying `#[cfg(test)]`. +fn scan_tree(src_root: &Path) -> BTreeMap { + let mut out = BTreeMap::new(); + let mut stack = vec![src_root.to_path_buf()]; + while let Some(dir) = stack.pop() { + let entries = fs::read_dir(&dir).expect("src/ is readable"); + for entry in entries { + let path = entry.expect("readable dir entry").path(); + if path.is_dir() { + if path.file_name().is_some_and(|n| n == "tests") { + continue; + } + stack.push(path); + continue; + } + if path.extension().is_none_or(|e| e != "rs") + || path.file_name().is_some_and(|n| n == "tests.rs") + { + continue; + } + let rel = rel_key(src_root, &path); + if rel.starts_with("api/core/language/") { + continue; + } + let text = fs::read_to_string(&path).expect("source file is UTF-8"); + let n = scan_file(&text); + if n > 0 { + out.insert(rel, n); + } + } + } + out +} + +fn rel_key(root: &Path, path: &Path) -> String { + path.strip_prefix(root) + .expect("path came from walking root") + .components() + .map(|c| c.as_os_str().to_string_lossy()) + .collect::>() + .join("/") +} + +/// Sites in one file's text. +fn scan_file(text: &str) -> usize { + let stream = TokenStream::from_str(text).expect("source file parses as tokens"); + let aliases = collect_aliases(stream.clone()); + let mut n = 0; + count(stream, &aliases, &mut n); + n +} + +/// Idents bound to a watched enum by a `use` in this file — `use syn::Type` or +/// `use syn::Type as T`. +/// +/// Without this the check is defeated by a one-line import, and CI's +/// `imports_granularity=Crate` makes such an import more likely over time, not +/// less. No file does this today; the point is that none can start. +fn collect_aliases(stream: TokenStream) -> Vec { + let mut out = Vec::new(); + for run in use_runs(stream) { + // `use syn::...` only; `use crate::Type` binds something else entirely. + if run.first().map(String::as_str) != Some("syn") { + continue; + } + let mut i = 1; + while i < run.len() { + if WATCHED.contains(&run[i].as_str()) { + let renamed = run.get(i + 1).map(String::as_str) == Some("as"); + match (renamed, run.get(i + 2)) { + (true, Some(alias)) => { + out.push(alias.clone()); + i += 3; + continue; + } + _ => out.push(run[i].clone()), + } + } + i += 1; + } + } + out +} + +/// The idents of every `use` statement, flattened through brace groups so +/// `use syn::{Type, Expr as E}` reads as one run. +fn use_runs(stream: TokenStream) -> Vec> { + let mut out = Vec::new(); + let toks: Vec = stream.into_iter().collect(); + let mut i = 0; + while i < toks.len() { + match &toks[i] { + TokenTree::Ident(id) if *id == "use" => { + let mut run = Vec::new(); + i += 1; + while i < toks.len() && !is_punct(&toks[i], ';') { + flatten_idents(&toks[i], &mut run); + i += 1; + } + out.push(run); + } + // A `use` can be nested in a module body or a function. + TokenTree::Group(g) => { + out.extend(use_runs(g.stream())); + i += 1; + } + _ => i += 1, + } + } + out +} + +fn flatten_idents(tt: &TokenTree, out: &mut Vec) { + match tt { + TokenTree::Ident(id) => out.push(id.to_string()), + TokenTree::Group(g) => { + for inner in g.stream() { + flatten_idents(&inner, out); + } + } + _ => {} + } +} + +fn count(stream: TokenStream, aliases: &[String], n: &mut usize) { + let toks: Vec = stream.into_iter().collect(); + let mut i = 0; + while i < toks.len() { + // `#[cfg(test)] ` — skip the item wholesale. Five inline test + // modules exist and one is named `replace_ident_tests`, so a rule keyed + // on the module name would miss it. + if is_punct(&toks[i], '#') { + if let Some(TokenTree::Group(g)) = toks.get(i + 1) { + if g.delimiter() == Delimiter::Bracket && is_cfg_test(g.stream()) { + i += 2; + skip_item(&toks, &mut i); + continue; + } + } + i += 1; + continue; + } + // An import is not a classifier, and counting one would churn the ledger + // whenever rustfmt regroups imports. + if matches!(&toks[i], TokenTree::Ident(id) if *id == "use") { + while i < toks.len() && !is_punct(&toks[i], ';') { + i += 1; + } + i += 1; + continue; + } + // `syn :: :: ` + if matches!(&toks[i], TokenTree::Ident(id) if *id == "syn") + && is_sep(&toks, i + 1) + && matches!(toks.get(i + 3), Some(TokenTree::Ident(id)) if WATCHED.contains(&id.to_string().as_str())) + && is_sep(&toks, i + 4) + && matches!(toks.get(i + 6), Some(TokenTree::Ident(_))) + { + *n += 1; + i += 7; + continue; + } + // ` :: ` + if matches!(&toks[i], TokenTree::Ident(id) if aliases.contains(&id.to_string())) + && is_sep(&toks, i + 1) + && matches!(toks.get(i + 3), Some(TokenTree::Ident(_))) + { + *n += 1; + i += 4; + continue; + } + if let TokenTree::Group(g) = &toks[i] { + count(g.stream(), aliases, n); + } + i += 1; + } +} + +/// Consume the item an attribute was attached to: everything up to and including +/// its first brace-delimited body, or its terminating `;`, whichever comes first. +/// That covers `mod x { .. }`, `mod x;`, `fn f() -> T { .. }`, `use ..;` and +/// `impl T { .. }` alike, and steps over any further attributes on the way. +fn skip_item(toks: &[TokenTree], i: &mut usize) { + while *i < toks.len() { + let tt = &toks[*i]; + *i += 1; + match tt { + TokenTree::Group(g) if g.delimiter() == Delimiter::Brace => return, + _ if is_punct(tt, ';') => return, + _ => {} + } + } +} + +/// True only for the exact predicate `cfg(test)`. +/// +/// Deliberately conservative: this check's job is to stop a classifier hiding, so +/// anything it cannot *prove* is test-only gets counted. Looking for the ident +/// `test` anywhere in the predicate got that backwards — `cfg(not(test))` and +/// `cfg(any(test, feature = "x"))` both compile in a production build, and both +/// were treated as test-only, so a classifier under either evaded the ledger +/// entirely. +/// +/// `cfg(all(test, …))` is genuinely test-only and is nonetheless counted. That is +/// the safe direction to err in, and nothing in the tree writes one; if that +/// changes, widening this is a deliberate edit with a ledger diff attached. +fn is_cfg_test(stream: TokenStream) -> bool { + let mut idents = Vec::new(); + for tt in stream { + flatten_idents(&tt, &mut idents); + } + idents == ["cfg", "test"] +} + +/// A `::` is two `Punct` tokens, so a path separator spans `i` and `i + 1`. +fn is_sep(toks: &[TokenTree], i: usize) -> bool { + toks.get(i).is_some_and(|t| is_punct(t, ':')) + && toks.get(i + 1).is_some_and(|t| is_punct(t, ':')) +} + +fn is_punct(tt: &TokenTree, c: char) -> bool { + matches!(tt, TokenTree::Punct(p) if p.as_char() == c) +} + +fn render(sites: &BTreeMap) -> String { + let mut s = String::from(HEADER); + s.push('\n'); + for (path, n) in sites { + s.push_str(&format!("{n}\t{path}\n")); + } + s.push_str(&format!("\n# total: {}\n", sites.values().sum::())); + s +} + +fn parse(text: &str) -> BTreeMap { + text.lines() + .filter(|l| !l.starts_with('#') && !l.trim().is_empty()) + .map(|l| { + let (n, path) = l + .split_once('\t') + .expect("ledger line is `\\t`"); + ( + path.to_string(), + n.parse().expect("ledger count is a number"), + ) + }) + .collect() +} + +fn src_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("src") +} + +#[test] +fn boundary_ledger() { + let root = src_root(); + let found = scan_tree(&root); + let ledger_path = root.join(LEDGER); + + if std::env::var_os("UPDATE_BOUNDARY_LEDGER").is_some() { + fs::write(&ledger_path, render(&found)).expect("ledger is writable"); + return; + } + + let committed = parse(&fs::read_to_string(&ledger_path).expect("ledger is committed")); + if committed == found { + return; + } + + let mut drift = String::new(); + let paths: std::collections::BTreeSet<_> = committed.keys().chain(found.keys()).collect(); + for path in paths { + let (was, now) = (committed.get(path), found.get(path)); + if was != now { + let fmt = |v: Option<&usize>| v.map_or("-".to_string(), usize::to_string); + drift.push_str(&format!(" {path}: {} -> {}\n", fmt(was), fmt(now))); + } + } + panic!( + "BOUNDARY LEDGER DRIFT — source-syntax classification sites changed:\n\ + {drift}\n\ + A new classifier outside core::language needs one of:\n\ + \x20 * move it into core::language (see #211), or\n\ + \x20 * regenerate and justify the change in review:\n\ + \x20 UPDATE_BOUNDARY_LEDGER=1 cargo test -p prebindgen boundary_ledger\n\ + \x20 git diff prebindgen/src/{LEDGER}\n" + ); +} + +#[test] +fn scanner_recognizes_the_shapes_that_matter() { + // A match arm, and a `matches!` body — invisible to an AST visitor, which is + // why this walks tokens. + assert_eq!( + scan_file("fn f(t: &syn::Type) { match t { syn::Type::Slice(s) => g(s), _ => {} } }"), + 1 + ); + assert_eq!( + scan_file("fn f() -> bool { matches!(ty, syn::Type::Tuple(t) if t.elems.is_empty()) }"), + 1 + ); + assert_eq!( + scan_file("fn f() { let syn::Expr::Lit(l) = e else { return; }; }"), + 1 + ); + // Two on one line: a line count would report one. + assert_eq!( + scan_file("fn f() { if let (syn::Type::Path(a), syn::Type::Path(b)) = p {} }"), + 2 + ); + + // The one-line defeat the alias handling closes. + assert_eq!( + scan_file("use syn::Type;\nfn f() { if let Type::Reference(r) = t {} }"), + 1 + ); + assert_eq!( + scan_file( + "use syn::{Expr, Type as T};\nfn f() { if let T::Ptr(p) = t { h(Expr::Lit(l)) } }" + ), + 2 + ); + // An import is not a site, and neither is a non-syn `Type`. + assert_eq!(scan_file("use syn::Type;\nfn f() {}"), 0); + assert_eq!(scan_file("fn f() { if let Type::Reference(r) = t {} }"), 0); + + // Outside WATCHED — emitters construct these constantly. + assert_eq!(scan_file("fn f() { let i = syn::Item::Fn(f); }"), 0); + + // Test code does not count, whatever the module is called. + assert_eq!( + scan_file( + "fn f() { match t { syn::Type::Slice(s) => (), _ => () } }\n\ + #[cfg(test)]\n\ + mod replace_ident_tests { fn g() { let _ = syn::Type::Ptr(p); } }" + ), + 1 + ); + assert_eq!(scan_file("#[cfg(test)]\nmod tests;"), 0); + + // But ONLY code proven test-only. Both of these compile in a production + // build, and a rule that looked for the ident `test` anywhere let a + // classifier under either evade the count. + assert_eq!( + scan_file("#[cfg(not(test))]\nfn g() { let _ = syn::Type::Ptr(p); }"), + 1, + "cfg(not(test)) is production code" + ); + assert_eq!( + scan_file("#[cfg(any(test, feature = \"x\"))]\nfn g() { let _ = syn::Type::Ptr(p); }"), + 1, + "cfg(any(test, ..)) compiles whenever the other arm holds" + ); + // A feature literally named "test" is not the test predicate either. + assert_eq!( + scan_file("#[cfg(feature = \"test\")]\nfn g() { let _ = syn::Type::Ptr(p); }"), + 1 + ); +} diff --git a/prebindgen/src/api/core/language/element.rs b/prebindgen/src/api/core/language/element.rs new file mode 100644 index 00000000..5ab3e8fc --- /dev/null +++ b/prebindgen/src/api/core/language/element.rs @@ -0,0 +1,292 @@ +//! The elements: one variant per structure the source language allows. +//! +//! Every node — an item, a parameter, a field, a variant, a type — carries one +//! [`Origin`], so generated Rust names the source by re-emitting what the source +//! wrote, nothing re-parses a whole item to find a part of it, and no level has +//! to copy a piece of provenance down from the level above. +//! +//! **Structure only.** Everything that turns an element back into Rust tokens +//! lives in [`spell`](super::spell), so the shape of an element says nothing +//! about the language it came from. + +use super::{origin::Origin, ty::Type}; +use crate::SourceLocation; + +/// One structure of the prebindgen source language. +/// +/// The five modelled kinds, plus [`Element::Unsupported`] for anything the +/// language cannot express. There is no verbatim-passthrough variant: a +/// `#[prebindgen]` crate marks the items that cross the boundary, and the +/// supporting code around them is the consumer crate's job — the proc-macro +/// enforces that already, refusing to mark a `use`, `mod`, `impl` or +/// `macro_rules!` at all. +#[derive(Clone, Debug)] +pub enum Element { + Function(Function), + Struct(Struct), + /// An enum whose alternatives carry payloads — a sum type. + Variant(Variant), + /// An enum whose every alternative is fieldless — a named set of integers. + Enum(Enum), + Const(Const), + /// An item the language cannot express — a parameter type outside the + /// grammar, a `self` receiver, or a whole item kind it does not model such + /// as a `union`. + /// + /// Inert: it is indexed under its name so nothing else can claim it, and + /// the diagnosis rides along, to be raised by whatever declares it. See the + /// [module docs](super) on where acceptance is enforced. + Unsupported(Unsupported), +} + +impl Element { + /// The item's name, which is also its address: `#[prebindgen]` names live + /// in one flat namespace across every ingested source crate. + /// + /// `None` when the item has no address — an unnamed `const _` (each + /// source's injected feature guard, so several may coexist), or an item + /// kind with no identifier at all. + pub fn name(&self) -> Option<&syn::Ident> { + let named = match self { + Element::Function(f) => Some(&f.name), + Element::Struct(s) => Some(&s.name), + Element::Variant(v) => Some(&v.name), + Element::Enum(e) => Some(&e.name), + Element::Const(c) => Some(&c.name), + Element::Unsupported(u) => u.name.as_ref(), + }; + named.filter(|id| *id != "_") + } + + /// Where the item was captured, including the crate that marked it. + /// + /// The same location every component of this item carries — they share one + /// [`Origin::location`], because one captured record is one item. + pub fn location(&self) -> &SourceLocation { + match self { + Element::Function(f) => &f.origin.location, + Element::Struct(s) => &s.origin.location, + Element::Variant(v) => &v.origin.location, + Element::Enum(e) => &e.origin.location, + Element::Const(c) => &c.origin.location, + Element::Unsupported(u) => &u.origin.location, + } + } + + /// The whole item as the source wrote it. + pub fn syntax(&self) -> syn::Item { + match self { + Element::Function(f) => syn::Item::Fn(f.origin.syntax.clone()), + Element::Struct(s) => syn::Item::Struct(s.origin.syntax.clone()), + Element::Variant(v) => syn::Item::Enum(v.origin.syntax.clone()), + Element::Enum(e) => syn::Item::Enum(e.origin.syntax.clone()), + Element::Const(c) => syn::Item::Const(c.origin.syntax.clone()), + Element::Unsupported(u) => u.origin.syntax.clone(), + } + } +} + +/// A `#[prebindgen]` free function. +#[derive(Clone, Debug)] +pub struct Function { + pub name: syn::Ident, + /// Parameters in declaration order. + pub params: Vec, + /// What the function returns. An elided return is + /// [`TypeKind::Unit`](super::TypeKind), exactly as a written `-> ()` is: + /// they mean the same thing, differ only in spelling, and every consumer + /// today already normalizes one to the other on the spot. + pub ret: Type, + /// The whole item: attributes, `cfg`, doc comments, body. + pub origin: Origin, +} + +/// One parameter of a [`Function`]. +#[derive(Clone, Debug)] +pub struct Param { + pub name: syn::Ident, + pub ty: Type, + /// The parameter as written — `mode: Mode`. + pub origin: Origin, +} + +/// A `#[prebindgen]` struct: a product of fields, or an opaque one. +#[derive(Clone, Debug)] +pub struct Struct { + pub name: syn::Ident, + /// The fields, when they are a boundary surface — `Some(vec![])` for a + /// struct with none. + /// + /// `None` means **opaque**: the contents are not part of the boundary and + /// are deliberately not lowered, so a field type outside the grammar is not + /// an error. That is today's tuple struct — usable as a handle, its fields + /// never crossed by any adapter — and lowering them would turn types that + /// are ignored now into refusals. + /// + /// Whether a shape has named or positional fields is not recorded here: a + /// [`Field`] already knows its own address, and the delimiters are + /// spelling, read off `syntax` by + /// [`spell::fields`](super::spell::fields). + pub fields: Option>, + pub origin: Origin, +} + +impl Struct { + /// The modelled fields — empty when the struct is opaque. + pub fn fields(&self) -> &[Field] { + self.fields.as_deref().unwrap_or(&[]) + } +} + +/// A `#[prebindgen]` enum whose alternatives carry payloads — a sum type. +/// +/// Distinct from [`Enum`], which is the fieldless shape, because the two are +/// consumed as different constructs and **numbered differently**. A sum's +/// alternatives are identified by position: the mirror an adapter builds carries +/// no `repr` and numbers its own arms, so a Rust discriminant would be the wrong +/// answer here — which is why there is no slot for one. +/// +/// Both shapes are spelled `enum` in Rust and both keep a `syn::ItemEnum` in +/// their origin. Which one an item *is* is the classification, and it is decided +/// once: any alternative with a field makes it a `Variant`. +#[derive(Clone, Debug)] +pub struct Variant { + pub name: syn::Ident, + /// Alternatives in declaration order; `alternatives[i].index == i`. + pub alternatives: Vec, + pub origin: Origin, +} + +/// One alternative of a [`Variant`]. +#[derive(Clone, Debug)] +pub struct Alternative { + pub name: syn::Ident, + /// Position within its sum, `0..N-1` — the same fact a [`Field`] carries, + /// for the same reason: a node handed out on its own still knows where it + /// sits. + /// + /// This is the *only* numbering a sum has. What a destination language does + /// with it is its own business: one may transmit it to say which alternative + /// is live, another may send a name instead. + pub index: usize, + /// The alternative's payload, in declaration order. May be empty — a sum can + /// mix payload-carrying and payload-free alternatives, and only the presence + /// of *some* payload makes the type a `Variant`. + pub fields: Vec, + /// The alternative as written: delimiters, attributes, doc comments. + pub origin: Origin, +} + +impl Alternative { + /// True when this alternative carries no payload. + /// + /// The *group* question, not the syntax one: `B`, `B()` and `B {}` are all + /// empty by this test, and [`spell::fields`](super::spell::fields) is what + /// keeps their delimiters apart. + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } +} + +/// A `#[prebindgen]` enum whose every alternative is fieldless — the C-style +/// shape, a named set of integers. +/// +/// Distinct from [`Variant`] because the identity of a member here is the value +/// Rust **assigns** it, not where it sits: a C header re-states each `= expr` +/// and a Kotlin `enum class` entry is `NAME(7)`. A sum has no such value, which +/// is why the two are separate entities rather than one with a dead field each. +#[derive(Clone, Debug)] +pub struct Enum { + pub name: syn::Ident, + /// Values in declaration order; `values[i].index == i`. + pub values: Vec, + pub origin: Origin, +} + +impl Enum { + /// Every value paired with the number Rust assigns it, or the first value + /// whose discriminant could not be evaluated. + /// + /// This is the numbering a destination language needs when it has no way to + /// reference a Rust constant: a Kotlin `enum class` entry is `NAME(3)`, and + /// the generated `int → value` decode matches on the same numbers, so both + /// come from here and cannot drift. An `Err` is a refusal for *that* + /// consumer only — one that re-emits the source spelling never asks. + pub fn discriminant_values(&self) -> Result, &syn::Ident> { + self.values + .iter() + .map(|v| match v.discriminant { + Some(n) => Ok((&v.name, n)), + None => Err(&v.name), + }) + .collect() + } +} + +/// One named value of an [`Enum`]. +#[derive(Clone, Debug)] +pub struct EnumValue { + pub name: syn::Ident, + /// Position within its enum, `0..N-1`. Not the identity — see + /// [`Self::discriminant`] — but the same "where it sits" fact every node in + /// an ordered list carries, and what a consumer falls back to when a + /// discriminant cannot be evaluated. + pub index: usize, + /// The value Rust assigns — an explicit `= N` sets it, an implicit value + /// takes the previous plus one, starting at 0. **This shape's identity.** + /// + /// `None` once a spelling the frontend cannot evaluate (a `const`, a `cfg`, + /// arithmetic) has broken the chain, or once the chain has run out of `i64`. + /// That is not a failure: only a consumer that needs the *number* is + /// affected, and one that re-emits the *spelling* reads + /// [`Self::origin`]`.syntax.discriminant` instead. + pub discriminant: Option, + /// The value as written: `= 0x07`, attributes, doc comments — and its + /// delimiters, since `B` and `B()` are both fieldless and still spelled + /// differently. + pub origin: Origin, +} + +/// One field of a [`Struct`] or of an [`Alternative`]. +#[derive(Clone, Debug)] +pub struct Field { + /// The field's name, or `None` for a positional one. + pub name: Option, + /// Position within its struct or alternative, `0..N-1` — the same fact an + /// [`Alternative`] carries. + /// + /// The address of a positional field. A named field has one too, and simply + /// does not need it: it is addressed by name, so this is available rather + /// than used — the same way it carries its item's location. + pub index: usize, + pub ty: Type, + /// The field as written — `pub id: u64`, attributes and docs included. + pub origin: Origin, +} + +/// A `#[prebindgen]` const. +/// +/// Also the home of the unnamed `const _` feature guard each source injects: it +/// is a const, so it is modelled as one, and [`Element::name`] returning `None` +/// for `_` is what keeps several of them from colliding in the flat namespace. +#[derive(Clone, Debug)] +pub struct Const { + pub name: syn::Ident, + pub ty: Type, + /// The whole item — the initializer expression included, which is where a + /// consumer that re-emits the value reads it from. + pub origin: Origin, +} + +/// An item the language cannot express. +#[derive(Clone, Debug)] +pub struct Unsupported { + /// The item's identifier, or `None` for an item kind that has none. + pub name: Option, + /// What could not be expressed, ready to be raised by whatever declares + /// this item. Boxed: it is the size outlier among the elements, and this + /// one is the rare variant. + pub error: Box, + /// The item as written, so a diagnosis can quote the source. + pub origin: Origin, +} diff --git a/prebindgen/src/api/core/language/mod.rs b/prebindgen/src/api/core/language/mod.rs new file mode 100644 index 00000000..6a018aac --- /dev/null +++ b/prebindgen/src/api/core/language/mod.rs @@ -0,0 +1,836 @@ +//! The prebindgen **source language**: one parser from captured records to +//! [`Element`]s. +//! +//! > Naming: `core::language` is the *source* language — the Rust subset a +//! > `#[prebindgen]` crate may write. `api::lang` is the *destination* adapters +//! > (C, JNI). They are opposite ends of the pipeline. +//! +//! ```text +//! Source(s) ──items──> Language ──Elements──> Registry ──> adapters +//! raw records parse + indexes classify off `kind` +//! (syn::Item) validate elements spell off `origin` +//! ``` +//! +//! [`Language::source`] folds the first arrow in for the common case, so a build +//! script names one directory and gets elements; [`Language::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 classification** — [`TypeKind`], the field list, which of the two +//! enum shapes an item is — that says what the source *means*, in terms every +//! destination language shares; +//! * 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 off `origin.syntax`.** +//! +//! Matching a `syn::Type` or `syn::Expr` variant outside this module is a +//! classifier, and issue #211 says classification lives here alone. Passing a +//! node's [`Origin`] into `quote!` is spelling, and spelling the source is +//! exactly what generated Rust must do — see [`spell`] for the helpers that do +//! it. +//! +//! # What earns a variant +//! +//! A concept, not a Rust spelling. The test is whether a *destination* language +//! would act on the distinction; if only Rust can see it, it is spelling, and +//! the slice already carries it: +//! +//! | Rust writes | The model says | Because | +//! |---|---|---| +//! | `String`, `str` | [`TypeKind::Str`] | one concept, two Rust types | +//! | `Vec`, `[T]` | [`TypeKind::Sequence`] | a run of `T`; owned vs borrowed is the [`Ref`](TypeKind::Ref) layer's fact | +//! | `Box` | whatever `T` is | an owned `T` either way | +//! | `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 | +//! | no `->`, `-> ()` | [`TypeKind::Unit`] | the same function | +//! | `*const T` | *rejected* | a source crate is idiomatic Rust; the adapter owns pointers | +//! +//! 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`, `Foo<'a>` is not `Foo`. A model that carries no syntax has +//! to become *lossless* to serve it — which is how a language-neutral IR turns +//! back into a second `syn`. Carrying the original slice costs nothing and lets +//! the classification stay small: a lifetime, a delimiter and a literal's base +//! are simply not modelled facts. +//! +//! # 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. +//! +//! But an item the language cannot express is not automatically a build failure. +//! A source crate may mark items no binding uses, and those have never been +//! required to be expressible — the pipeline scans a signature only once an +//! adapter *declares* it. So parsing diagnoses per item and defers the raising: +//! such an item becomes [`Element::Unsupported`], carrying the diagnosis, inert +//! until something declares it. Only whole-stream rules — a duplicate name in +//! the flat namespace — are [`ParseError`]s, because no declaration can make +//! two items with one name unambiguous. +//! +//! There is **no verbatim passthrough**, 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 an item kind this module does +//! not model is a `union` or a type alias, and it is diagnosed like any other +//! thing the language cannot express. +//! +//! # 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`, `fn f()`, `struct S` | `T` as a nominal reference | a parameter is indistinguishable from an item named `T` | +//! +//! All three are [`ItemError`]s, inert until declared, like any other refusal. 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. + +use std::{fmt, rc::Rc}; + +use quote::ToTokens; + +mod array_len; +#[cfg(test)] +mod boundary; +mod element; +mod origin; +pub mod spell; +mod ty; + +#[cfg(test)] +mod tests; + +use self::{array_len::ConstIndex, ty::lower_type}; +pub use self::{ + array_len::{ArrayExtent, ArrayLenReason, ConstId, ExtentSource, UnsupportedArrayLen}, + element::{ + Alternative, Const, Element, Enum, EnumValue, Field, Function, Param, Struct, Unsupported, + Variant, + }, + origin::Origin, + ty::{ScalarKind, Type, TypeId, TypeKind, UnsupportedType, UnsupportedTypeReason}, +}; +use crate::SourceLocation; + +/// The parser for the prebindgen source language. +/// +/// Carries no configuration about what it *accepts* — that is a property of the +/// language, not of the call site. What it does carry is **what to parse**: +/// collect the inputs, then [`parse`](Self::parse) once. +/// +/// # Reading a source directory +/// +/// A build script's whole job, in one expression — the +/// [`Source`](crate::Source) step included. Pass +/// `::PREBINDGEN_OUT_DIR`: +/// +/// ``` +/// # prebindgen::Source::init_doctest_simulate(); +/// use prebindgen::core::Language; +/// +/// let elements = Language::new().source("source_ffi").parse()?; +/// assert_eq!(elements.len(), 2); +/// # Ok::<_, prebindgen::core::language::ParseError>(()) +/// ``` +/// +/// # Reading a stream +/// +/// [`Self::items`] takes any `(syn::Item, SourceLocation)` iterator, so +/// everything a [`Source`](crate::Source) can express still composes — a group +/// selection, a renamed dependency, several sources at once. The feeders +/// accumulate, so mix them freely: +/// +/// ``` +/// # prebindgen::Source::init_doctest_simulate(); +/// use prebindgen::{core::Language, Source}; +/// +/// // A dependency renamed in Cargo.toml needs the name THIS crate uses, so it +/// // is configured rather than named by directory. +/// let helpers = Source::builder("source_ffi").crate_name("helpers").build(); +/// let elements = Language::new() +/// .items(helpers.items_in_groups(&["functions"])) +/// .parse()?; +/// assert_eq!(elements.len(), 1); +/// # Ok::<_, prebindgen::core::language::ParseError>(()) +/// ``` +/// +/// # Why accumulate, rather than parse each input +/// +/// The rules that make a parse fail are **whole-stream** rules: one flat +/// namespace across every ingested crate, one const index an array length may +/// reach into, one set of source modules to normalize paths against. None can be +/// decided per input, so every input is in hand before any of it is classified. +#[derive(Debug, Default)] +pub struct Language { + items: Vec<(syn::Item, SourceLocation)>, +} + +impl Language { + pub fn new() -> Self { + Self::default() + } + + /// Every `#[prebindgen]` item captured in `dir`. + /// + /// Sugar for [`Self::items`] over [`Source::items_all`](crate::Source::items_all), + /// which is the whole of what a build script normally needs — pass + /// `::PREBINDGEN_OUT_DIR`. Reach for a + /// [`Source`](crate::Source) directly, and feed it through [`Self::items`], + /// only when it needs configuring. + /// + /// Panics the way [`Source::new`](crate::Source::new) does if `dir` is not + /// readable prebindgen output: a build script has nothing to recover with. + /// + /// ``` + /// # prebindgen::Source::init_doctest_simulate(); + /// use prebindgen::core::Language; + /// + /// let elements = Language::new().source("source_ffi").parse().unwrap(); + /// let mut names: Vec = + /// elements.iter().filter_map(|e| e.name()).map(|n| n.to_string()).collect(); + /// names.sort(); + /// assert_eq!(names, ["TestStruct", "test_function"]); + /// ``` + pub fn source>(self, dir: P) -> Self { + let source = crate::Source::new(dir); + self.items(source.items_all()) + } + + /// Add a captured item stream. + /// + /// The general feeder: any `(syn::Item, SourceLocation)` iterator, so + /// item-level selection and multi-source composition stay upstream where + /// they already are. Call it as often as needed; the streams accumulate. + /// + /// ``` + /// # prebindgen::Source::init_doctest_simulate(); + /// use prebindgen::{core::Language, Source}; + /// + /// let source = Source::new("source_ffi"); + /// let elements = Language::new() + /// .items(source.items_in_groups(&["structs"])) + /// .parse() + /// .unwrap(); + /// assert_eq!(elements.len(), 1); + /// ``` + pub fn items(mut self, items: I) -> Self + where + I: IntoIterator, + { + self.items.extend(items); + self + } + + /// Parse everything collected so far into elements. + /// + /// **Transactional**: an `Err` yields no elements at all, so a refused + /// stream cannot leave a half-built model behind. + /// + /// Order-independent: source modules are gathered, and consts indexed, + /// before anything is lowered — so a cross-source type reference and an + /// array length may both name something declared later, in this input or + /// another. + pub fn parse(self) -> Result, ParseError> { + let mut items = self.items; + + // Pass 0: normalize every item's types to the canonical flat spelling + // before a single one is classified. `std::option::Option` is an + // `Option`, and `source_a::TypeA` is `TypeA` — decisions this module + // owns, so it must be the one to see the reduced form. Gathering EVERY + // module name first is what makes a cross-source reference in an + // earlier item normalize the same as in a later one. + // + // The consequence is deliberate and stated on `Origin`: a slice + // is the spelling generation must EMIT, which is the normalized one — + // the flat namespace is what the generated crate can actually name. + let mut modules: Vec = Vec::new(); + for (_, loc) in &items { + if let Some(crate_name) = &loc.crate_name { + let module = crate_name.replace('-', "_"); + if !modules.contains(&module) { + modules.push(module); + } + } + } + for (item, _) in &mut items { + crate::api::core::types_util::normalize_item_types(item, &modules); + } + + // Pass 1: the consts an array length may name. Unnamed `const _` items + // are excluded for the same reason `Element::name` skips them — they + // are not addressable, so no length can name one. + let consts = ConstIndex::new(items.iter().filter_map(|(item, loc)| match item { + syn::Item::Const(c) if c.ident != "_" => Some(( + c.ident.to_string(), + (*c.expr).clone(), + loc.crate_name.clone(), + )), + _ => None, + })); + + // Pass 2: lower, checking the flat namespace as we go. + let mut out: Vec = Vec::with_capacity(items.len()); + let mut seen: Vec<(syn::Ident, SourceLocation)> = Vec::new(); + for (item, loc) in items { + let element = lower_item(item, loc, &consts); + if let Some(name) = element.name() { + if let Some((first_name, first)) = seen.iter().find(|(n, _)| n == name) { + return Err(ParseError::DuplicateName(Box::new(DuplicateName { + name: first_name.clone(), + first: first.clone(), + second: element.location().clone(), + }))); + } + seen.push((name.clone(), element.location().clone())); + } + out.push(element); + } + Ok(out) + } +} + +/// If `ty` is `impl Fn(T1, T2, ...) + Send + Sync + 'static`, return the `Fn` +/// argument types in declaration order. Otherwise `None`. +/// +/// A callback **returns nothing**, and that is checked, not assumed: a written +/// `-> ()` is the same thing spelled out, and any other return is refused. +/// [`TypeKind::Callback`] has no slot for one, so accepting `impl Fn() -> u8` +/// would drop a fact a destination language needs — and drop it silently, which +/// is worse than the refusal. +/// +/// The callback grammar, and the language's alone: [`TypeKind::Callback`] is +/// exactly what this accepts, so acceptance cannot drift from classification. +/// The registry re-exports it for the consumers that have not migrated yet. +pub fn extract_fn_trait_args(ty: &syn::Type) -> Option> { + let syn::Type::ImplTrait(it) = ty else { + return None; + }; + let mut args: Option> = None; + let mut has_send = false; + let mut has_sync = false; + let mut has_static = false; + for bound in &it.bounds { + match bound { + syn::TypeParamBound::Trait(tb) => { + let last = tb.path.segments.last()?; + let name = last.ident.to_string(); + match name.as_str() { + "Fn" => { + let syn::PathArguments::Parenthesized(p) = &last.arguments else { + return None; + }; + match &p.output { + syn::ReturnType::Default => {} + syn::ReturnType::Type(_, t) if ty::is_unit_type(t) => {} + syn::ReturnType::Type(..) => return None, + } + args = Some(p.inputs.iter().cloned().collect()); + } + "Send" => has_send = true, + "Sync" => has_sync = true, + _ => return None, + } + } + syn::TypeParamBound::Lifetime(lt) if lt.ident == "static" => has_static = true, + _ => return None, + } + } + if has_send && has_sync && has_static { + args + } else { + None + } +} + +/// A rule of the language that no single item can satisfy on its own, and that +/// no adapter declaration can excuse. +#[derive(Clone, Debug)] +pub enum ParseError { + /// Two `#[prebindgen]` items share a name. Names live in one flat namespace + /// across every ingested source crate, so this is ambiguous however the + /// crates are arranged. + DuplicateName(Box), +} + +/// The two items of a [`ParseError::DuplicateName`]. +#[derive(Clone, Debug)] +pub struct DuplicateName { + pub name: syn::Ident, + pub first: SourceLocation, + pub second: SourceLocation, +} + +impl fmt::Display for ParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ParseError::DuplicateName(d) => write!( + f, + "duplicate `#[prebindgen]` name `{}`: first at {}, again at {} — marked items \ + share one flat namespace across all source crates", + d.name, d.first, d.second + ), + } + } +} + +impl std::error::Error for ParseError {} + +/// Why one item could not be expressed in the language. +/// +/// Carried by [`Element::Unsupported`] rather than raised at parse time: see +/// the [module docs](self) on where acceptance is enforced. +#[derive(Clone, Debug)] +pub enum ItemError { + /// A `self` receiver. `#[prebindgen]` captures free functions only. + UnsupportedReceiver, + /// A parameter pattern that is not a plain name — `(a, b): (u8, u8)`. + UnsupportedParamPattern { pattern: String }, + /// A parameter's type is not in the language. + ParamType { + param: syn::Ident, + source: UnsupportedType, + }, + /// A return type is not in the language. + ReturnType { source: UnsupportedType }, + /// A named struct field's type is not in the language. + FieldType { + field: syn::Ident, + source: UnsupportedType, + }, + /// A variant payload's type is not in the language. + VariantFieldType { + variant: syn::Ident, + /// The field's name, or its position for a tuple variant. + field: String, + source: UnsupportedType, + }, + /// A const's type is not in the language. + ConstType { source: UnsupportedType }, + /// An `async fn`. + /// + /// The most dangerous shape to accept quietly: [`Function`] has a direct + /// return, so an `async fn ping()` lowers as one returning `()`, and a + /// generated wrapper calls it, drops the future and exports a function whose + /// body never runs. + UnsupportedAsync, + /// A C-variadic tail — `fn f(a: u8, ...)`. + /// + /// [`Function`] holds ordinary parameters only, so the tail would simply be + /// dropped from the signature. + UnsupportedVariadic, + /// A type or const generic parameter on the item. + /// + /// The elements have no generic binder, so a `T` in a field or parameter + /// would lower as [`TypeKind::Named`] — an ordinary nominal reference into + /// the flat namespace, indistinguishable from a real item called `T`. That + /// loses the scoping every downstream resolver needs, and no destination + /// language can express an uninstantiated parameter anyway. + /// + /// A lifetime parameter is *not* this: lifetimes are spelling and already + /// travel in the syntax. + UnsupportedGenericParam { + param: String, + /// `a type parameter` / `a const generic parameter`. + kind: &'static str, + }, + /// A whole item kind the language does not model — a `union`, a type alias. + /// + /// The proc-macro refuses to mark a `use`, `mod`, `impl` or `macro_rules!` + /// at all, so only the kinds it accepts can reach here. + UnsupportedItemKind { kind: &'static str }, +} + +impl fmt::Display for ItemError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ItemError::UnsupportedReceiver => write!( + f, + "takes a `self` receiver; `#[prebindgen]` captures free functions only" + ), + ItemError::UnsupportedParamPattern { pattern } => write!( + f, + "parameter pattern `{pattern}` is not a plain name — bind each parameter to one \ + identifier" + ), + ItemError::ParamType { param, source } => { + write!(f, "parameter `{param}`: {source}") + } + ItemError::ReturnType { source } => write!(f, "return type: {source}"), + ItemError::FieldType { field, source } => write!(f, "field `{field}`: {source}"), + ItemError::VariantFieldType { + variant, + field, + source, + } => write!(f, "variant `{variant}` field `{field}`: {source}"), + ItemError::ConstType { source } => write!(f, "const type: {source}"), + ItemError::UnsupportedAsync => write!( + f, + "is an `async fn`; the boundary has no way to drive a future, and the generated \ + wrapper would drop it and export a function whose body never runs — expose a \ + blocking wrapper instead" + ), + ItemError::UnsupportedVariadic => write!( + f, + "has a C-variadic tail, which the prebindgen source language does not model — \ + take a slice, or one parameter per value" + ), + ItemError::UnsupportedGenericParam { param, kind } => write!( + f, + "declares `{param}`, {kind}: the prebindgen source language has no generic \ + binder, so an uninstantiated parameter is indistinguishable from a nominal type \ + of the same name and no destination language can express it — write the \ + concrete types, one marked item per instantiation (a newtype is the usual way)" + ), + ItemError::UnsupportedItemKind { kind } => write!( + f, + "is {kind}; the prebindgen source language models functions, structs, enums and \ + consts — everything else belongs in the consumer crate" + ), + } + } +} + +impl std::error::Error for ItemError {} + +/// Lower one captured item. Total: every item becomes an element, and an item +/// whose contents the language cannot express becomes [`Element::Unsupported`] +/// rather than failing the parse. +fn lower_item(item: syn::Item, loc: SourceLocation, consts: &ConstIndex) -> Element { + // One captured record is one item, so this is allocated once and shared by + // the item and every node lowered out of it. + let at = Rc::new(loc); + match item { + syn::Item::Fn(f) => match lower_fn(&f, &at, consts) { + Ok(func) => Element::Function(func), + Err(error) => unsupported(f.sig.ident.clone(), syn::Item::Fn(f), &at, error), + }, + syn::Item::Struct(s) => match lower_struct(&s, &at, consts) { + Ok(st) => Element::Struct(st), + Err(error) => unsupported(s.ident.clone(), syn::Item::Struct(s), &at, error), + }, + syn::Item::Enum(e) => match lower_enum(&e, &at, consts) { + Ok(element) => element, + Err(error) => unsupported(e.ident.clone(), syn::Item::Enum(e), &at, error), + }, + // Including the unnamed `const _` each source injects as its feature + // guard: it is a const, so it is one here. `Element::name` returns + // `None` for `_`, which is what keeps several sources' guards from + // colliding in the flat namespace. + syn::Item::Const(c) => match lower_type(&c.ty, consts, &at) { + Ok(ty) => Element::Const(Const { + name: c.ident.clone(), + ty, + origin: Origin::new(c, at), + }), + Err(source) => unsupported( + c.ident.clone(), + syn::Item::Const(c), + &at, + ItemError::ConstType { source }, + ), + }, + // An item kind the language does not model. The proc-macro accepts + // only six kinds, so in practice this is a `union` or a type alias — + // both named, neither ever written by a source crate. It is diagnosed + // rather than carried: a `#[prebindgen]` crate marks what crosses the + // boundary, and the code around that belongs to the consumer. + other => { + let (name, kind) = match &other { + syn::Item::Union(u) => (Some(u.ident.clone()), "a union"), + syn::Item::Type(t) => (Some(t.ident.clone()), "a type alias"), + _ => (None, "an item kind"), + }; + unsupported(name, other, &at, ItemError::UnsupportedItemKind { kind }) + } + } +} + +fn unsupported( + name: impl Into>, + syntax: syn::Item, + at: &Rc, + error: ItemError, +) -> Element { + Element::Unsupported(Unsupported { + name: name.into(), + error: Box::new(error), + origin: Origin::new(syntax, Rc::clone(at)), + }) +} + +/// Refuse a type or const generic parameter, naming the first one found. +/// +/// Lifetimes pass: they say nothing a destination language can act on, and the +/// spelling that needs them is already in the syntax — the same call +/// [`lower_type`] makes for a lifetime *argument*. +fn reject_generic_params(generics: &syn::Generics) -> Result<(), ItemError> { + for param in &generics.params { + let (name, kind) = match param { + syn::GenericParam::Lifetime(_) => continue, + syn::GenericParam::Type(t) => (t.ident.to_string(), "a type parameter"), + syn::GenericParam::Const(c) => (c.ident.to_string(), "a const generic parameter"), + }; + return Err(ItemError::UnsupportedGenericParam { param: name, kind }); + } + Ok(()) +} + +fn lower_fn( + f: &syn::ItemFn, + at: &Rc, + consts: &ConstIndex, +) -> Result { + // Shapes `Function` has no slot for, and would therefore drop in silence. + if f.sig.asyncness.is_some() { + return Err(ItemError::UnsupportedAsync); + } + if f.sig.variadic.is_some() { + return Err(ItemError::UnsupportedVariadic); + } + reject_generic_params(&f.sig.generics)?; + let mut params = Vec::with_capacity(f.sig.inputs.len()); + for input in &f.sig.inputs { + let pt = match input { + syn::FnArg::Receiver(_) => return Err(ItemError::UnsupportedReceiver), + syn::FnArg::Typed(pt) => pt, + }; + let syn::Pat::Ident(pat) = &*pt.pat else { + return Err(ItemError::UnsupportedParamPattern { + pattern: pt.pat.to_token_stream().to_string(), + }); + }; + let name = pat.ident.clone(); + let ty = lower_type(&pt.ty, consts, at).map_err(|source| ItemError::ParamType { + param: name.clone(), + source, + })?; + params.push(Param { + name, + ty, + origin: Origin::new(pt.clone(), Rc::clone(at)), + }); + } + // An elided return and a written `-> ()` are the same function. The model + // says so once, here, instead of leaving every consumer to normalize one to + // the other — which is what they all do today, in eight separate copies. + let ret = match &f.sig.output { + syn::ReturnType::Default => Type { + kind: TypeKind::Unit, + origin: Origin::new(syn::parse_quote!(()), Rc::clone(at)), + }, + syn::ReturnType::Type(_, t) => { + lower_type(t, consts, at).map_err(|source| ItemError::ReturnType { source })? + } + }; + Ok(Function { + name: f.sig.ident.clone(), + params, + ret, + origin: Origin::new(f.clone(), Rc::clone(at)), + }) +} + +fn lower_struct( + s: &syn::ItemStruct, + at: &Rc, + consts: &ConstIndex, +) -> Result { + reject_generic_params(&s.generics)?; + let fields = match &s.fields { + syn::Fields::Named(named) => { + let mut out = Vec::with_capacity(named.named.len()); + for (index, f) in named.named.iter().enumerate() { + let name = f.ident.clone().expect("named fields have idents"); + let ty = lower_type(&f.ty, consts, at).map_err(|source| ItemError::FieldType { + field: name.clone(), + source, + })?; + out.push(Field { + name: Some(name), + index, + ty, + origin: Origin::new(f.clone(), Rc::clone(at)), + }); + } + Some(out) + } + // Opaque: a tuple struct's contents are not a boundary surface, so they + // are not lowered and a field type outside the grammar is not an error. + syn::Fields::Unnamed(_) => None, + syn::Fields::Unit => Some(Vec::new()), + }; + Ok(Struct { + name: s.ident.clone(), + fields, + origin: Origin::new(s.clone(), Rc::clone(at)), + }) +} + +/// Lower an `enum` item to whichever of the two shapes it is. +/// +/// **The classification**: any alternative with a field makes it a [`Variant`] — +/// a sum, numbered by position. Otherwise it is an [`Enum`] — a named set of +/// integers, identified by the value Rust assigns. Both are spelled `enum` in +/// Rust and both keep the `syn::ItemEnum`; only what a destination language can +/// do with them differs, and that is what the model records. +/// +/// `enum E {}` has no alternative carrying anything, so it is the degenerate +/// `Enum`. +fn lower_enum( + e: &syn::ItemEnum, + at: &Rc, + consts: &ConstIndex, +) -> Result { + reject_generic_params(&e.generics)?; + + if e.variants.iter().any(|v| !v.fields.is_empty()) { + return Ok(Element::Variant(lower_variant(e, at, consts)?)); + } + Ok(Element::Enum(lower_c_enum(e, at))) +} + +/// The payload-carrying shape. Position is the only numbering a sum has, so no +/// discriminant is evaluated: the mirror an adapter builds numbers its own arms. +fn lower_variant( + e: &syn::ItemEnum, + at: &Rc, + consts: &ConstIndex, +) -> Result { + let mut alternatives = Vec::with_capacity(e.variants.len()); + for (index, v) in e.variants.iter().enumerate() { + let mut fields = Vec::with_capacity(v.fields.len()); + for (field_index, f) in v.fields.iter().enumerate() { + let ty = + lower_type(&f.ty, consts, at).map_err(|source| ItemError::VariantFieldType { + variant: v.ident.clone(), + field: match &f.ident { + Some(id) => id.to_string(), + None => field_index.to_string(), + }, + source, + })?; + fields.push(Field { + name: f.ident.clone(), + index: field_index, + ty, + origin: Origin::new(f.clone(), Rc::clone(at)), + }); + } + alternatives.push(Alternative { + name: v.ident.clone(), + index, + fields, + origin: Origin::new(v.clone(), Rc::clone(at)), + }); + } + Ok(Variant { + name: e.ident.clone(), + alternatives, + origin: Origin::new(e.clone(), Rc::clone(at)), + }) +} + +/// The fieldless shape. Nothing here can fail to lower — there are no field +/// types — so an unevaluable discriminant ends the numeric chain rather than +/// refusing the item. +fn lower_c_enum(e: &syn::ItemEnum, at: &Rc) -> Enum { + let mut values = Vec::with_capacity(e.variants.len()); + // Rust's own numbering rule: an explicit `= N` sets the value, an implicit + // one takes the previous plus one, starting at 0. + let mut next: Option = Some(0); + for (index, v) in e.variants.iter().enumerate() { + let discriminant = match v.discriminant.as_ref() { + Some((_, expr)) => int_literal(expr), + None => next, + }; + // `checked_add`: a discriminant at the top of the range is valid Rust + // (`#[repr(u64)] enum E { A = i64::MAX as u64, B }`), so running out of + // `i64` ends the numeric chain exactly as an unevaluable spelling does. + // The spelling is untouched either way — it is in `EnumValue::origin`. + next = discriminant.and_then(|n| n.checked_add(1)); + + values.push(EnumValue { + name: v.ident.clone(), + index, + discriminant, + origin: Origin::new(v.clone(), Rc::clone(at)), + }); + } + Enum { + name: e.ident.clone(), + values, + origin: Origin::new(e.clone(), Rc::clone(at)), + } +} + +/// Pull a signed integer out of a literal expression (`5`, `-3`, `0x07`). +/// `None` for anything else — a `const`, a path, arithmetic. +fn int_literal(expr: &syn::Expr) -> Option { + i64::try_from(int_literal_wide(expr)?).ok() +} + +/// [`int_literal`] before the range check. +/// +/// The magnitude is parsed **wider than the result** so the sign can be applied +/// first: `-9223372036854775808` is `i64::MIN` and a valid Rust discriminant, +/// but its magnitude is one past `i64::MAX`, so parsing the digits as `i64` +/// would reject the whole literal. A magnitude too large for `i128` fails here +/// and is reported as an unevaluable discriminant, which is the existing +/// contract for anything the frontend cannot reduce to a number. +fn int_literal_wide(expr: &syn::Expr) -> Option { + match expr { + syn::Expr::Lit(lit) => match &lit.lit { + syn::Lit::Int(int) => int.base10_parse::().ok(), + _ => None, + }, + syn::Expr::Unary(syn::ExprUnary { + op: syn::UnOp::Neg(_), + expr, + .. + }) => int_literal_wide(expr).map(|v| -v), + _ => None, + } +} diff --git a/prebindgen/src/api/core/language/origin.rs b/prebindgen/src/api/core/language/origin.rs new file mode 100644 index 00000000..743fe237 --- /dev/null +++ b/prebindgen/src/api/core/language/origin.rs @@ -0,0 +1,88 @@ +//! Where a node came from: the syntax it was built from, and the source that +//! syntax arrived in. +//! +//! One uniform property of **every** node in the model — item, parameter, +//! field, variant, type, array extent alike. Some levels know less than others +//! (a field has no line of its own), but the shape does not change with the +//! level, so nothing has to copy a piece of provenance downward by hand. + +use std::rc::Rc; + +use quote::ToTokens; + +use crate::SourceLocation; + +/// The syntax a node was built from, plus where that syntax came from. +/// +/// # Why the two travel together +/// +/// `syn` tokens normally carry spans, so in principle the syntax alone could +/// answer "where was this written". Not here: the proc-macro serializes each +/// marked item as a **string** into JSONL, and `build.rs` re-parses it, so every +/// span in [`Self::syntax`] points into an anonymous buffer. +/// [`SourceLocation::from_span`] captures file, line and column at +/// macro-expansion time — while real rustc spans still exist — precisely because +/// they cannot survive that trip. +/// +/// # Why the location is shared +/// +/// One captured record is one item, so there is exactly one location per item +/// and none of its own for any component. An item and everything inside it — +/// parameters, fields, variants, types, extents — therefore point at the *same* +/// [`SourceLocation`], which is both the honest answer and the cheap one: a +/// struct with twenty fields keeps one location, not twenty copies of a path. +/// +/// `Rc` rather than `Arc`: this holds `syn` values, which are `!Send`, so the +/// model can never cross a thread boundary and an atomic refcount would only +/// cost. [`TypeKey`](crate::api::core::registry::TypeKey) made the same call for +/// the same reason. +/// +/// # The rule about origins +/// +/// > A reference carries a name; the declaration carries the origin. +/// +/// `location.crate_name` here is the crate whose source this node was written +/// *in* — the use site. It is never part of a referenced item's identity: +/// [`TypeId`](super::TypeId) is a name alone, because `#[prebindgen]` names live +/// in one flat namespace. [`ConstId`](super::ConstId) is not an exception — the +/// crate it records is the const's *declaring* crate, obtained by lookup, and +/// that is exactly what lets an array extent refuse a const from another source. +#[derive(Clone, Debug)] +pub struct Origin { + /// The exact tokens this node was built from. Feed it to `quote!` when + /// generated Rust has to spell the node; never `match` on it to decide what + /// the node is — see the [module docs](super) on classifying off `kind`. + pub syntax: S, + /// The captured item this node belongs to, shared with every sibling. + pub location: Rc, +} + +impl Origin { + pub fn new(syntax: S, location: Rc) -> Self { + Self { syntax, location } + } + + /// The crate this node's source was written in. + /// + /// The **use site**, not the declaring crate of anything it names. + pub fn crate_name(&self) -> Option<&str> { + self.location.crate_name.as_deref() + } + + /// The same location over different syntax — for building a component's + /// origin from the item's. + pub fn with(&self, syntax: T) -> Origin { + Origin { + syntax, + location: Rc::clone(&self.location), + } + } +} + +/// Spelling a node emits its syntax, so a site that re-states a whole node +/// needs no `.syntax` hop. +impl ToTokens for Origin { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + self.syntax.to_tokens(tokens) + } +} diff --git a/prebindgen/src/api/core/language/spell.rs b/prebindgen/src/api/core/language/spell.rs new file mode 100644 index 00000000..d45962d1 --- /dev/null +++ b/prebindgen/src/api/core/language/spell.rs @@ -0,0 +1,83 @@ +//! Spelling an element back as Rust: the one place the model turns into tokens. +//! +//! The other half of **classify off `kind`, spell off `syntax`**. Every helper +//! here reads an element's retained syntax and emits Rust; none of them decides +//! what anything *means*. Keeping them out of [`element`](super::element) is +//! what lets that module describe structure alone — a `Field` is a name, a +//! position and a type, and whether Rust writes it `id: v` or `v` is answered +//! here. +//! +//! Nothing outside generated Rust reads any of this: a destination language +//! cannot tell `E::B` from `E::B()`, which is exactly why the delimiters are +//! spelling rather than a modelled shape. + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; + +use super::element::{Alternative, EnumValue, Field, Struct}; + +/// Spell a field group: `head`, `head(parts…)` or `head { parts… }`, following +/// the delimiters the source wrote. +/// +/// The one place those delimiters are chosen — for match patterns and +/// constructors alike, in either direction, for a struct and a variant alike. +/// `B()` carries no payload and still must be written `E::B()` wherever Rust +/// names it. +/// +/// `head` is the type's or variant's path, and each part is an already-rendered +/// [`Field::bind`]. +pub fn fields(shape: &syn::Fields, head: TokenStream, parts: &[TokenStream]) -> TokenStream { + match shape { + syn::Fields::Unit => head, + syn::Fields::Unnamed(_) => quote!(#head(#(#parts),*)), + syn::Fields::Named(_) => quote!(#head { #(#parts),* }), + } +} + +impl Alternative { + /// [`fields`] over this alternative's own delimiters. + pub fn spell(&self, head: TokenStream, parts: &[TokenStream]) -> TokenStream { + fields(&self.origin.syntax.fields, head, parts) + } +} + +impl EnumValue { + /// [`fields`] over this value's own delimiters. + /// + /// A fieldless alternative still has them: `A`, `B()` and `C {}` carry no + /// payload alike, and Rust demands the delimiters wherever the last two are + /// named. `parts` is therefore always empty — the signature matches + /// [`Alternative::spell`] so one caller can spell either. + pub fn spell(&self, head: TokenStream, parts: &[TokenStream]) -> TokenStream { + fields(&self.origin.syntax.fields, head, parts) + } +} + +impl Struct { + /// [`fields`] over this struct's own delimiters — the dual of + /// [`Variant::spell`], and the reason neither needs a modelled shape. + pub fn spell(&self, head: TokenStream, parts: &[TokenStream]) -> TokenStream { + fields(&self.origin.syntax.fields, head, parts) + } +} + +impl Field { + /// How the field is addressed in a pattern or an initializer: by name when + /// it has one, else by position. + pub fn member(&self) -> syn::Member { + match &self.name { + Some(id) => syn::Member::Named(id.clone()), + None => syn::Member::Unnamed(syn::Index::from(self.index)), + } + } + + /// The field bound to `bind`, shaped for whichever address it uses — + /// `id: __f0` for a named field, `__f0` for a positional one. The part + /// [`fields`] takes. + pub fn bind(&self, bind: &impl ToTokens) -> TokenStream { + match &self.name { + Some(id) => quote!(#id: #bind), + None => quote!(#bind), + } + } +} diff --git a/prebindgen/src/api/core/language/tests/acceptance.rs b/prebindgen/src/api/core/language/tests/acceptance.rs new file mode 100644 index 00000000..39eee2ac --- /dev/null +++ b/prebindgen/src/api/core/language/tests/acceptance.rs @@ -0,0 +1,891 @@ +//! The acceptance matrix: source spelling → element, or a diagnosis naming the +//! item and the component that could not be expressed. +//! +//! Ported from #212/#226 and extended to functions, consts and item kinds. + +use super::*; + +/// Lower one type by putting it in a struct field, and report what the language +/// made of it. The field path is used because a field is the position every +/// consumer already agrees is a boundary surface. +fn lower(ty: proc_macro2::TokenStream) -> Result { + let item: syn::Item = syn::parse_quote!( + pub struct S { + pub f: #ty, + } + ); + match parse(vec![tag_len_const(), item]).remove(1) { + Element::Struct(s) => Ok(s.fields()[0].ty.clone()), + Element::Unsupported(u) => match *u.error { + ItemError::FieldType { source, .. } => Err(source), + other => panic!("expected a field-type diagnosis, got {other}"), + }, + other => panic!("expected a struct, got {}", describe(&other)), + } +} + +fn kind(ty: proc_macro2::TokenStream) -> TypeKind { + lower(ty).expect("in the language").kind +} + +fn reason(ty: proc_macro2::TokenStream) -> UnsupportedTypeReason { + lower(ty).expect_err("outside the language").reason +} + +// ── Types ────────────────────────────────────────────────────────────── + +#[test] +fn scalars_and_strings() { + assert!(matches!( + kind(quote::quote!(u8)), + TypeKind::Scalar(ScalarKind::U8) + )); + assert!(matches!( + kind(quote::quote!(bool)), + TypeKind::Scalar(ScalarKind::Bool) + )); + assert!(matches!( + kind(quote::quote!(f64)), + TypeKind::Scalar(ScalarKind::F64) + )); + assert!(matches!(kind(quote::quote!(String)), TypeKind::Str)); + assert!(matches!(kind(quote::quote!(())), TypeKind::Unit)); +} + +/// `String` and `str` are one concept, and the borrow is the `Ref` layer's +/// fact. Every adapter already treats `&str` as a borrowed string by hand; +/// classifying `str` as a nominal type would send them all looking for an item +/// named `str` to resolve. +#[test] +fn a_string_is_a_string_however_it_is_spelled() { + assert!(matches!(kind(quote::quote!(str)), TypeKind::Str)); + for spelling in [quote::quote!(&str), quote::quote!(&String)] { + let TypeKind::Ref { mutable, inner } = kind(spelling) else { + panic!("a borrow"); + }; + assert!(!mutable); + assert!(matches!(inner.kind, TypeKind::Str)); + } +} + +#[test] +fn the_builtin_generics() { + assert!(matches!( + kind(quote::quote!(Option)), + TypeKind::Optional(_) + )); + assert!(matches!( + kind(quote::quote!(Vec)), + TypeKind::Sequence(_) + )); + assert!(matches!( + kind(quote::quote!(Result)), + TypeKind::Fallible { .. } + )); +} + +/// `Box` **is** `T` — an owned value either way, and no destination language +/// can tell them apart, so it carries no kind of its own. The `Box` survives +/// where it matters: in the syntax generated Rust spells. +#[test] +fn a_box_classifies_as_what_it_wraps() { + let ty = lower(quote::quote!(Box)).expect("in the language"); + assert!(matches!(ty.kind, TypeKind::Str)); + assert_eq!(tokens(&ty.origin.syntax), "Box < String >"); + + // And it composes: the nullable heap string of a `#[repr(C)]` struct field + // is an optional string, spelled with its `Box`. + let ty = lower(quote::quote!(Option>)).expect("in the language"); + let TypeKind::Optional(inner) = &ty.kind else { + panic!("an option"); + }; + assert!(matches!(inner.kind, TypeKind::Str)); + assert_eq!(tokens(&inner.origin.syntax), "Box < String >"); +} + +/// A builtin must be spelled BARE **after normalization**: the real std path +/// reduces and classifies, while a path-qualified lookalike is a foreign type +/// that merely shares the name, and collapsing it would silently retype the +/// field. +#[test] +fn a_qualified_builtin_is_a_named_type() { + assert!(matches!( + kind(quote::quote!(std::option::Option)), + TypeKind::Optional(_) + )); + let TypeKind::Named { id, .. } = kind(quote::quote!(foreign::Option)) else { + panic!("a named type"); + }; + assert_eq!(id.name, "foreign::Option"); +} + +#[test] +fn references() { + assert!(matches!( + kind(quote::quote!(&Sample)), + TypeKind::Ref { mutable: false, .. } + )); + assert!(matches!( + kind(quote::quote!(&mut Sample)), + TypeKind::Ref { mutable: true, .. } + )); +} + +/// `Vec` and `[T]` are one concept — a run of `T` — and ownership is the +/// `Ref` layer's fact, not a second variant. That is already how the pipeline +/// behaves: one `Shape::Iterable` covers both, and jnigen rewrites a `&[T]` +/// input into the `Vec<_>` pattern outright. +#[test] +fn a_sequence_is_a_sequence_borrowed_or_owned() { + assert!(matches!( + kind(quote::quote!(Vec)), + TypeKind::Sequence(_) + )); + // Bare, as a callback argument is written: `impl Fn([T])`. + assert!(matches!(kind(quote::quote!([u8])), TypeKind::Sequence(_))); + let TypeKind::Ref { inner, .. } = kind(quote::quote!(&[u8])) else { + panic!("a reference"); + }; + assert!(matches!(inner.kind, TypeKind::Sequence(_))); +} + +/// A raw pointer is not in the language. A `#[prebindgen]` crate is idiomatic +/// Rust and the adapter owns the lowering to pointers — no adapter has a +/// selection arm for one, so accepting it would only defer the failure to a +/// late "unresolved type". +#[test] +fn a_raw_pointer_is_not_in_the_language() { + assert_eq!( + reason(quote::quote!(*const u8)), + UnsupportedTypeReason::UnsupportedForm + ); + assert_eq!( + reason(quote::quote!(*mut Sample)), + UnsupportedTypeReason::UnsupportedForm + ); +} + +/// A lifetime argument is accepted and not modelled — `Foo<'a, T>` classifies +/// as `Foo` with one type argument, and the spelling keeps the rest. +#[test] +fn a_lifetime_argument_is_spelling_only() { + let ty = lower(quote::quote!(Foo<'a, u8>)).expect("in the language"); + let TypeKind::Named { id, args } = &ty.kind else { + panic!("a named type"); + }; + assert_eq!(id.name, "Foo"); + assert_eq!(args.len(), 1); + assert_eq!(tokens(&ty.origin.syntax), "Foo < 'a , u8 >"); +} + +#[test] +fn the_callback_form() { + let TypeKind::Callback { args } = + kind(quote::quote!(impl Fn(&Sample, u32) + Send + Sync + 'static)) + else { + panic!("a callback"); + }; + assert_eq!(args.len(), 2); +} + +/// A callback returns nothing, and that is **checked**. `TypeKind::Callback` has +/// no slot for a return, so accepting `impl Fn() -> u8` would drop a fact a +/// destination language needs — silently, which is worse than the refusal. +#[test] +fn a_callback_must_return_nothing() { + // Written out, `-> ()` is the same callback. + let TypeKind::Callback { args } = + kind(quote::quote!(impl Fn(u32) -> () + Send + Sync + 'static)) + else { + panic!("a callback"); + }; + assert_eq!(args.len(), 1); + + // Anything else is not the accepted `impl Trait` form. + for spelling in [ + quote::quote!(impl Fn() -> u8 + Send + Sync + 'static), + quote::quote!(impl Fn(u32) -> Sample + Send + Sync + 'static), + quote::quote!(impl Fn() -> Option + Send + Sync + 'static), + ] { + assert_eq!( + reason(spelling), + UnsupportedTypeReason::DisallowedImplTrait, + "a returning callback is refused, not silently truncated" + ); + } +} + +#[test] +fn types_outside_the_language() { + assert_eq!( + reason(quote::quote!((u8, u8))), + UnsupportedTypeReason::UnsupportedTuple + ); + assert_eq!( + reason(quote::quote!(::Assoc)), + UnsupportedTypeReason::AssociatedType + ); + assert_eq!( + reason(quote::quote!(Option)), + UnsupportedTypeReason::WrongGenericArity { expected: 1 } + ); + assert_eq!( + reason(quote::quote!(Result)), + UnsupportedTypeReason::WrongGenericArity { expected: 2 } + ); + assert_eq!( + reason(quote::quote!(impl Iterator)), + UnsupportedTypeReason::DisallowedImplTrait + ); + assert_eq!( + reason(quote::quote!(dyn Fn(u8))), + UnsupportedTypeReason::UnsupportedForm + ); + assert_eq!( + reason(quote::quote!(!)), + UnsupportedTypeReason::UnsupportedForm + ); +} + +// ── Array extents ────────────────────────────────────────────────────── + +fn extent_reason(ty: proc_macro2::TokenStream) -> ArrayLenReason { + match reason(ty) { + UnsupportedTypeReason::BadArrayExtent(e) => e.reason, + other => panic!("expected an extent diagnosis, got {other:?}"), + } +} + +#[test] +fn extents_outside_the_subgrammar() { + assert_eq!( + extent_reason(quote::quote!([u8; TAG_LEN + 1])), + ArrayLenReason::NotLiteralOrName + ); + assert_eq!( + extent_reason(quote::quote!([u8; crate::limits::MAX])), + ArrayLenReason::NotABareName + ); + assert_eq!( + extent_reason(quote::quote!([u8; UNMARKED])), + ArrayLenReason::NotAMarkedConst + ); + assert_eq!( + extent_reason(quote::quote!([u8; 'c'])), + ArrayLenReason::NotAnIntegerLiteral + ); +} + +/// A const may be declared after the item that uses it: the const index is +/// built before anything is lowered. +#[test] +fn an_extent_may_name_a_const_declared_later() { + let elements = parse(vec![ + syn::parse_quote!( + pub struct Marker { + pub tag: [u8; TAG_LEN], + } + ), + tag_len_const(), + ]); + assert_eq!( + as_struct(&elements[0]).fields()[0] + .ty + .array_extent() + .expect("an extent") + .value, + 4 + ); +} + +/// An extent carries three facts for three questions, and they come apart. No +/// blanket equality could serve all three, which is why the type provides none: +/// a consumer projects the one it needs. +#[test] +fn the_three_extent_projections_are_independent() { + // `A` and `TAG_LEN` are both 4; `0x04` and `4` are the same literal value + // spelled differently. + let elements = parse(vec![ + syn::parse_quote!( + pub struct Marker { + pub by_const: [u8; TAG_LEN], + pub by_other_const: [u8; ALSO_FOUR], + pub by_literal: [u8; 4], + pub by_hex_literal: [u8; 0x04], + pub longer: [u8; 8], + } + ), + tag_len_const(), + syn::parse_quote!( + pub const ALSO_FOUR: usize = 4; + ), + ]); + let fields = as_struct(&elements[0]).fields(); + let at = |i: usize| fields[i].ty.array_extent().expect("an extent"); + let (by_const, by_other_const, by_literal, by_hex, longer) = + (at(0), at(1), at(2), at(3), at(4)); + + // Type identity is the evaluated value: all four fours are ONE type and one + // converter, however they were addressed or spelled. + for e in [by_const, by_other_const, by_literal, by_hex] { + assert_eq!(e.value, 4); + } + assert_ne!(longer.value, by_literal.value); + + // Declaration spelling is per occurrence, and distinguishes cases the value + // cannot: `4` is not `0x04`, and neither is `TAG_LEN`. + assert_eq!(tokens(&by_literal.origin.syntax), "4"); + assert_eq!(tokens(&by_hex.origin.syntax), "0x04"); + assert_eq!(tokens(&by_const.origin.syntax), "TAG_LEN"); + + // Header dependency is the named const, and distinguishes cases the + // spelling groups together and the value does not see at all. + assert_eq!( + by_const.const_id().expect("a const dependency").name, + "TAG_LEN" + ); + assert_eq!( + by_other_const.const_id().expect("a const dependency").name, + "ALSO_FOUR" + ); + assert!(by_literal.const_id().is_none()); + assert!(by_hex.const_id().is_none()); + + // The three really are orthogonal: each pair below agrees on one projection + // and differs on another. + assert!(by_const.value == by_literal.value && by_const.const_id() != by_literal.const_id()); + assert!( + by_literal.value == by_hex.value + && tokens(&by_literal.origin.syntax) != tokens(&by_hex.origin.syntax) + ); + assert!( + by_const.const_id() != by_other_const.const_id() && by_const.value == by_other_const.value + ); +} + +/// A const whose own initializer is not a literal cannot be a length — +/// `build.rs` cannot evaluate it — but it is still a perfectly good const. +#[test] +fn a_computed_const_is_indexed_but_is_not_a_length() { + let elements = parse(vec![ + syn::parse_quote!( + pub const COMPUTED: usize = 2 * 2; + ), + syn::parse_quote!( + pub struct Marker { + pub tag: [u8; COMPUTED], + } + ), + ]); + assert_eq!(as_const(&elements[0]).name, "COMPUTED"); + match as_unsupported(&elements[1]) { + ItemError::FieldType { + source: + UnsupportedType { + reason: UnsupportedTypeReason::BadArrayExtent(e), + .. + }, + .. + } => assert_eq!(e.reason, ArrayLenReason::ConstIsNotALiteral), + other => panic!("expected an extent diagnosis, got {other}"), + } +} + +// ── Item kinds ───────────────────────────────────────────────────────── + +/// A struct is a product of fields, or opaque. A tuple struct is the opaque +/// one: usable as a handle, its fields deliberately not lowered, because no +/// adapter has ever crossed them and lowering would turn types that are ignored +/// today into errors. A unit struct is the empty product, not a third shape — +/// the delimiters are spelling, and `spell` reads them off the syntax. +#[test] +fn struct_shapes() { + let named = parse_one(syn::parse_quote!( + pub struct A { + pub x: u8, + } + )); + assert_eq!(as_struct(&named).fields().len(), 1); + + let tuple = parse_one(syn::parse_quote!( + pub struct B(SomethingUnexpressible<'_, dyn Trait>); + )); + assert!(as_struct(&tuple).fields.is_none(), "opaque"); + assert!(as_struct(&tuple).fields().is_empty()); + + let unit = parse_one(syn::parse_quote!( + pub struct C; + )); + assert!(as_struct(&unit).fields.is_some(), "empty, not opaque"); + assert!(as_struct(&unit).fields().is_empty()); +} + +/// A variant's index is its declaration order and is never its discriminant: +/// one is where the source *put* it, the other is the value Rust *assigns* it. +/// The two numberings are independent, and this is the pair that proves it. +#[test] +fn tags_are_declaration_order() { + let element = parse_one(syn::parse_quote!( + pub enum E { + A = 5, + B = 9, + } + )); + let e = as_enum(&element); + assert_eq!( + e.values.iter().map(|v| v.index).collect::>(), + vec![0, 1] + ); + assert_eq!( + e.discriminant_values() + .expect("literals") + .into_iter() + .map(|(_, v)| v) + .collect::>(), + vec![5, 9] + ); +} + +/// The two enum shapes are two entities, and the classification is decided once: +/// any alternative with a field makes it a sum. +/// +/// They are not one model with a dead field each. A sum has no discriminant slot, +/// because its alternatives are identified by position — the mirror an adapter +/// builds numbers its own arms — and a fieldless enum's identity is exactly the +/// value Rust assigns. +#[test] +fn the_two_enum_shapes_are_two_entities() { + let fieldless = parse_one(syn::parse_quote!( + pub enum E { + A, + B = 7, + } + )); + let e = as_enum(&fieldless); + assert_eq!(e.values.len(), 2); + assert_eq!(e.values[1].discriminant, Some(7)); + + // Empty delimiters are still fieldless — the group question, not the syntax + // one — so this is an enum, and `spell` keeps the delimiters. + let empty_groups = parse_one(syn::parse_quote!( + pub enum E { + A, + B(), + C {}, + } + )); + assert_eq!(as_enum(&empty_groups).values.len(), 3); + + // One field anywhere makes it a sum. + let sum = parse_one(syn::parse_quote!( + pub enum E { + A, + B(u32), + C { x: u8 }, + } + )); + let v = as_variant(&sum); + assert_eq!(v.alternatives.len(), 3); + assert!(v.alternatives[0].is_empty(), "a sum may mix"); + assert_eq!(v.alternatives[1].fields.len(), 1); + assert_eq!( + v.alternatives.iter().map(|a| a.index).collect::>(), + vec![0, 1, 2] + ); + + // No alternatives at all: nothing carries a payload, so it is the degenerate + // enum rather than an empty sum. + let empty = parse_one(syn::parse_quote!( + pub enum E {} + )); + assert!(as_enum(&empty).values.is_empty()); +} + +/// A field is addressed by name or by position, and the model says which +/// without anyone reading `syn::Fields`. +#[test] +fn field_members_follow_the_addressing() { + let element = parse_one(syn::parse_quote!( + pub enum Reading { + Exact(i64, i64), + Range { low: i64 }, + } + )); + let v = as_variant(&element); + assert!(matches!( + v.alternatives[0].fields[1].member(), + syn::Member::Unnamed(i) if i.index == 1 + )); + assert!(matches!( + v.alternatives[1].fields[0].member(), + syn::Member::Named(id) if id == "low" + )); +} + +#[test] +fn consts_carry_their_type_and_value() { + let element = parse_one(tag_len_const()); + let c = as_const(&element); + assert_eq!(c.name, "TAG_LEN"); + assert!(matches!(c.ty.kind, TypeKind::Scalar(ScalarKind::Usize))); + assert_eq!(tokens(&c.origin.syntax.expr), "4"); +} + +/// An unnamed `const _` — each source's injected feature guard — is a const +/// like any other, and simply has no address, so several sources may each carry +/// one without colliding in the flat namespace. +#[test] +fn an_unnamed_const_is_a_const_without_an_address() { + let elements = parse(vec![ + syn::parse_quote!( + const _: () = (); + ), + syn::parse_quote!( + const _: () = (); + ), + ]); + assert!(elements.iter().all(|e| matches!(e, Element::Const(_)))); + assert!(elements.iter().all(|e| e.name().is_none())); +} + +/// An item kind the language does not model is diagnosed, not carried: a +/// `#[prebindgen]` crate marks what crosses the boundary and leaves the code +/// around it to the consumer. The proc-macro refuses to mark a `use` at all, so +/// only a `union` or a type alias can reach here — and both keep their name, so +/// nothing else can claim it. +#[test] +fn an_unmodelled_item_kind_is_diagnosed() { + for (item, expected) in [ + ( + syn::parse_quote!( + pub union U { + a: u8, + } + ), + "a union", + ), + ( + syn::parse_quote!( + pub type Alias = u32; + ), + "a type alias", + ), + ] { + let element = parse_one(item); + assert!(element.name().is_some(), "keeps its address"); + assert!(matches!( + as_unsupported(&element), + ItemError::UnsupportedItemKind { kind } if *kind == expected + )); + } +} + +// ── Functions ────────────────────────────────────────────────────────── + +#[test] +fn function_signatures() { + let element = parse_one(syn::parse_quote!( + pub fn put(key: &KeyExpr, payload: Vec) -> Result<(), Error> { + unimplemented!() + } + )); + let f = as_fn(&element); + assert_eq!(f.name, "put"); + assert_eq!( + f.params + .iter() + .map(|p| p.name.to_string()) + .collect::>(), + vec!["key", "payload"] + ); + assert!(matches!(f.ret.kind, TypeKind::Fallible { .. })); +} + +/// An elided return and a written `-> ()` are the same function. Nothing in the +/// pipeline distinguishes them — every consumer normalizes one to the other on +/// the spot — so the model does it once instead. +#[test] +fn an_elided_return_is_the_unit() { + for sig in [ + quote::quote!( + pub fn f() {} + ), + quote::quote!( + pub fn f() -> () {} + ), + ] { + let element = parse_one(syn::parse_quote!(#sig)); + assert!(matches!(as_fn(&element).ret.kind, TypeKind::Unit)); + } +} + +/// Function shapes `Function` has no slot for, and would therefore drop in +/// silence. +/// +/// `async` is the one that bites: the future would be dropped and the export +/// would be a function whose body never runs. +#[test] +fn function_shapes_outside_the_language() { + let element = parse_one(syn::parse_quote!( + pub async fn ping() {} + )); + assert!(matches!( + as_unsupported(&element), + ItemError::UnsupportedAsync + )); + // Named, so nothing else can claim the address while it sits inert. + assert_eq!(element.name().expect("named"), "ping"); + + let element = parse_one(syn::parse_quote!( + pub unsafe extern "C" fn log(fmt: u8, ...) {} + )); + assert!(matches!( + as_unsupported(&element), + ItemError::UnsupportedVariadic + )); +} + +/// A generic binder is refused on every item kind. The elements have no binder, +/// so a `T` would lower as an ordinary nominal reference into the flat namespace +/// — indistinguishable from a real item named `T`. +#[test] +fn a_generic_parameter_is_outside_the_language() { + let cases: Vec<(syn::Item, &str, &str)> = vec![ + ( + syn::parse_quote!( + pub struct Wrapper { + pub value: T, + } + ), + "T", + "a type parameter", + ), + ( + syn::parse_quote!( + pub fn first(items: Vec) -> T { + unimplemented!() + } + ), + "T", + "a type parameter", + ), + ( + syn::parse_quote!( + pub enum Either { + Left(L), + Right(R), + } + ), + "L", + "a type parameter", + ), + ( + // Unused, and still a binder. + syn::parse_quote!( + pub struct Padded { + pub value: u8, + } + ), + "N", + "a const generic parameter", + ), + ]; + for (item, expected_param, expected_kind) in cases { + let element = parse_one(item); + let ItemError::UnsupportedGenericParam { param, kind } = as_unsupported(&element) else { + panic!( + "expected a generic-parameter diagnosis, got {}", + describe(&element) + ); + }; + assert_eq!(param, expected_param); + assert_eq!(*kind, expected_kind); + } +} + +/// A **lifetime** binder is not a generic parameter for this purpose: lifetimes +/// say nothing a destination language can act on, and the spelling that needs +/// them is already in the syntax — the same call made for a lifetime argument. +#[test] +fn a_lifetime_binder_is_accepted() { + let element = parse_one(syn::parse_quote!( + pub struct Borrowed<'a> { + pub key: &'a str, + } + )); + let s = as_struct(&element); + assert_eq!(s.fields().len(), 1); + assert_eq!(tokens(&s.fields()[0].ty.origin.syntax), "& 'a str"); +} + +/// `impl Trait` in argument position is an anonymous type parameter in Rust, but +/// `syn` does not desugar it into the binder list — so the callback form, which +/// every callback-taking source function uses, is untouched by the generic +/// refusal. This is the test that says so. +#[test] +fn a_callback_parameter_is_not_a_generic_binder() { + let element = parse_one(syn::parse_quote!( + pub fn for_each(f: impl Fn(u64) + Send + Sync + 'static) {} + )); + let func = as_fn(&element); + assert!(matches!(func.params[0].ty.kind, TypeKind::Callback { .. })); +} + +#[test] +fn a_receiver_is_not_a_free_function() { + let element = parse_one(syn::parse_quote!( + pub fn get(self) -> u8 { + unimplemented!() + } + )); + assert!(matches!( + as_unsupported(&element), + ItemError::UnsupportedReceiver + )); +} + +#[test] +fn a_parameter_must_be_bound_to_one_name() { + let element = parse_one(syn::parse_quote!( + pub fn f((a, b): (u8, u8)) {} + )); + assert!(matches!( + as_unsupported(&element), + ItemError::UnsupportedParamPattern { .. } + )); +} + +/// The diagnosis names the component, not just the item — the whole point of +/// lowering each part separately. +#[test] +fn a_diagnosis_names_the_component() { + let element = parse_one(syn::parse_quote!( + pub fn f(ok: u8, bad: (u8, u8)) {} + )); + match as_unsupported(&element) { + ItemError::ParamType { param, source } => { + assert_eq!(param, "bad"); + assert_eq!(source.reason, UnsupportedTypeReason::UnsupportedTuple); + } + other => panic!("expected a parameter diagnosis, got {other}"), + } + + let element = parse_one(syn::parse_quote!( + pub fn f() -> (u8, u8) { + unimplemented!() + } + )); + assert!(matches!( + as_unsupported(&element), + ItemError::ReturnType { .. } + )); + + let element = parse_one(syn::parse_quote!( + pub enum E { + V { bad: (u8, u8) }, + } + )); + match as_unsupported(&element) { + ItemError::VariantFieldType { variant, field, .. } => { + assert_eq!(variant, "V"); + assert_eq!(field, "bad"); + } + other => panic!("expected a variant diagnosis, got {other}"), + } +} + +/// An item the language cannot express is inert, not fatal: a source crate may +/// mark items no binding uses, and those have never had to be expressible. It +/// keeps its name (so nothing else can claim it) and its syntax. +#[test] +fn an_unsupported_item_is_indexed_not_refused() { + let elements = parse(vec![ + syn::parse_quote!( + pub fn unusable(pair: (u8, u8)) {} + ), + syn::parse_quote!( + pub fn usable(x: u8) {} + ), + ]); + assert_eq!(elements[0].name().expect("named"), "unusable"); + assert!(matches!(elements[0], Element::Unsupported(_))); + assert!(matches!(elements[1], Element::Function(_))); +} + +// ── The flat namespace ───────────────────────────────────────────────── + +/// The feeders accumulate, and the whole-stream rules span them. +/// +/// This is why inputs are collected before anything is classified rather than +/// parsed one at a time: a duplicate name is only visible with every input in +/// hand, and so is a const that an array length in another input reaches for. +#[test] +fn the_feeders_accumulate_and_whole_stream_rules_span_them() { + let marker: syn::Item = syn::parse_quote!( + pub struct Marker { + pub tag: [u8; TAG_LEN], + } + ); + + // A length in the first feeder naming a const from the second. + let elements = Language::new() + .items(vec![(marker.clone(), loc())]) + .items(vec![(tag_len_const(), loc())]) + .parse() + .expect("the const is found across feeders"); + assert_eq!(elements.len(), 2); + assert_eq!( + as_struct(&elements[0]).fields()[0] + .ty + .array_extent() + .expect("an extent") + .value, + 4 + ); + + // And a name colliding across feeders is still the one hard failure. + let err = Language::new() + .items(vec![(marker.clone(), loc())]) + .items(vec![(marker, loc())]) + .parse() + .expect_err("a duplicate across feeders is still a duplicate"); + let ParseError::DuplicateName(d) = err; + assert_eq!(d.name, "Marker"); +} + +/// Two marked items with one name are ambiguous however the crates are +/// arranged, so this is the one thing a parse refuses outright. +#[test] +fn duplicate_names_are_a_hard_error() { + let err = try_parse(vec![ + syn::parse_quote!( + pub struct Sample { + pub x: u8, + } + ), + syn::parse_quote!( + pub fn Sample() {} + ), + ]) + .expect_err("a duplicate"); + let ParseError::DuplicateName(d) = err; + assert_eq!(d.name, "Sample"); +} + +/// Even an item the language could not express holds its name against the +/// namespace: it is still a marked item, and a second one would still be +/// ambiguous. +#[test] +fn an_unsupported_item_still_holds_its_name() { + assert!(try_parse(vec![ + syn::parse_quote!( + pub fn Thing(pair: (u8, u8)) {} + ), + syn::parse_quote!( + pub struct Thing { + pub x: u8, + } + ), + ]) + .is_err()); +} diff --git a/prebindgen/src/api/core/language/tests/mod.rs b/prebindgen/src/api/core/language/tests/mod.rs new file mode 100644 index 00000000..0967b404 --- /dev/null +++ b/prebindgen/src/api/core/language/tests/mod.rs @@ -0,0 +1,118 @@ +//! The language's own test suite, in two halves: +//! +//! * [`roundtrip`] — every element's syntax slices re-emit what the source +//! wrote. This is the property the whole design rests on: if a slice were +//! rebuilt rather than kept, the classification would have to be lossless and +//! would grow back into a second `syn`. +//! * [`acceptance`] — source spelling → element, or a diagnosis naming the item +//! and the component. The matrix issue #211 asks for. + +use std::rc::Rc; + +use quote::ToTokens; + +use super::*; + +mod acceptance; +mod roundtrip; + +/// Parse one item, stamped with an origin crate so array extents can name +/// `#[prebindgen]` consts from "their own" crate. +fn parse_one(item: syn::Item) -> Element { + let mut out = parse(vec![item]); + assert_eq!(out.len(), 1); + out.remove(0) +} + +/// Parse a whole stream, all items stamped with the same origin crate. +fn parse(items: Vec) -> Vec { + try_parse(items).expect("stream parses") +} + +fn try_parse(items: Vec) -> Result, ParseError> { + Language::new() + .items(items.into_iter().map(|i| (i, loc()))) + .parse() +} + +fn loc() -> SourceLocation { + SourceLocation { + file: "src/lib.rs".to_string(), + line: 1, + column: 1, + crate_name: Some("myflat".to_string()), + } +} + +/// `pub const TAG_LEN: usize = 4;` — the const an array extent may name. +fn tag_len_const() -> syn::Item { + syn::parse_quote!( + pub const TAG_LEN: usize = 4; + ) +} + +/// Whitespace-insensitive token comparison, so a test states what the tokens +/// are rather than how they were spaced. +fn tokens(t: &impl ToTokens) -> String { + t.to_token_stream().to_string() +} + +/// The element as a [`Function`], or a panic naming what it actually is. +fn as_fn(e: &Element) -> &Function { + match e { + Element::Function(f) => f, + other => panic!("expected a function, got {}", describe(other)), + } +} + +fn as_struct(e: &Element) -> &Struct { + match e { + Element::Struct(s) => s, + other => panic!("expected a struct, got {}", describe(other)), + } +} + +fn as_enum(e: &Element) -> &Enum { + match e { + Element::Enum(en) => en, + other => panic!("expected a fieldless enum, got {}", describe(other)), + } +} + +fn as_variant(e: &Element) -> &Variant { + match e { + Element::Variant(v) => v, + other => panic!("expected a sum, got {}", describe(other)), + } +} + +fn as_const(e: &Element) -> &Const { + match e { + Element::Const(c) => c, + other => panic!("expected a const, got {}", describe(other)), + } +} + +/// The diagnosis of an [`Element::Unsupported`], or a panic naming what the +/// element actually is — so a test that expected a refusal and got an +/// acceptance says so. +fn as_unsupported(e: &Element) -> &ItemError { + match e { + Element::Unsupported(u) => &u.error, + other => panic!("expected an unsupported item, got {}", describe(other)), + } +} + +fn describe(e: &Element) -> String { + match e { + Element::Function(f) => format!("function `{}`", f.name), + Element::Struct(s) => format!("struct `{}`", s.name), + Element::Variant(v) => format!("sum `{}`", v.name), + Element::Enum(en) => format!("enum `{}`", en.name), + Element::Const(c) => format!("const `{}`", c.name), + Element::Unsupported(u) => match &u.name { + Some(name) => format!("unsupported `{name}` ({})", u.error), + None => format!("unsupported ({})", u.error), + }, + } +} diff --git a/prebindgen/src/api/core/language/tests/roundtrip.rs b/prebindgen/src/api/core/language/tests/roundtrip.rs new file mode 100644 index 00000000..8808137b --- /dev/null +++ b/prebindgen/src/api/core/language/tests/roundtrip.rs @@ -0,0 +1,466 @@ +//! The round-trip property: an element's syntax slices are the source's own +//! tokens, sliced — never a reconstruction. +//! +//! Every test here would also pass against a model that rebuilt syntax from its +//! classification *for the easy cases*. The ones that matter are the cases where +//! a reconstruction loses: an empty tuple variant, a hex discriminant, a +//! lifetime, an aliased path, a doc comment. Those are the reason the slices +//! ride along at all. + +use super::*; + +/// Each parameter and the return type re-emit exactly what was written — +/// including a lifetime, which the classification does not model. +#[test] +fn function_parts_are_the_source_tokens() { + let f = syn::parse_quote!( + pub fn publish( + key: &'a KeyExpr, + payload: Vec, + count: Option, + ) -> Result<(), Error> { + unimplemented!() + } + ); + let element = parse_one(syn::Item::Fn(f)); + let func = as_fn(&element); + + assert_eq!( + func.params + .iter() + .map(|p| tokens(&p.origin.syntax)) + .collect::>(), + vec![ + "key : & 'a KeyExpr", + "payload : Vec < u8 >", + "count : Option < i32 >", + ] + ); + // The lifetime is nowhere in the classification, and still survives. + assert_eq!(tokens(&func.params[0].ty.origin.syntax), "& 'a KeyExpr"); + assert!(matches!(func.params[0].ty.kind, TypeKind::Ref { .. })); + + assert_eq!(tokens(&func.ret.origin.syntax), "Result < () , Error >"); +} + +/// A defaulted return and a written `-> ()` are the same function, and both +/// spell as `()`. The one thing that separates them — whether the source typed +/// an arrow — is in `Function::syntax.sig.output`, where the only consumer that +/// could care (one re-emitting the signature verbatim) already looks. +#[test] +fn a_defaulted_return_spells_as_the_unit() { + for item in [ + syn::parse_quote!( + pub fn a() {} + ), + syn::parse_quote!( + pub fn b() -> () {} + ), + ] { + let element = parse_one(item); + let ret = &as_fn(&element).ret; + assert!(matches!(ret.kind, TypeKind::Unit)); + assert_eq!(tokens(&ret.origin.syntax), "()"); + } + + let defaulted = parse_one(syn::parse_quote!( + pub fn a() {} + )); + assert!(matches!( + as_fn(&defaulted).origin.syntax.sig.output, + syn::ReturnType::Default + )); +} + +/// A field's slice keeps its attributes and visibility, so an emitter can +/// re-state the field rather than rebuild it from name and type. +#[test] +fn struct_field_slices_keep_attributes() { + let element = parse_one(syn::parse_quote!( + pub struct Sample { + /// The key it was published on. + pub key: String, + #[allow(dead_code)] + pub(crate) seq: u64, + } + )); + let fields = as_struct(&element).fields(); + assert_eq!(fields.len(), 2); + assert!(tokens(&fields[0].origin.syntax).contains("The key it was published on.")); + assert_eq!( + tokens(&fields[1].origin.syntax), + "# [allow (dead_code)] pub (crate) seq : u64" + ); +} + +/// One captured record is one item, so an item and every node lowered out of it +/// point at the **same** location — not equal copies, the same allocation. +/// +/// That is the model, not an optimisation: a field has no location of its own, +/// and the honest answer to "where is this field" is "wherever its item is". +#[test] +fn an_item_and_its_components_share_one_location() { + let element = parse_one(syn::parse_quote!( + pub struct Sample { + pub key: String, + pub tags: Vec, + } + )); + let s = as_struct(&element); + let item = &s.origin.location; + for field in s.fields() { + assert!(Rc::ptr_eq(item, &field.origin.location), "field"); + assert!(Rc::ptr_eq(item, &field.ty.origin.location), "field type"); + } + // And down through a nested type's arguments. + let TypeKind::Sequence(elem) = &s.fields()[1].ty.kind else { + panic!("a sequence"); + }; + assert!(Rc::ptr_eq(item, &elem.origin.location), "element type"); + + // Alternatives, their fields, parameters and extents alike. + let element = parse_one(syn::parse_quote!( + pub enum E { + A { x: [u8; 4] }, + } + )); + let v = as_variant(&element); + let item = &v.origin.location; + let a = &v.alternatives[0]; + assert!(Rc::ptr_eq(item, &a.origin.location), "alternative"); + let f = &a.fields[0]; + assert!(Rc::ptr_eq(item, &f.origin.location), "alternative field"); + let extent = f.ty.array_extent().expect("an extent"); + assert!(Rc::ptr_eq(item, &extent.origin.location), "extent"); + assert_eq!(tokens(&extent.origin.syntax), "4"); + + let element = parse_one(syn::parse_quote!( + pub fn f(a: u8) {} + )); + let func = as_fn(&element); + let item = &func.origin.location; + assert!(Rc::ptr_eq(item, &func.params[0].origin.location), "param"); + assert!(Rc::ptr_eq(item, &func.ret.origin.location), "elided return"); +} + +/// A component's diagnosis carries the item's location, which is the only one +/// there is — the record is per-item, so nothing finer was ever captured. +#[test] +fn a_component_diagnosis_carries_the_items_location() { + let element = parse_one(syn::parse_quote!( + pub struct Sample { + pub bad: (u8, u8), + } + )); + let Element::Unsupported(u) = &element else { + panic!("a tuple field is outside the language"); + }; + assert!(matches!(*u.error, ItemError::FieldType { .. })); + // The item's own location, reachable the same way as for any other element. + assert!(std::ptr::eq(element.location(), &*u.origin.location)); +} + +/// The case that motivated the design. `B()` and `C {}` carry no payload and +/// are still not unit variants: Rust demands the delimiters wherever the variant +/// is named. The classification calls all three unit *groups*; `spell` keeps +/// them apart, off the syntax. +#[test] +fn empty_delimiters_survive_and_spell() { + let element = parse_one(syn::parse_quote!( + pub enum E { + A, + B(), + C {}, + D(u32), + } + )); + let v = as_variant(&element); + + // All four groups, and which of them are empty. + assert_eq!( + v.alternatives + .iter() + .map(|a| a.is_empty()) + .collect::>(), + vec![true, true, true, false] + ); + + let spell = |a: &Alternative| { + let name = &a.name; + a.spell(quote::quote!(E::#name), &[]).to_string() + }; + assert_eq!(spell(&v.alternatives[0]), "E :: A"); + assert_eq!(spell(&v.alternatives[1]), "E :: B ()"); + assert_eq!(spell(&v.alternatives[2]), "E :: C { }"); + + // The same in a FIELDLESS enum, where every group is empty and the whole + // item is the other shape: the delimiters still have to survive. + let element = parse_one(syn::parse_quote!( + pub enum F { + A, + B(), + C {}, + } + )); + let e = as_enum(&element); + let spell = |v: &EnumValue| { + let name = &v.name; + v.spell(quote::quote!(F::#name), &[]).to_string() + }; + assert_eq!(spell(&e.values[0]), "F :: A"); + assert_eq!(spell(&e.values[1]), "F :: B ()"); + assert_eq!(spell(&e.values[2]), "F :: C { }"); + + // And with payloads, in both addressing modes. + let element = parse_one(syn::parse_quote!( + pub enum Reading { + Exact(i64), + Range { low: i64, high: i64 }, + } + )); + let v = as_variant(&element); + let bind = |a: &Alternative| { + let parts: Vec<_> = a + .fields + .iter() + .map(|f| f.bind("e::format_ident!("__f{}", f.index))) + .collect(); + let name = &a.name; + a.spell(quote::quote!(Reading::#name), &parts).to_string() + }; + assert_eq!(bind(&v.alternatives[0]), "Reading :: Exact (__f0)"); + assert_eq!( + bind(&v.alternatives[1]), + "Reading :: Range { low : __f0 , high : __f1 }" + ); +} + +/// The same property for a struct, which is why it needs no modelled shape +/// either. `struct S;` and `struct S {}` hold zero fields alike and are still +/// spelled differently wherever Rust names them — one `spell` off the syntax +/// covers a struct and a variant, in either direction. +#[test] +fn struct_delimiters_survive_and_spell() { + let spell = |item: syn::Item, parts: &[proc_macro2::TokenStream]| { + let element = parse_one(item); + let s = as_struct(&element); + let name = &s.name; + ( + s.fields.is_some(), + s.fields().len(), + s.spell(quote::quote!(#name), parts).to_string(), + ) + }; + + assert_eq!( + spell( + syn::parse_quote!( + pub struct A; + ), + &[] + ), + (true, 0, "A".to_string()) + ); + assert_eq!( + spell( + syn::parse_quote!( + pub struct B {} + ), + &[] + ), + (true, 0, "B { }".to_string()) + ); + assert_eq!( + spell( + syn::parse_quote!( + pub struct C { + pub x: u8, + } + ), + &[quote::quote!(x: __f0)] + ), + (true, 1, "C { x : __f0 }".to_string()) + ); + // Opaque: no modelled fields, and still spellable. + assert_eq!( + spell( + syn::parse_quote!( + pub struct D(Whatever<'_, dyn Trait>); + ), + &[quote::quote!(__f0)] + ), + (false, 0, "D (__f0)".to_string()) + ); +} + +/// A discriminant is two facts with two homes: the number is modelled, the +/// spelling stays in the variant's slice. `0x07` must reach a C header as +/// `0x07`, and no reconstruction from `7` can do that. +#[test] +fn discriminant_number_and_spelling_both_survive() { + let element = parse_one(syn::parse_quote!( + pub enum Priority { + Low = 0x07, + High, + } + )); + let e = as_enum(&element); + + assert_eq!( + e.discriminant_values().expect("literal discriminants"), + vec![(&e.values[0].name, 7), (&e.values[1].name, 8)] + ); + let (_, expr) = e.values[0] + .origin + .syntax + .discriminant + .as_ref() + .expect("an explicit discriminant"); + assert_eq!(tokens(expr), "0x07"); + assert!(e.values[1].origin.syntax.discriminant.is_none()); +} + +/// A discriminant the frontend cannot evaluate breaks the *numeric* chain and +/// nothing else: the spelling is still there, so a consumer that re-emits it +/// carries on while one that needs the number is told which variant to blame. +#[test] +fn an_unevaluable_discriminant_keeps_its_spelling() { + let element = parse_one(syn::parse_quote!( + pub enum E { + A = OTHER, + B, + } + )); + let e = as_enum(&element); + assert!(e.values.iter().all(|v| v.discriminant.is_none())); + assert_eq!(e.discriminant_values().expect_err("no numbers"), "A"); + let (_, expr) = e.values[0] + .origin + .syntax + .discriminant + .as_ref() + .expect("explicit"); + assert_eq!(tokens(expr), "OTHER"); +} + +/// A discriminant at the top of the range is valid Rust, so running out of +/// `i64` ends the numeric chain the way an unevaluable spelling does — it does +/// not panic during ingest, which would take down every consumer including the +/// ones that only re-emit. +#[test] +fn a_discriminant_at_the_top_of_the_range_does_not_overflow() { + let element = parse_one(syn::parse_quote!( + #[repr(u64)] + pub enum E { + A = 9223372036854775807, + B, + } + )); + let e = as_enum(&element); + assert_eq!(e.values[0].discriminant, Some(i64::MAX)); + assert_eq!(e.values[1].discriminant, None); + + // The last variant needs no successor, so it must not fail either. + let element = parse_one(syn::parse_quote!( + pub enum E { + A = 9223372036854775807, + } + )); + assert_eq!( + as_enum(&element).discriminant_values().expect("a number")[0].1, + i64::MAX + ); +} + +/// The bottom of the range too. `i64::MIN` is a valid Rust discriminant, and its +/// magnitude is one past `i64::MAX` — so the sign has to be applied before the +/// range check, not after. +#[test] +fn a_discriminant_at_the_bottom_of_the_range_evaluates() { + let element = parse_one(syn::parse_quote!( + #[repr(i64)] + pub enum E { + A = -9223372036854775808, + B, + } + )); + let e = as_enum(&element); + assert_eq!(e.values[0].discriminant, Some(i64::MIN)); + assert_eq!(e.values[1].discriminant, Some(i64::MIN + 1)); + // And the spelling is still the source's, as for any other discriminant. + let (_, expr) = e.values[0] + .origin + .syntax + .discriminant + .as_ref() + .expect("explicit"); + assert_eq!(tokens(expr), "- 9223372036854775808"); + + // One step further out is not a number, and ends the chain rather than + // panicking — the same contract as an unevaluable spelling. + let element = parse_one(syn::parse_quote!( + #[repr(i128)] + pub enum E { + A = -9223372036854775809, + B, + } + )); + let e = as_enum(&element); + assert_eq!(e.values[0].discriminant, None); + assert_eq!(e.values[1].discriminant, None); + assert_eq!(e.discriminant_values().expect_err("no numbers"), "A"); +} + +/// An array's extent is modelled as a number AND the const it named, while the +/// type's slice keeps the symbolic spelling — the three-way split that lets one +/// consumer emit `[u8; 4]` and another `uint8_t tag[TAG_LEN]`. +#[test] +fn array_extent_carries_number_const_and_spelling() { + let elements = parse(vec![ + tag_len_const(), + syn::parse_quote!( + pub struct Marker { + pub tag: [u8; TAG_LEN], + pub pad: [u8; 2], + } + ), + ]); + let fields = as_struct(&elements[1]).fields(); + + let named = fields[0].ty.array_extent().expect("an extent"); + assert_eq!(named.value, 4); + assert_eq!(named.const_id().expect("a const").name, "TAG_LEN"); + assert_eq!(tokens(&fields[0].ty.origin.syntax), "[u8 ; TAG_LEN]"); + + let literal = fields[1].ty.array_extent().expect("an extent"); + assert_eq!(literal.value, 2); + assert!(literal.const_id().is_none()); +} + +/// The whole item is kept too, so anything the element model does not describe +/// — attributes, `cfg`, the function body — is still emittable. +#[test] +fn the_whole_item_survives() { + let source: syn::ItemFn = syn::parse_quote!( + /// Adds two numbers. + #[inline] + pub fn add(a: i32, b: i32) -> i32 { + a + b + } + ); + let element = parse_one(syn::Item::Fn(source.clone())); + assert_eq!(tokens(&as_fn(&element).origin.syntax), tokens(&source)); + assert_eq!(tokens(&element.syntax()), tokens(&syn::Item::Fn(source))); +} + +/// An item the language cannot express still keeps its tokens, so a diagnosis +/// can quote the source and nothing is lost by refusing it. +#[test] +fn an_unsupported_item_keeps_its_tokens() { + let source: syn::Item = syn::parse_quote!( + pub type Alias = u32; + ); + let element = parse_one(source.clone()); + assert!(matches!(element, Element::Unsupported(_))); + assert_eq!(tokens(&element.syntax()), tokens(&source)); +} diff --git a/prebindgen/src/api/core/language/ty.rs b/prebindgen/src/api/core/language/ty.rs new file mode 100644 index 00000000..e7c1c61f --- /dev/null +++ b/prebindgen/src/api/core/language/ty.rs @@ -0,0 +1,521 @@ +//! Types: a closed classification paired with the syntax it was read from. +//! +//! [`Type`] is the pattern the whole element model follows — `kind` says what +//! the type *means*, `syntax` is the tokens the source wrote. Consumers +//! **classify off `kind` and spell off `syntax`**; see the [module docs](super) +//! for why that split is the point. +//! +//! [`TypeKind`] is total over the accepted grammar: a form with no variant here +//! is a form the language does not accept, so acceptance is a consequence of +//! lowering rather than a second list that can drift from it. Same contract, and +//! for the same reason, as [`lower_array_len`]. + +use std::{fmt, rc::Rc}; + +use quote::ToTokens; + +use super::{ + array_len::{lower_array_len, ArrayExtent, ConstIndex, UnsupportedArrayLen}, + origin::Origin, +}; +use crate::SourceLocation; + +/// A type as the language decided it, plus the exact syntax it came from. +/// +/// The [`Origin::syntax`] slice is what removes the pressure to make `kind` +/// lossless: a lifetime, an elided argument, a `Box` that changes nothing +/// outside Rust all survive there at zero modelling cost, so `kind` can stay +/// language-neutral and small. +#[derive(Clone, Debug)] +pub struct Type { + /// What the type means — the closed, destination-neutral classification. + pub kind: TypeKind, + /// The type as generated Rust must spell it — the source's own tokens, + /// normalized to the flat namespace the generated crate can name (see + /// [`Language::parse`](super::Language::parse)) — plus the source they came + /// from. + /// + /// The syntax can say strictly more than `kind` does — `Box` is a + /// `Str` here — which is the point: what Rust needs and no destination + /// language can see lives in the tokens, not in the classification. + pub origin: Origin, +} + +impl Type { + /// The extent of this type when it is an array, else `None`. + pub fn array_extent(&self) -> Option<&ArrayExtent> { + match &self.kind { + TypeKind::Array { extent, .. } => Some(extent), + _ => None, + } + } + + /// Every extent reachable from this type, outermost first — so a nested + /// `[[u8; A]; B]` yields `B` then `A`. + /// + /// Used to find which consts an emitted C type may name, and therefore which + /// must reach the header as a `#define`. + pub fn extents(&self) -> Vec<&ArrayExtent> { + let mut out = Vec::new(); + self.collect_extents(&mut out); + out + } + + fn collect_extents<'a>(&'a self, out: &mut Vec<&'a ArrayExtent>) { + match &self.kind { + TypeKind::Array { elem, extent } => { + out.push(extent); + elem.collect_extents(out); + } + TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => { + t.collect_extents(out) + } + TypeKind::Fallible { ok, err } => { + ok.collect_extents(out); + err.collect_extents(out); + } + TypeKind::Callback { args } | TypeKind::Named { args, .. } => { + args.iter().for_each(|t| t.collect_extents(out)) + } + TypeKind::Scalar(_) | TypeKind::Str | TypeKind::Unit => {} + } + } +} + +/// What a [`Type`] means. The variants are the accepted type grammar. +/// +/// One Rust spelling per concept is **not** the rule here — several are. A +/// concept earns a variant when a destination language would act on it; a +/// spelling that changes nothing outside Rust folds into the concept it carries +/// and survives in [`Type::syntax`]: +/// +/// | Spelling | Kind | Why | +/// |---|---|---| +/// | `String`, `str` | [`Str`](TypeKind::Str) | one concept, two Rust types | +/// | `Vec`, `[T]` | [`Sequence`](TypeKind::Sequence) | a run of `T`; owned vs borrowed is the [`Ref`](TypeKind::Ref) layer's fact, not a second variant | +/// | `Box` | *whatever `T` is* | an owned `T` either way; nothing outside Rust can tell | +#[derive(Clone, Debug)] +pub enum TypeKind { + /// A primitive with a fixed C/JVM counterpart. + Scalar(ScalarKind), + /// A UTF-8 string — `String` owned, `str` behind a [`Ref`](TypeKind::Ref). + /// + /// Both spellings are one concept: `&str` and `&String` are each a borrowed + /// string and classify identically, which is what every adapter already + /// does by hand. + Str, + /// `Option`. + Optional(Box), + /// A run of `T` — `Vec` owned, `[T]` behind a [`Ref`](TypeKind::Ref). + /// + /// One variant, because ownership is already the [`Ref`](TypeKind::Ref) + /// layer's fact: `&[T]` is `Ref(Sequence)`, `Vec` is `Sequence`. A + /// second variant would encode ownership twice and let the two copies + /// disagree. `[T; N]` is *not* this — a fixed extent is a different + /// concept, see [`Array`](TypeKind::Array). + Sequence(Box), + /// `Result`. + Fallible { ok: Box, err: Box }, + /// Any other named type: a `#[prebindgen]` struct or enum, or a foreign + /// path. + /// + /// `id` is the type's **identity** — a name, not syntax, so nothing outside + /// this module has to take a path apart to learn what a type is. The last + /// segment's generic arguments live in `args`, and only the *type* + /// arguments: a lifetime argument says nothing a destination language can + /// act on. The full spelling is in [`Type::syntax`] for whoever re-emits it. + Named { id: TypeId, args: Vec }, + /// `[T; N]` — a run of `T` whose length is known at compile time. + /// + /// Deliberately not a [`Sequence`](TypeKind::Sequence) with an optional + /// extent: a fixed array crosses by value as a primitive array, a `Vec` + /// crosses as a heap collection, and every adapter branches between the two + /// at every site. + Array { + elem: Box, + extent: ArrayExtent, + }, + /// A borrow — `&T` / `&'a T` / `&mut T`. The lifetime is spelling, so it + /// lives in [`Type::syntax`] rather than here. + /// + /// This is the ownership layer for every concept underneath it: `&str` is + /// `Ref(Str)`, `&[T]` is `Ref(Sequence)`. A shared-ownership handle + /// (`Arc`, `Rc`) belongs here too when the language accepts one. + Ref { mutable: bool, inner: Box }, + /// `impl Fn(A, B, …) + Send + Sync + 'static` — the callback form. + Callback { args: Vec }, + /// `()`. + Unit, +} + +/// A nominal type's identity: a name, and nothing else. +/// +/// `#[prebindgen]` names live in one flat namespace — a duplicate is a +/// [`ParseError`](super::ParseError) — so the name is the whole address. It +/// deliberately carries **no crate**: a reference carries a name, and the +/// declaring crate belongs to the declaration, reachable by looking the name up +/// among the elements. Putting the use site's crate here would make the same +/// type compare unequal to itself across two source crates. +/// +/// A name rather than a `syn::Path` on purpose: an identity kept as syntax +/// makes every consumer take a path apart to learn what a type is, which is the +/// re-classification issue #211 exists to stop — and one the boundary ledger +/// would not even see, since it watches `syn::Type` and `syn::Expr`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TypeId { + /// The path as written, minus any generic arguments — `Foo`, + /// `foreign::Option`. Normalized, so a reducible std or source-module path + /// has already collapsed to its final segment. + pub name: String, +} + +/// The primitives the source language accepts. Mirrors the set every adapter +/// already treats as directly representable. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ScalarKind { + Bool, + I8, + I16, + I32, + I64, + Isize, + U8, + U16, + U32, + U64, + Usize, + F32, + F64, +} + +impl ScalarKind { + fn from_name(name: &str) -> Option { + Some(match name { + "bool" => Self::Bool, + "i8" => Self::I8, + "i16" => Self::I16, + "i32" => Self::I32, + "i64" => Self::I64, + "isize" => Self::Isize, + "u8" => Self::U8, + "u16" => Self::U16, + "u32" => Self::U32, + "u64" => Self::U64, + "usize" => Self::Usize, + "f32" => Self::F32, + "f64" => Self::F64, + _ => return None, + }) + } + + /// The Rust spelling — the identity this was lowered from. + pub fn as_str(self) -> &'static str { + match self { + Self::Bool => "bool", + Self::I8 => "i8", + Self::I16 => "i16", + Self::I32 => "i32", + Self::I64 => "i64", + Self::Isize => "isize", + Self::U8 => "u8", + Self::U16 => "u16", + Self::U32 => "u32", + Self::U64 => "u64", + Self::Usize => "usize", + Self::F32 => "f32", + Self::F64 => "f64", + } + } +} + +/// A type the prebindgen source language does not accept. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct UnsupportedType { + /// The offending type as written. + pub offending: String, + pub reason: UnsupportedTypeReason, +} + +/// Why [`lower_type`] refused a type. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum UnsupportedTypeReason { + /// A syntactic form with no place in the language: a raw pointer, a bare + /// trait object, a closure type, a macro, `Self`, a never type, an inferred + /// type. + /// + /// A `#[prebindgen]` crate is idiomatic Rust — the adapter owns the lowering + /// to pointers — so `*const T` / `*mut T` are refused here rather than + /// modelled. No adapter has a selection arm for one, so accepting them would + /// only defer the failure to a late "unresolved type". + UnsupportedForm, + /// `impl Trait` that is not the accepted callback form — anything but + /// `impl Fn(..) + Send + Sync + 'static` returning `()`. + DisallowedImplTrait, + /// A generic that takes a fixed arity and did not get it — `Option` with no + /// argument, `Result` with one. + WrongGenericArity { expected: usize }, + /// A non-empty tuple. Only `()` is in the language: no adapter has ever + /// lowered a tuple, so accepting one would defer the failure to a late + /// "unresolved type" instead of naming it here. + UnsupportedTuple, + /// A path with a qualified self — `::Assoc`. + /// + /// The frontend never captures `impl` blocks, so it cannot know what an + /// associated type resolves to; carrying the spelling would only move the + /// failure downstream. + AssociatedType, + /// A generic argument that is neither a type nor a lifetime — a const + /// generic, an associated-type binding. + UnsupportedGenericArgument, + /// The array's extent — see [`ArrayLenReason`](super::ArrayLenReason). + BadArrayExtent(Box), +} + +impl fmt::Display for UnsupportedType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match &self.reason { + UnsupportedTypeReason::BadArrayExtent(e) => return write!(f, "{e}"), + UnsupportedTypeReason::UnsupportedForm => write!( + f, + "type `{}` is a form the prebindgen source language does not accept", + self.offending + ), + UnsupportedTypeReason::DisallowedImplTrait => write!( + f, + "type `{}` is not an accepted callback — the only `impl Trait` in the language is \ + `impl Fn(..) + Send + Sync + 'static` returning `()`", + self.offending + ), + UnsupportedTypeReason::WrongGenericArity { expected } => write!( + f, + "type `{}` needs exactly {expected} type argument(s)", + self.offending + ), + UnsupportedTypeReason::UnsupportedTuple => write!( + f, + "type `{}` is a tuple; only the unit `()` is supported — return the \ + components separately, or wrap them in a `#[prebindgen]` struct", + self.offending + ), + UnsupportedTypeReason::AssociatedType => write!( + f, + "type `{}` is an associated type; `#[prebindgen]` never captures `impl` \ + blocks, so its resolution is unknowable here — name the concrete type", + self.offending + ), + UnsupportedTypeReason::UnsupportedGenericArgument => write!( + f, + "type `{}` has a generic argument that is neither a type nor a lifetime", + self.offending + ), + }?; + write!(f, " — see docs/source-language.md for the accepted grammar") + } +} + +impl std::error::Error for UnsupportedType {} + +/// Lower one captured type. +/// +/// **Total over the accepted grammar**: `Ok` means every part of the type was +/// understood, so a form this function does not lower is a form the language +/// does not accept. +/// +/// `at` is the origin of the item this type was written in — the location every +/// node lowered from that item shares, and the crate an array extent's const +/// must come from. +pub(crate) fn lower_type( + ty: &syn::Type, + consts: &ConstIndex, + at: &Rc, +) -> Result { + let fail = |reason| UnsupportedType { + offending: ty.to_token_stream().to_string(), + reason, + }; + // Every arm builds `kind` only; the origin is attached once, here, so no arm + // can forget it or attach a rebuilt approximation. + let kind = match ty { + // A group or paren wraps the same type. Its inner node keeps the inner + // spelling, which is the one a consumer wants to emit. + syn::Type::Group(g) => return lower_type(&g.elem, consts, at), + syn::Type::Paren(p) => return lower_type(&p.elem, consts, at), + syn::Type::Reference(r) => TypeKind::Ref { + mutable: r.mutability.is_some(), + inner: Box::new(lower_type(&r.elem, consts, at)?), + }, + // `[T]` is the borrowed spelling of the same concept `Vec` owns. + syn::Type::Slice(s) => TypeKind::Sequence(Box::new(lower_type(&s.elem, consts, at)?)), + _ if is_unit_type(ty) => TypeKind::Unit, + // Only the unit is in the language. Refusing here names the type; + // accepting would defer the failure to an "unresolved type" much later. + syn::Type::Tuple(_) => return Err(fail(UnsupportedTypeReason::UnsupportedTuple)), + syn::Type::Array(a) => { + let rendered = a.to_token_stream().to_string(); + let extent = lower_array_len(&a.len, &rendered, at, consts) + .map_err(|e| fail(UnsupportedTypeReason::BadArrayExtent(Box::new(e))))?; + TypeKind::Array { + elem: Box::new(lower_type(&a.elem, consts, at)?), + extent, + } + } + // The callback shape is decided by `extract_fn_trait_args`, this + // module's own — and the pipeline's only — authority for the form. + syn::Type::ImplTrait(_) => match super::extract_fn_trait_args(ty) { + Some(args) => TypeKind::Callback { + args: args + .iter() + .map(|a| lower_type(a, consts, at)) + .collect::>()?, + }, + None => return Err(fail(UnsupportedTypeReason::DisallowedImplTrait)), + }, + syn::Type::Path(tp) => lower_path(ty, tp, consts, at)?, + _ => return Err(fail(UnsupportedTypeReason::UnsupportedForm)), + }; + Ok(Type { + kind, + origin: Origin::new(ty.clone(), Rc::clone(at)), + }) +} + +fn lower_path( + ty: &syn::Type, + tp: &syn::TypePath, + consts: &ConstIndex, + at: &Rc, +) -> Result { + let fail = |reason| UnsupportedType { + offending: ty.to_token_stream().to_string(), + reason, + }; + // An associated type is refused rather than carried: the frontend never + // captures `impl` blocks, so what `::Assoc` resolves to is + // unknowable here, and keeping the spelling would only move the failure + // downstream. + if tp.qself.is_some() { + return Err(fail(UnsupportedTypeReason::AssociatedType)); + } + let Some(last) = tp.path.segments.last() else { + return Err(fail(UnsupportedTypeReason::UnsupportedForm)); + }; + let name = last.ident.to_string(); + + // Type arguments only. A lifetime argument is accepted and dropped: it is + // part of the spelling (`Foo<'a>` is not `Foo`), and the spelling is in + // `Type::syntax`, so modelling it would be a second copy of one fact. + let mut has_lifetime_arg = false; + let args: Vec = match &last.arguments { + syn::PathArguments::None => Vec::new(), + syn::PathArguments::AngleBracketed(ab) => { + let mut out = Vec::new(); + for a in &ab.args { + match a { + syn::GenericArgument::Type(t) => { + out.push(lower_type(t, consts, at)?); + } + syn::GenericArgument::Lifetime(_) => has_lifetime_arg = true, + _ => return Err(fail(UnsupportedTypeReason::UnsupportedGenericArgument)), + } + } + out + } + syn::PathArguments::Parenthesized(_) => { + return Err(fail(UnsupportedTypeReason::UnsupportedForm)) + } + }; + + // A builtin must be spelled BARE. `normalize_type` has already reduced the + // real std paths (`std::option::Option` → `Option`) at ingest and + // deliberately leaves unknown crate paths alone, so anything still carrying + // a prefix is a foreign type that merely shares a name — `foreign::Option` + // is not `Option`, and collapsing it would silently retype the field. + let is_bare = tp.path.leading_colon.is_none() && tp.path.segments.len() == 1; + if is_bare { + if args.is_empty() && !has_lifetime_arg { + if let Some(kind) = ScalarKind::from_name(&name) { + return Ok(TypeKind::Scalar(kind)); + } + // `String` and `str` are one concept. `str` is unsized and so only + // ever appears behind a `&`, which the `Ref` layer already records + // — classifying it as a nominal type instead would send every + // adapter looking for an item named `str` to resolve. + if name == "String" || name == "str" { + return Ok(TypeKind::Str); + } + } + // A builtin generic takes types only; a lifetime argument on one is not + // a shape this language has. + if !has_lifetime_arg { + let mut args = args; + let arity = |n: usize| { + if args.len() == n { + Ok(()) + } else { + Err(fail(UnsupportedTypeReason::WrongGenericArity { + expected: n, + })) + } + }; + match name.as_str() { + "Option" => { + arity(1)?; + return Ok(TypeKind::Optional(Box::new(args.remove(0)))); + } + "Vec" => { + arity(1)?; + return Ok(TypeKind::Sequence(Box::new(args.remove(0)))); + } + // `Box` **is** `T`: an owned value either way, and no + // destination language can tell the two apart. So it carries no + // kind of its own and classifies as whatever it wraps — the + // `Box` survives in `Type::syntax`, which is what generated + // Rust spells. (A shared-ownership handle would classify as a + // `Ref` for the same reason, when the language accepts one.) + "Box" => { + arity(1)?; + return Ok(args.remove(0).kind); + } + "Result" => { + arity(2)?; + let err = Box::new(args.remove(1)); + let ok = Box::new(args.remove(0)); + return Ok(TypeKind::Fallible { ok, err }); + } + _ => return Ok(named(tp, args)), + } + } + } + Ok(named(tp, args)) +} + +/// True when `ty` is the unit type `()`. +/// +/// The language's one answer to that question: [`lower_type`] classifies it as +/// [`TypeKind::Unit`], and the callback grammar uses it to insist a callback +/// returns nothing. +pub(crate) fn is_unit_type(ty: &syn::Type) -> bool { + match ty { + syn::Type::Tuple(t) => t.elems.is_empty(), + // A parenthesized or grouped `()` is still `()`. + syn::Type::Paren(p) => is_unit_type(&p.elem), + syn::Type::Group(g) => is_unit_type(&g.elem), + _ => false, + } +} + +/// `Named` with the identity read off the path: every segment joined, minus the +/// generic arguments, which are already in `args`. +fn named(tp: &syn::TypePath, args: Vec) -> TypeKind { + let name = tp + .path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::"); + TypeKind::Named { + id: TypeId { name }, + args, + } +} diff --git a/prebindgen/src/api/core/mod.rs b/prebindgen/src/api/core/mod.rs index b61a63d1..cd9ecba8 100644 --- a/prebindgen/src/api/core/mod.rs +++ b/prebindgen/src/api/core/mod.rs @@ -22,6 +22,7 @@ pub mod domain; pub mod expand; pub mod gravestone; +pub mod language; pub mod niches; pub mod prebindgen; pub mod registry; @@ -34,6 +35,7 @@ pub(crate) mod write; pub use self::{ domain::{DomainScalar, RepresentationDomain, ScalarValue}, gravestone::{Gravestone, Transmute}, + language::{Element, Language}, niches::{NicheSlot, Niches}, prebindgen::{const_path_alias, ConverterImpl, Prebindgen, Stage}, registry::{Direction, Generation, Registry, ScanError, TypeEntry, TypeKey, WriteRustError}, diff --git a/prebindgen/src/api/core/prebindgen.rs b/prebindgen/src/api/core/prebindgen.rs index b7d123a3..8f78c831 100644 --- a/prebindgen/src/api/core/prebindgen.rs +++ b/prebindgen/src/api/core/prebindgen.rs @@ -196,10 +196,17 @@ pub trait Prebindgen { /// [`crate::api::core::unfold::UnfoldPlan`] on the registry and its leaf /// types are registered as required outputs. /// - /// Returned by value, same as [`Self::expansions`]. + /// Returned by value, same as [`Self::expansions`]. The registry is + /// available because a declaration may name a **value form** (an accessor + /// returning "this type's fields in one struct") whose fields have to be + /// read off the indexed struct to become records. /// /// Default: `None`. - fn deconstructors(&self) -> Option { + fn deconstructors( + &self, + registry: &Registry, + ) -> Option { + let _ = registry; None } diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs index 7656ab66..8b346647 100644 --- a/prebindgen/src/api/core/registry.rs +++ b/prebindgen/src/api/core/registry.rs @@ -1395,7 +1395,7 @@ impl Registry { &declared.method_receivers, )?; } - if let Some(dec) = ext.deconstructors() { + if let Some(dec) = ext.deconstructors(self) { crate::api::core::unfold::apply(self, &dec, &declared.functions, &declared.accessors)?; } // Synthesized by-value `data_class` decompositions: build the leaves @@ -1483,43 +1483,9 @@ pub fn immediate_subtype_positions(ty: &syn::Type) -> Vec { } } -/// If `ty` is `impl Fn(T1, T2, ...) + Send + Sync + 'static`, return the -/// `Fn` argument types in declaration order. Otherwise None. -pub fn extract_fn_trait_args(ty: &syn::Type) -> Option> { - let syn::Type::ImplTrait(it) = ty else { - return None; - }; - let mut args: Option> = None; - let mut has_send = false; - let mut has_sync = false; - let mut has_static = false; - for bound in &it.bounds { - match bound { - syn::TypeParamBound::Trait(tb) => { - let last = tb.path.segments.last()?; - let name = last.ident.to_string(); - match name.as_str() { - "Fn" => { - let syn::PathArguments::Parenthesized(p) = &last.arguments else { - return None; - }; - args = Some(p.inputs.iter().cloned().collect()); - } - "Send" => has_send = true, - "Sync" => has_sync = true, - _ => return None, - } - } - syn::TypeParamBound::Lifetime(lt) if lt.ident == "static" => has_static = true, - _ => return None, - } - } - if has_send && has_sync && has_static { - args - } else { - None - } -} +/// The callback grammar, which the source language owns — re-exported here for +/// the existing call sites until they consume elements (stages L2–L4 of #229). +pub use crate::api::core::language::extract_fn_trait_args; /// A **resolved** binding generation: the [`Registry`] after /// [`Registry::resolve`] ran the adapter's scan, plans, and type diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index cb4f96d0..805ba7a1 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -37,7 +37,10 @@ mod plan; pub use self::{ error::{UnfoldDeclError, UnfoldError}, - plan::{DeconId, DeconSpec, LeafSource, UnfoldLeaf, UnfoldPlan, UnfoldShape}, + plan::{ + steps_are_movable, DeconId, DeconSpec, Hoist, LeafSource, PathStep, UnfoldLeaf, UnfoldPlan, + UnfoldShape, + }, }; // ────────────────────────────────────────────────────────────────────── @@ -73,6 +76,58 @@ pub enum DeconRecord { /// moved for an owned `T`). At most one per /// deconstructor. Identity, + /// Read the fields of the type's **value form**: call `func` once + /// (`f(&T) -> TStruct`) and contribute one record per [`FieldRecord`], + /// reached by field access on the returned struct. The language adapter + /// builds the field list (it knows which structs are declared classes and + /// therefore inline); this record only says how to get there. + /// + /// Each field then decomposes exactly like an [`Acc`](Self::Acc) record's + /// return does — its own [`records`](FieldRecord::records) if the + /// declaration overrode it, else its type's own deconstructor if it has + /// one, else one leaf — so a value form and a hand-written field list + /// produce the same leaves. + Fields { + func: syn::Ident, + /// The accessor **consumes** its receiver (`f(T) -> TStruct`): the + /// value is moved in and each field moved *out* into its leaf, instead + /// of being cloned out of a borrow. Declared by the adapter rather than + /// read off the signature — giving the value away is a boundary + /// decision — and cross-checked against the signature when the records + /// are flattened, so the two cannot drift. + consuming: bool, + fields: Vec, + }, +} + +/// One field of a value form (see [`DeconRecord::Fields`]). +#[derive(Clone)] +pub struct FieldRecord { + /// Field-access chain from the value form's returned struct. More than one + /// element when the adapter inlined a nested declared class. + pub members: Vec, + /// The leaf name (already `__`-joined across inlined nesting). + pub name: String, + /// The field's type as written, `Option` / `Vec` layers included. + pub ty: syn::Type, + /// How this field decomposes. + pub decon: FieldDecon, +} + +/// How one [`FieldRecord`] decomposes. +#[derive(Clone)] +pub enum FieldDecon { + /// By the field type's own deconstructor if it has one, else one leaf — + /// the same default a [`DeconRecord::Acc`] record's return follows. + Default, + /// Explicit records, replacing the type default wholesale (the declaration + /// stated this field's complete leaf set). + Records(Vec), + /// Leaves the **adapter** built, appended with this field's path and name + /// prefixed onto each. For shapes whose leaf structure only the adapter + /// knows — a decomposed sum, which is a selector plus one group per + /// alternative rather than a product of records. + Leaves(Vec), } impl DeconRecord { @@ -230,23 +285,7 @@ pub fn apply( // Binding-local records skip the gate — there is no `#[prebindgen]` item // behind them — but keep the reserved-separator name check. for d in &acc.deconstructors { - for rec in &d.records { - let (func, name) = match rec { - DeconRecord::Acc { func, name } => (Some(func), name), - DeconRecord::LocalAcc { name, .. } => (None, name), - DeconRecord::Identity => continue, - }; - // `"__"` is the reserved nesting/chain separator — author leaf names - // must not contain it. - if name.contains("__") { - return Err(UnfoldError::ReservedSeparator { name: name.clone() }); - } - if let Some(func) = func { - if !accessor_fns.contains(func) { - return Err(UnfoldError::RecordNotAccessor { func: func.clone() }); - } - } - } + check_records(&d.records, accessor_fns)?; } // Explicit decls first; they take precedence over (and suppress) a default @@ -624,6 +663,7 @@ fn wire_fixed_returns( delivery: Delivery::Callback, convert_out_ty: None, fixed_builder: true, + hoists: Vec::new(), }; registry.unfold_plans.insert(func.clone(), plan); } @@ -690,6 +730,7 @@ fn wire_fixed_callbacks( delivery: Delivery::Callback, convert_out_ty: None, fixed_builder: true, + hoists: Vec::new(), }; registry.callback_arg_plans.insert(key, plan); } @@ -804,11 +845,57 @@ fn whole_leaf_fold_plan(vec_elem: &syn::Type, shape: UnfoldShape) -> UnfoldPlan delivery: Delivery::Callback, convert_out_ty: None, fixed_builder: true, + hoists: Vec::new(), + } +} + +/// The deconstructor gate: every accessor-function record must be a declared +/// `.fun_accessor` (the single source of truth for "accessor"), and no author +/// leaf name may contain the reserved `"__"` chain separator. Binding-local +/// records skip the accessor check — there is no `#[prebindgen]` item behind +/// them — but keep the name check. +/// +/// Recurses into a value form's per-field override records, so an override is +/// held to the same rules as the declaration it replaces. +fn check_records( + records: &[DeconRecord], + accessor_fns: &HashSet, +) -> Result<(), UnfoldError> { + for rec in records { + let (func, name) = match rec { + DeconRecord::Acc { func, name } => (Some(func), name), + DeconRecord::LocalAcc { name, .. } => (None, name), + DeconRecord::Identity => continue, + // A value form's field names come from struct idents, not from the + // author, so the `"__"` in an inlined nested name is the separator + // doing its job. An author-supplied rename is checked where it is + // declared. + DeconRecord::Fields { func, fields, .. } => { + if !accessor_fns.contains(func) { + return Err(UnfoldError::RecordNotAccessor { func: func.clone() }); + } + for fr in fields { + if let FieldDecon::Records(recs) = &fr.decon { + check_records(recs, accessor_fns)?; + } + } + continue; + } + }; + if name.contains("__") { + return Err(UnfoldError::ReservedSeparator { name: name.clone() }); + } + if let Some(func) = func { + if !accessor_fns.contains(func) { + return Err(UnfoldError::RecordNotAccessor { func: func.clone() }); + } + } } + Ok(()) } /// Strip a single leading `&` (one level) from a type. -fn peel_ref(ty: &syn::Type) -> syn::Type { +pub(crate) fn peel_ref(ty: &syn::Type) -> syn::Type { match ty { syn::Type::Reference(r) => (*r.elem).clone(), other => other.clone(), @@ -949,6 +1036,7 @@ fn process_decl( delivery: ed.delivery, convert_out_ty: None, fixed_builder: false, + hoists: Vec::new(), } } } else { @@ -982,16 +1070,28 @@ fn process_decl( plan }; // Delivery is by **leaf count**, not a per-decl flag: - // * Output, single leaf, non-Iterable ⇒ Return (wrapper returns the - // value via its ordinary output converter — `convert_out_ty`). + // * Output, single non-nullable leaf, non-Iterable ⇒ Return (wrapper + // returns the value via its ordinary output converter — + // `convert_out_ty`). // * Output, multiple leaves or Iterable (at any layer — an // `Optional(Iterable)` fold has no single value to return) ⇒ // Callback (builder / fold). // * Error ⇒ always Callback-shaped: every leaf is a `ze` arg after the // fixed `je` (no return-value path; `convert_out_ty` stays None). + // + // A NULLABLE leaf is one whose path passes through an `Option` that + // something is decomposed below (`Option` reached by + // `.field_self()`, a nested value form behind an `Option`). Returning it + // has nowhere to put the absent case: a return value is one expression, + // so there is no `None` arm, and `convert_out_ty` names the leaf's own + // type rather than an optional of it. Callback delivery has that arm + // already — the leaf crosses as a boxed `Long` / JVM null — so the + // shape goes there instead of being composed into Rust that hands + // `&Option` to a converter typed for `T`. let single_return = ed.target == DeconTarget::Output && !plan.shape.has_iterable_layer() - && plan.leaves.len() == 1; + && plan.leaves.len() == 1 + && !plan.leaves[0].nullable; let plan = if single_return { let leaf_ty = plan.leaves[0].out_ty.clone(); let cv_ty: syn::Type = if matches!(plan.shape, UnfoldShape::Optional((), _)) { @@ -1052,6 +1152,9 @@ fn register_decon_spec( false, &mut visited, &mut leaves, + // A `DeconSpec` describes the leaf list only — signature artifacts are + // derived from it, never emitted code — so its hoists are discarded. + &mut Vec::new(), )?; require_unique_leaf_names(source, &leaves)?; registry.decon_plans.insert( @@ -1116,6 +1219,7 @@ fn build_plan( let mut leaves: Vec = Vec::new(); let mut visited: HashSet = HashSet::new(); visited.insert(TypeKey::from_type(source)); + let mut hoists: Vec = Vec::new(); flatten( acc, registry, @@ -1127,6 +1231,7 @@ fn build_plan( false, &mut visited, &mut leaves, + &mut hoists, )?; require_unique_leaf_names(source, &leaves)?; require_root_identity_last(by_ref, source, &leaves)?; @@ -1141,6 +1246,7 @@ fn build_plan( delivery: ed.delivery, convert_out_ty: None, fixed_builder: false, + hoists, }) } @@ -1194,12 +1300,13 @@ fn flatten( registry: &Registry, records: &[DeconRecord], source: &syn::Type, - path_prefix: &[syn::Ident], + path_prefix: &[PathStep], name_prefix: &[String], by_ref: bool, nullable: bool, visited: &mut HashSet, leaves: &mut Vec, + hoists: &mut Vec, ) -> Result<(), UnfoldError> { let source_key = TypeKey::from_type(source); // The author-supplied (literal) leaf-name segment at this level, appended @@ -1222,11 +1329,15 @@ fn flatten( }); } seen_identity = true; - // Owned at the root of an owned value (a `Copy` blob copies / - // an opaque handle moves); borrowed (clone) otherwise. The - // adapter-side type + projection come from this `out_ty`'s - // output converter. - let out_ty: syn::Type = if path_prefix.is_empty() && !by_ref { + // Owned where the value is OURS to give: the root of an owned + // plan (a `Copy` blob copies / an opaque handle moves), or a + // field of a value form that CONSUMED its value — that form was + // handed the value, so its fields move out like every other + // field of it. Borrowed (clone) otherwise. The adapter-side type + // + projection come from this `out_ty`'s output converter, so + // this is what decides whether the leaf is boxed by move or + // cloned through the borrowed-opaque one. + let out_ty: syn::Type = if place_is_owned(hoists, path_prefix, by_ref) { source.clone() } else { syn::parse_quote!(&#source) @@ -1245,6 +1356,179 @@ fn flatten( group: None, }); } + DeconRecord::Fields { + func, + consuming, + fields, + } => { + let consuming = *consuming; + // The value form is called once; every field hangs off that one + // call, so the whole record shares a single `Call` step and the + // emitter can hoist it. + let (takes, _ret) = accessor_signature(registry, func)?; + check_takes(func, &takes, source)?; + // The declarator states whether the value is given away; the + // signature has to agree, or the emitted call would not compile + // in the consumer's crate. Checked rather than inferred so that + // declaring `.fields_self_into(..)` on a borrowing accessor is a + // named error instead of a silently downgraded boundary. + if consuming != accessor_consumes(registry, func) { + return Err(UnfoldError::Unsupported { + func: func.clone(), + reason: if consuming { + "declared as a CONSUMING value form (`.fields_self_into(..)`) but the \ + accessor borrows its receiver — declare it with `.fields(..)`, or \ + name the by-value accessor" + } else { + "declared as a BORROWING value form (`.fields(..)`) but the accessor \ + takes its receiver by value — declare it with `.fields_self_into(..)`, or \ + name the `&Self` accessor" + }, + }); + } + let mut root_path = path_prefix.to_vec(); + root_path.push(PathStep::call(func.clone(), false)); + // A hoist below an optional step cannot be emitted as an + // unconditional local: composing the path directly would pass + // `&Option` to the child value-form accessor. The current + // flat leaf emitter has no conditional-hoist representation, + // so reject the shape instead of generating ill-typed Rust. + // A top-level `Option` is represented by + // `UnfoldShape::Optional`, not by a path step, and is unaffected. + if root_path.iter().any(PathStep::is_optional) { + return Err(UnfoldError::Unsupported { + func: func.clone(), + reason: "a nested value form reached through `Option` — conditional \ + value-form hoisting is not implemented", + }); + } + // A consuming value form DESTROYS the value into its parts, so + // a sibling record — `.field_self()` or another `.field()` — + // would read what it just gave away. jnigen refuses this in the + // declarator, where the author can see it; this is the backstop + // for records built directly against core. + // + // Being reached through ANOTHER value form is fine: a hoisted + // value form is an owned struct and its fields are disjoint, so + // the parent's field is handed over by move. + if consuming && records.len() > 1 { + return Err(UnfoldError::Unsupported { + func: func.clone(), + reason: "a consuming value form must be the only record of its \ + declaration — it moves the value, so `.field_self()` or \ + a sibling `.field()` would read a moved value", + }); + } + // Evaluate this value form ONCE. Recorded at the prefix it sits + // at rather than as a lone accessor, so a nested value form + // (this record reached through another one's field) gets its own + // hoist instead of being rebuilt per child leaf. `path_prefix` + // grows as `flatten` descends, so the list comes out + // outermost-first. + hoists.push(Hoist { + prefix: root_path.clone(), + consuming, + }); + + for fr in fields { + // A field's own `Option` makes everything under it nullable, + // exactly as an `Option`-returning accessor step does. + let (opt, core) = match option_inner_type(&fr.ty) { + Some(inner) => (true, inner), + None => (false, fr.ty.clone()), + }; + let child_ty = peel_ref(&core); + let child_key = TypeKey::from_type(&child_ty); + + // Same three-way choice a `.field()` record makes: declared + // override, else the field type's own deconstructor, else + // one leaf — with the adapter able to pre-build the leaves + // for a shape only it can describe. + let child_records = match &fr.decon { + FieldDecon::Records(recs) => Some(recs.clone()), + FieldDecon::Leaves(_) => None, + FieldDecon::Default => match find_deconstructor_by_type(acc, &child_key) { + Some(child_decl) if !visited.contains(&child_key) => { + Some(child_decl.records.clone()) + } + Some(_) => { + return Err(UnfoldError::Cycle { + target: child_key.to_string(), + }); + } + None => None, + }, + }; + let decomposed = + child_records.is_some() || matches!(fr.decon, FieldDecon::Leaves(_)); + + // The field's own `Option` is a nullable NESTING step only + // when something is decomposed below it. For a plain leaf + // the whole `Option` is what the converter takes — the + // same rule that makes a terminal accessor's `Option` ride + // its converter instead of being unwrapped. + let mut field_path = root_path.clone(); + let (last, lead) = fr + .members + .split_last() + .expect("a field record addresses at least one member"); + // Only the LAST member can be optional — an inlined nested + // class is reached directly, never through an `Option`. + field_path.extend(lead.iter().map(|m| PathStep::field(m.clone(), false))); + field_path.push(PathStep::field(last.clone(), opt && decomposed)); + + // Adapter-built leaves: rebase each onto this field's path + // and name. Their internal structure (a selector plus its + // groups) is opaque here and passes through untouched. + if let FieldDecon::Leaves(built) = &fr.decon { + for l in built { + let mut path = field_path.clone(); + path.extend(l.path.iter().cloned()); + let mut name = seg_name(&fr.name); + name.push(l.name.clone()); + leaves.push(UnfoldLeaf { + name: name.join("__"), + path, + nullable: l.nullable || nullable || opt, + ..l.clone() + }); + } + continue; + } + + if let Some(child_records) = child_records { + visited.insert(child_key.clone()); + flatten( + acc, + registry, + &child_records, + &child_ty, + &field_path, + &seg_name(&fr.name), + by_ref, + nullable || opt, + visited, + leaves, + hoists, + )?; + visited.remove(&child_key); + } else { + // A plain field leaf: the value is CLONED out of the + // struct, so its converter takes the owned field type as + // written — `Option` and all, which is why a terminal + // `Option` step is not a nesting step for it. + leaves.push(UnfoldLeaf { + name: seg_name(&fr.name).join("__"), + path: field_path, + out_ty: fr.ty.clone(), + identity: false, + nullable, + source: LeafSource::Field, + group: None, + }); + } + } + } DeconRecord::Acc { name, .. } | DeconRecord::LocalAcc { name, .. } => { // A binding-local record resolves through its synthesized // registry entry (see `synthesize_local_accessors`), so both @@ -1253,7 +1537,7 @@ fn flatten( let (func, local) = match rec { DeconRecord::Acc { func, .. } => (func.clone(), false), DeconRecord::LocalAcc { path, .. } => (DeconRecord::local_ident(path), true), - DeconRecord::Identity => unreachable!(), + DeconRecord::Identity | DeconRecord::Fields { .. } => unreachable!(), }; let (takes, ret) = accessor_signature(registry, &func)?; check_takes(&func, &takes, source)?; @@ -1288,7 +1572,7 @@ fn flatten( visited.insert(child_key.clone()); let child_records = child_decl.records.clone(); let mut child_path = path_prefix.to_vec(); - child_path.push(func.clone()); + child_path.push(PathStep::call(func.clone(), opt)); flatten( acc, registry, @@ -1300,6 +1584,7 @@ fn flatten( nullable || opt, visited, leaves, + hoists, )?; visited.remove(&child_key); } else { @@ -1332,7 +1617,7 @@ fn flatten( (ret, nullable, false) }; let mut path = path_prefix.to_vec(); - path.push(func.clone()); + path.push(PathStep::call(func.clone(), opt)); leaves.push(UnfoldLeaf { name: seg_name(name).join("__"), path, @@ -1415,6 +1700,45 @@ fn accessor_signature( Ok((takes, ret)) } +/// Whether the value sitting at `path_prefix` is the plan's **to give away**: +/// the root of an owned plan, or a field of a value form that consumed its +/// value and is reached by a movable run of field steps. +/// +/// Consulted where a leaf's `out_ty` is chosen, so the ownership decision is +/// made ONCE, in the plan, rather than re-derived by each emitter — a leaf +/// whose `out_ty` is the owned type is boxed by move, one whose `out_ty` is a +/// borrow is cloned through the borrowed-opaque converter. +fn place_is_owned(hoists: &[Hoist], path_prefix: &[PathStep], by_ref: bool) -> bool { + if path_prefix.is_empty() { + return !by_ref; + } + hoists + .iter() + .filter(|h| h.prefix.len() <= path_prefix.len() && path_prefix.starts_with(&h.prefix)) + .max_by_key(|h| h.prefix.len()) + .is_some_and(|h| h.consuming && steps_are_movable(&path_prefix[h.prefix.len()..])) +} + +/// Whether an accessor takes its receiver **by value** — a *consuming* value +/// form, which destroys the object into its parts instead of cloning them out +/// of a borrow. +/// +/// Asked separately because [`accessor_signature`] peels the `&` in order to +/// compare target types, so `f(v: T)` and `f(v: &T)` are indistinguishable +/// there by design. +fn accessor_consumes(registry: &Registry, func: &syn::Ident) -> bool { + registry.functions.get(func).is_some_and(|(f, _)| { + f.sig + .inputs + .iter() + .find_map(|input| match input { + syn::FnArg::Typed(pt) => Some(!matches!(*pt.ty, syn::Type::Reference(_))), + _ => None, + }) + .unwrap_or(false) + }) +} + fn check_takes( func: &syn::Ident, takes: &syn::Type, diff --git a/prebindgen/src/api/core/unfold/plan.rs b/prebindgen/src/api/core/unfold/plan.rs index 40fb3d05..5a485845 100644 --- a/prebindgen/src/api/core/unfold/plan.rs +++ b/prebindgen/src/api/core/unfold/plan.rs @@ -58,6 +58,93 @@ pub struct DeconSpec { pub leaves: Vec, } +/// One step of a leaf's [`UnfoldLeaf::path`] — how to get from the value +/// reached so far to the next one. +/// +/// A step is typed rather than a bare ident because a single path may **mix** +/// the two: an `expand_return!(T).fields(fields!(t_to_struct))` leaf calls the +/// value-form accessor, reads a struct field, and may then call that field +/// type's own accessor — `Call(t_to_struct)`, `Field(key_expr)`, +/// `Call(keyexpr_as_str)`. [`LeafSource`] still says what *kind* of leaf sits +/// at the end of the path; the steps say how it is reached. +/// +/// Each step also records whether it is **optional** — its accessor returns +/// `Option<…>`, or its field is typed `Option<…>`. A `true` on a step *before* +/// the last makes it a nullable nesting step: the emitter matches on it and the +/// `None` arm short-circuits the whole leaf to null. The flag is carried rather +/// than re-derived so both kinds answer the question the same way and the +/// emitter needs no type walk (an accessor's `Option` was already peeled where +/// the step was built). +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum PathStep { + /// Call a `#[prebindgen]` accessor on the value reached so far: + /// `source_module::f(&value)`. + Call { ident: syn::Ident, optional: bool }, + /// Read a struct field of the value reached so far: `value.f`. + Field { ident: syn::Ident, optional: bool }, +} + +impl PathStep { + /// An accessor call step. + pub fn call(ident: syn::Ident, optional: bool) -> Self { + Self::Call { ident, optional } + } + + /// A struct-field read step. + pub fn field(ident: syn::Ident, optional: bool) -> Self { + Self::Field { ident, optional } + } + + /// The step's ident, whichever kind it is. + pub fn ident(&self) -> &syn::Ident { + match self { + Self::Call { ident, .. } | Self::Field { ident, .. } => ident, + } + } + + /// Whether the step yields an `Option` — a nullable nesting step when it is + /// not the last on the path. + pub fn is_optional(&self) -> bool { + match self { + Self::Call { optional, .. } | Self::Field { optional, .. } => *optional, + } + } + + /// Whether the step is a plain (non-optional) field read — a path made only + /// of these renders as `value.a.b`, needing no nesting `match`. + pub fn is_plain_field(&self) -> bool { + matches!( + self, + Self::Field { + optional: false, + .. + } + ) + } + + /// Whether the step is a field read, `Option` or not. + pub fn is_field(&self) -> bool { + matches!(self, Self::Field { .. }) + } +} + +/// Whether a run of steps can be **moved** out of the value it hangs off: +/// field reads only, with an `Option` allowed on the last one — a `None` arm +/// still hands over the whole `Option` by value, while an `Option` in the +/// middle would have to be unwrapped and so can only be borrowed through. +/// +/// This is the one place the rule is written: the resolver uses it to decide +/// whether a leaf OWNS what it reaches (its `out_ty` then being the owned type +/// rather than a borrow), and the emitters use it to project that place. Two +/// readings of it would drift, and the disagreement would be a borrow handed to +/// an owning converter. +pub fn steps_are_movable(steps: &[PathStep]) -> bool { + steps + .iter() + .enumerate() + .all(|(i, s)| s.is_field() && (!s.is_optional() || i + 1 == steps.len())) +} + /// How a leaf's [`UnfoldLeaf::path`] is reached from the decomposed value. #[derive(Clone, PartialEq, Eq, Debug, Default)] pub enum LeafSource { @@ -140,6 +227,34 @@ pub struct UnfoldPlan { /// generic over `R`/`A` — it returns the concrete type). `false` for the /// accessor-declared deconstructors, whose builder is caller-supplied. pub fixed_builder: bool, + /// Value forms that must be evaluated **once** and bound to a local. Every + /// leaf below one reaches off that local — otherwise each field would + /// rebuild the whole struct, cloning all of it once per leaf. + /// + /// A list rather than a single accessor because value forms **compose**: a + /// field may splice a child type whose own boundary is derived from *its* + /// value form, and that child call is a second hoist nested under the + /// first. Ordered outermost-first, so a hoist can be composed from the + /// longest already-bound prefix of itself. + pub hoists: Vec, +} + +/// One hoisted value form: where it sits, and whether it **consumes** the value +/// it decomposes. +#[derive(Clone)] +pub struct Hoist { + /// The path prefix to bind, ending in the value form's + /// [`PathStep::Call`] (`DeconRecord::Fields`). + pub prefix: Vec, + /// `true` when the accessor takes its receiver **by value** + /// (`f(v: T) -> TStruct`), so the value is moved in and each field can be + /// moved *out* into its leaf instead of cloned — the whole point of a + /// consuming value form. + /// + /// Carried on the hoist rather than on [`PathStep::Call`] because only a + /// value-form root can consume: the ordinary accessor-chain steps are + /// always borrows. + pub consuming: bool, } /// One flattened output leaf of a decomposed return value. @@ -151,9 +266,10 @@ pub struct UnfoldLeaf { /// `"keyExpr"` → `"sample__keyExpr"`); a root identity leaf is `"handle"`. /// Names are unique within a deconstructor (a duplicate is a hard error). pub name: String, - /// Accessor-call chain from the root value (`[]` = the identity/root - /// itself; `[f]` = `f(&root)`; longer = nested records, M3). - pub path: Vec, + /// Reach chain from the root value (`[]` = the identity/root itself; + /// `[Call(f)]` = `f(&root)`; longer = nested records, M3). Steps of both + /// kinds may mix — see [`PathStep`]. + pub path: Vec, /// Type whose resolved **output** converter encodes this leaf — a /// reference type for accessors (`&str`, `&F`), `&Source` for the identity /// leaf (so the borrowed-opaque clone converter / projection is reused). diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index 7ea9765b..d0e8a224 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -88,7 +88,10 @@ fn accessor_optional_primitive() { ); assert_eq!(plan.leaves.len(), 1); assert!(!plan.leaves[0].identity); - assert_eq!(plan.leaves[0].path[0].to_string(), "z_timestamp_ntp64"); + assert_eq!( + plan.leaves[0].path[0].ident().to_string(), + "z_timestamp_ntp64" + ); assert_eq!(plan.leaves[0].out_ty.to_token_stream().to_string(), "i64"); assert!(reg .required_outputs_scan @@ -150,7 +153,10 @@ fn accessor_plan_byref() { // Accessor leaf: out_ty `&str`, path `[z_keyexpr_as_str]`. assert!(!plan.leaves[1].identity); assert_eq!(plan.leaves[1].path.len(), 1); - assert_eq!(plan.leaves[1].path[0].to_string(), "z_keyexpr_as_str"); + assert_eq!( + plan.leaves[1].path[0].ident().to_string(), + "z_keyexpr_as_str" + ); assert_eq!(plan.leaves[1].out_ty.to_token_stream().to_string(), "& str"); // Leaf out_tys registered as required outputs so the resolver builds @@ -484,7 +490,7 @@ fn nested_accessor_flatten() { let path = |l: &UnfoldLeaf| { l.path .iter() - .map(|i| i.to_string()) + .map(|i| i.ident().to_string()) .collect::>() .join(".") }; @@ -627,7 +633,7 @@ fn reply_product_double_option_flatten() { let path = |l: &UnfoldLeaf| { l.path .iter() - .map(|i| i.to_string()) + .map(|i| i.ident().to_string()) .collect::>() .join(".") }; @@ -797,7 +803,10 @@ fn iterable_decomposed_plan() { assert!(matches!(&plan.shape, UnfoldShape::Iterable(_))); assert!(plan.element.is_none(), "decomposed: element not used"); assert_eq!(plan.leaves.len(), 2); - assert_eq!(plan.leaves[0].path[0].to_string(), "z_zenoh_id_to_string"); + assert_eq!( + plan.leaves[0].path[0].ident().to_string(), + "z_zenoh_id_to_string" + ); assert_eq!( plan.leaves[0].out_ty.to_token_stream().to_string(), "String" @@ -1067,7 +1076,7 @@ fn value_struct_vec_is_fixed_iterable_fold() { reg_with(&["fn storage_get_vec(s: &Storage) -> Option> { todo!() }"]); let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { name: name.to_string(), - path: vec![ident(name)], + path: vec![PathStep::field(ident(name), false)], out_ty: ty, identity: false, nullable: false, @@ -1120,7 +1129,7 @@ fn value_struct_slice_callback_is_fixed_iterable_fold() { ]); let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { name: name.to_string(), - path: vec![ident(name)], + path: vec![PathStep::field(ident(name), false)], out_ty: ty, identity: false, nullable: false, @@ -1328,13 +1337,16 @@ fn callback_arg_plan_derived() { assert_eq!(plan.leaves.len(), 3); // Nested keyexpr identity (borrowed: non-root) + string + direct enum. assert!(plan.leaves[0].identity); - assert_eq!(plan.leaves[0].path[0].to_string(), "z_sample_key_expr"); + assert_eq!( + plan.leaves[0].path[0].ident().to_string(), + "z_sample_key_expr" + ); assert_eq!( plan.leaves[0].out_ty.to_token_stream().to_string(), "& ZKeyExpr" ); assert_eq!( - plan.leaves[1].path.last().unwrap().to_string(), + plan.leaves[1].path.last().unwrap().ident().to_string(), "z_keyexpr_as_str" ); assert_eq!( @@ -1412,7 +1424,10 @@ fn callback_arg_borrowed_decomposed() { assert_eq!(plan.delivery, Delivery::Callback); assert_eq!(plan.leaves.len(), 3); assert!(plan.leaves[0].identity); - assert_eq!(plan.leaves[0].path[0].to_string(), "z_sample_key_expr"); + assert_eq!( + plan.leaves[0].path[0].ident().to_string(), + "z_sample_key_expr" + ); assert_eq!( plan.leaves[2].out_ty.to_token_stream().to_string(), "SampleKind" @@ -1561,6 +1576,7 @@ fn leaf_vec_fold_skips_unnominated_and_preexisting() { delivery: Delivery::Return, convert_out_ty: None, fixed_builder: false, + hoists: Vec::new(), }; reg.unfold_plans.insert(ident("strings"), sentinel); apply_leaf_vec_folds(&mut reg, vec![syn::parse_quote!(String)], &declared) diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 7427f558..47257a51 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -748,6 +748,7 @@ impl JniGen { /// once, on the member), else the camel-cased Rust name. fn lower_fields( &self, + registry: &Registry, key: &TypeKey, fields: &[LocalField], ) -> Vec { @@ -755,6 +756,11 @@ impl JniGen { fields .iter() .map(|f| match f { + LocalField::Fields(decl) => DeconRecord::Fields { + func: decl.func.clone(), + consuming: decl.consuming, + fields: self.lower_value_form(registry, key, decl), + }, LocalField::Named(func, name_override) => { let name = name_override .clone() @@ -782,6 +788,276 @@ impl JniGen { .collect() } + /// Expand a `.fields(fields!(f))` declaration into one + /// [`FieldRecord`](crate::api::core::unfold::FieldRecord) per field of the + /// struct `f` returns — the value form. + /// + /// The walk is the adapter's job because only it knows which structs are + /// declared `data_class!`es: a **non-optional** nested one is inlined (its + /// own fields become records with `__`-joined names), matching what + /// `synth_value_struct_leaves` does for a by-value data class; everything + /// else is one record and core decides whether that record's type splices + /// its own `expand_return!`. + /// + /// Per-field `.field(...)` overrides and `.name(...)` renames key on the + /// **Rust field ident**, and both are checked against the struct: naming a + /// field the value form doesn't have is a hard error, which is the point — + /// a field renamed upstream must not silently lose its adjustment. + fn lower_value_form( + &self, + registry: &Registry, + key: &TypeKey, + decl: &FieldsDecl, + ) -> Vec { + let func = &decl.func; + let (item_fn, _) = registry.functions.get(func).unwrap_or_else(|| { + panic!( + "expand_return!({}).fields(fields!({func})): no `#[prebindgen]` function \ + `{func}` — a value form is an accessor `fn {func}(v: &{}) -> {}Struct`", + key.as_str(), + key.as_str(), + key.as_str(), + ) + }); + let ret: syn::Type = match &item_fn.sig.output { + syn::ReturnType::Type(_, t) => crate::api::core::unfold::peel_ref(t), + syn::ReturnType::Default => panic!( + "expand_return!({}).fields(fields!({func})): `{func}` returns nothing — a \ + value form returns the struct holding this type's fields", + key.as_str() + ), + }; + let TypeKind::DataStruct { st, .. } = self.type_kind(registry, &ret) else { + panic!( + "expand_return!({}).fields(fields!({func})): `{func}` returns `{}`, which is \ + not a struct — a value form returns a struct whose fields become the leaves", + key.as_str(), + ret.to_token_stream(), + ) + }; + let st = st.clone(); + + let mut out = Vec::new(); + self.walk_value_form(registry, key, decl, &st, &[], "", 0, &mut out); + + // Every adjustment must have found its field. An unknown name is the + // drift this whole declarator exists to catch, so it is an error rather + // than a no-op. + let named: std::collections::HashSet = out + .iter() + .map(|r: &crate::api::core::unfold::FieldRecord| { + r.members + .iter() + .map(|m| m.to_string()) + .collect::>() + .join(".") + }) + .collect(); + for (field, _) in decl.overrides.iter() { + assert!( + named.contains(field), + "fields!({func}).field(\"{field}\", ...): `{}` has no field `{field}` \ + (fields: {})", + st.ident, + named.iter().cloned().collect::>().join(", "), + ); + } + for (field, _) in decl.names.iter() { + assert!( + named.contains(field), + "fields!({func}).name(\"{field}\", ...): `{}` has no field `{field}` \ + (fields: {})", + st.ident, + named.iter().cloned().collect::>().join(", "), + ); + } + out + } + + /// One level of [`Self::lower_value_form`]'s struct walk. `members` / + /// `name_prefix` accumulate through inlined nested data classes; an + /// override or rename keys on the dotted member path, so a nested field is + /// addressed as `"outer.inner"`. + #[allow(clippy::too_many_arguments)] + fn walk_value_form( + &self, + registry: &Registry, + key: &TypeKey, + decl: &FieldsDecl, + st: &syn::ItemStruct, + members: &[syn::Ident], + name_prefix: &str, + depth: usize, + out: &mut Vec, + ) { + use crate::api::core::unfold::{FieldDecon, FieldRecord}; + let syn::Fields::Named(named) = &st.fields else { + panic!( + "expand_return!({}).fields(fields!({})): `{}` has no named fields — a value \ + form is a plain struct whose fields become the leaves", + key.as_str(), + decl.func, + st.ident, + ) + }; + // A value form holding itself would expand forever; the cycle rule for + // everything reachable BELOW a field is core's `visited` check. + assert!( + depth <= 16, + "expand_return!({}).fields(fields!({})): `{}` nests data classes more than 16 \ + deep — is a value form holding itself?", + key.as_str(), + decl.func, + st.ident, + ); + for field in &named.named { + let Some(fname) = field.ident.as_ref() else { + continue; + }; + let mut member_path = members.to_vec(); + member_path.push(fname.clone()); + let dotted = member_path + .iter() + .map(|m| m.to_string()) + .collect::>() + .join("."); + let camel = mangle_kotlin_ident(&kt_snake_to_camel(&fname.to_string())); + let name = decl + .names + .iter() + .find(|(f, _)| *f == dotted) + .map(|(_, n)| n.clone()) + .unwrap_or(camel); + let name = if name_prefix.is_empty() { + name + } else { + format!("{name_prefix}__{name}") + }; + + // An explicit override replaces the field type's default + // decomposition wholesale — including any nesting it would have had. + if let Some((_, ovr)) = decl.overrides.iter().find(|(f, _)| *f == dotted) { + // The override states the field's type, so it is cross-checked + // against the field the same way a per-fn `.expand_param` / + // `.expand_return` decl is checked against its parameter or + // return. Without this an override outlives an upstream + // field-type change — the very drift `.fields()` exists to + // catch — and two same-shaped handle types silently swap. + // Core applies override records to the whole field after + // peeling only an outer `Option`: a `Vec` remains `Vec`. + // Mirror that exact normalization here; peeling `Vec` would + // accept `expand_return!(T)` and only fail later when core + // applies its records to `Vec`. + let peeled = option_inner_type(&field.ty) + .map(|t| crate::api::core::unfold::peel_ref(&t)) + .unwrap_or_else(|| crate::api::core::unfold::peel_ref(&field.ty)); + let actual = TypeKey::from_type(&peeled); + assert!( + actual == ovr.key, + "fields!({}).field(\"{dotted}\", expand_return!({})): `{}.{dotted}` is \ + `{}`, not `{}` — a per-field override names the field's own type", + decl.func, + ovr.key.as_str(), + st.ident, + actual.as_str(), + ovr.key.as_str(), + ); + out.push(FieldRecord { + members: member_path, + name, + ty: field.ty.clone(), + decon: FieldDecon::Records(self.lower_fields(registry, &ovr.key, &ovr.fields)), + }); + continue; + } + + // A nested `data_class!` inlines when it is reached directly; behind + // `Option` / `Vec` it stays one leaf, whose own converter builds the + // object (the rule `synth_value_struct_leaves` already follows). + // A `sealed_class!` field has no whole-value converter at all, so it + // must decompose into its selector and groups wherever it appears. + let bare = option_inner_type(&field.ty).unwrap_or_else(|| field.ty.clone()); + let probe = vec_inner_type(&bare).unwrap_or_else(|| bare.clone()); + match self.type_kind(registry, &probe) { + TypeKind::DataStruct { st, cfg: Some(_) } + if option_inner_type(&field.ty).is_none() + && vec_inner_type(&field.ty).is_none() => + { + let child = st.clone(); + self.walk_value_form( + registry, + key, + decl, + &child, + &member_path, + &name, + depth + 1, + out, + ); + continue; + } + TypeKind::Sum => { + // A sum's leaves are a selector plus one group per + // alternative, laid out side by side at a FIXED position. + // A `Vec` of them has variable arity; an `Option` of one + // needs a present flag the unfold leaf list has no notion of + // (the `fromParts` bridge's `PlanFieldKind::Sum` does — a + // data-class field can be `Option`). + assert!( + vec_inner_type(&bare).is_none(), + "expand_return!({}).fields(fields!({})): field `{}.{}` is a \ + `Vec<{}>` — a sequence of tag-gated groups has variable arity and \ + cannot be laid out in a fixed leaf list", + key.as_str(), + decl.func, + st.ident, + dotted, + probe.to_token_stream(), + ); + assert!( + option_inner_type(&field.ty).is_none(), + "expand_return!({}).fields(fields!({})): field `{}.{}` is an \ + `Option<{}>` — an optional sum would need a present flag beside its \ + tag, which an output leaf list cannot carry. Give the field a \ + payload-less alternative instead of wrapping the sum in `Option`, \ + or override it with .field(\"{}\", ...)", + key.as_str(), + decl.func, + st.ident, + dotted, + probe.to_token_stream(), + dotted, + ); + let ident = bare_path_ident(&probe).expect("a sum type is a path type"); + let (item_enum, _) = registry + .enums + .get(&ident) + .expect("TypeKind::Sum implies an indexed enum"); + let sum_cfg = self.types[&TypeKey::from_type(&probe)] + .sum() + .expect("TypeKind::Sum implies a sealed-class config"); + out.push(FieldRecord { + members: member_path, + name, + ty: field.ty.clone(), + decon: FieldDecon::Leaves(crate::api::lang::jnigen::jni::synth_sum_leaves( + self, sum_cfg, item_enum, + )), + }); + continue; + } + _ => {} + } + + out.push(FieldRecord { + members: member_path, + name, + ty: field.ty.clone(), + decon: FieldDecon::Default, + }); + } + } + /// Lower the raw [`ExpandReturnDecl`]s into the core's immutable /// [`Deconstructors`] record set — the output-side peer of /// [`Self::build_expansions`], a pure declaration → record mapping. @@ -789,7 +1065,10 @@ impl JniGen { /// them. `skip_output` is derived from the class members: a /// `.constructor()` member's return is a factory, never /// output-flattened. - pub(crate) fn build_deconstructors(&self) -> crate::api::core::unfold::Deconstructors { + pub(crate) fn build_deconstructors( + &self, + registry: &Registry, + ) -> crate::api::core::unfold::Deconstructors { use crate::api::core::unfold::{ DeconSel, DeconTarget, DeconstructorDecl, Deconstructors, Delivery, OutputDecl, }; @@ -817,7 +1096,7 @@ impl JniGen { ); dec.deconstructors.push(DeconstructorDecl { target: decl.key.to_type(), - records: self.lower_fields(&decl.key, &decl.fields), + records: self.lower_fields(registry, &decl.key, &decl.fields), default: Some((DeconTarget::Output, Delivery::Callback)), }); } @@ -838,7 +1117,7 @@ impl JniGen { ); dec.outputs.push(OutputDecl { func: func.clone(), - sel: DeconSel::Inline(self.lower_fields(&decl.key, &decl.fields)), + sel: DeconSel::Inline(self.lower_fields(registry, &decl.key, &decl.fields)), target: DeconTarget::Output, delivery: Delivery::Callback, declared_source: Some(decl.key.to_type()), @@ -1001,26 +1280,10 @@ impl JniGen { LocalVariant::Ctor(f) => Some(f.clone()), LocalVariant::SelfIdentity => None, }); - let accessors = self - .return_expand_decls - .iter() - .map(|d| &d.fields) - .chain(self.fn_return_expands.iter().map(|(_, d)| &d.fields)) - .flatten() - .filter_map(|f| match f { - LocalField::Named(func, _) => Some(func.clone()), - // A binding-local field's synthesized fn is helper-only too: - // called by the generated code, never externed, and its - // synthesized registry entry must not trip the warning. - LocalField::Local { path, .. } => Some( - path.segments - .last() - .expect("validated non-empty at decl time") - .ident - .clone(), - ), - LocalField::SelfField => None, - }); + // Includes a binding-local field's synthesized fn (called by the + // generated code, never externed, so its synthesized registry entry + // must not trip the warning) and a value form's accessor. + let accessors = self.field_referenced_fns().into_iter(); // Synthesized binding-local fns from every entry form (path-built // fun! at fun/method/constructor/convert sites): their registry // entries exist only for signature reads — helper-only unless also @@ -1037,26 +1300,52 @@ impl JniGen { /// from parameter composition. Derived from *usage* — a function need not /// also be a `.method()` class member to be referenced this way. pub(crate) fn field_accessor_fns(&self) -> std::collections::HashSet { - self.return_expand_decls + self.field_referenced_fns().into_iter().collect() + } + + /// Every function ident referenced as a field by any `expand_return!` decl + /// (type-level or per-fn), recursing into a value form's per-field + /// overrides. The one walk behind both [`Self::field_accessor_fns`] and the + /// helper-only set in [`Self::boundary_referenced_fns`] — they ask the same + /// question of the same declarations, so a new field kind is taught to both + /// at once. + fn field_referenced_fns(&self) -> Vec { + fn walk(fields: &[LocalField], out: &mut Vec) { + for f in fields { + match f { + LocalField::Named(func, _) => out.push(func.clone()), + // A binding-local field's synthesized fn IS an accessor — + // excluded from parameter composition, and acknowledged so + // the registry's "skipping undeclared" warning stays quiet. + LocalField::Local { path, .. } => out.push( + path.segments + .last() + .expect("validated non-empty at decl time") + .ident + .clone(), + ), + LocalField::SelfField => {} + // The value form's own accessor, plus whatever its + // per-field overrides reference. + LocalField::Fields(d) => { + out.push(d.func.clone()); + for (_, ovr) in &d.overrides { + walk(&ovr.fields, out); + } + } + } + } + } + let mut out = Vec::new(); + for fields in self + .return_expand_decls .iter() .map(|d| &d.fields) .chain(self.fn_return_expands.iter().map(|(_, d)| &d.fields)) - .flatten() - .filter_map(|f| match f { - LocalField::Named(func, _) => Some(func.clone()), - // A binding-local field's synthesized fn IS an accessor — - // excluded from parameter composition, and acknowledged so - // the registry's "skipping undeclared" warning stays quiet. - LocalField::Local { path, .. } => Some( - path.segments - .last() - .expect("validated non-empty at decl time") - .ident - .clone(), - ), - LocalField::SelfField => None, - }) - .collect() + { + walk(fields, &mut out); + } + out } } diff --git a/prebindgen/src/api/lang/jnigen/jni/decl.rs b/prebindgen/src/api/lang/jnigen/jni/decl.rs index 3af915f5..567f18ee 100644 --- a/prebindgen/src/api/lang/jnigen/jni/decl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/decl.rs @@ -51,6 +51,11 @@ pub(crate) enum LocalField { sig: syn::Signature, name_override: Option, }, + /// Include **every field of the type's value form** — the struct returned + /// by the named accessor — each as its own field. Expands to the same + /// records the fields would produce if named one by one; see + /// [`ExpandReturnDecl::fields`]. + Fields(FieldsDecl), } // Class members are stored as the full `(FunctionDecl, MemberKind)` pair — @@ -292,6 +297,16 @@ macro_rules! expand_return { }; } +/// Build a [`FieldsDecl`] from the ident of a **value-form accessor** — +/// `fields!(sample_to_struct)` is `FieldsDecl::new(prebindgen::ident!(sample_to_struct))`. +/// The argument of [`ExpandReturnDecl::fields`](crate::lang::ExpandReturnDecl::fields). +#[macro_export] +macro_rules! fields { + ($name:ident) => { + $crate::lang::FieldsDecl::new($crate::ident!($name)) + }; +} + // ────────────────────────────────────────────────────────────────────── // Class-kind decls // ────────────────────────────────────────────────────────────────────── @@ -682,6 +697,7 @@ impl ExpandReturnDecl { /// decomposition comes from ITS type's boundary decl, not from the /// accessor). pub fn field(mut self, accessor: FunctionDecl) -> Self { + self.reject_beside_consuming("field(..)"); assert!( accessor.param_expands.is_empty() && accessor.return_expand.is_none(), "expand_return!({}).field(fun!({})): expand overrides don't apply to a \ @@ -717,9 +733,206 @@ impl ExpandReturnDecl { /// Declare it **last**, after any field that decomposes a nested handle, /// so the generated Rust moves the value only after those borrows. pub fn field_self(mut self) -> Self { + self.reject_beside_consuming("field_self()"); self.fields.push(LocalField::SelfField); self } + + /// The one rule [`Self::fields_self_into`] adds: it hands the value **itself** + /// over, so nothing else in the decl can still read it. + fn reject_beside_consuming(&self, what: &str) { + if let Some(f) = self.fields.iter().find_map(|f| match f { + LocalField::Fields(d) if d.consuming => Some(&d.func), + _ => None, + }) { + panic!( + "expand_return!({k}).fields_self_into(fields!({f})).{what}: `.fields_self_into(..)` hands \ + the value ITSELF over as its fields, so nothing else can read it afterwards — \ + it must be the decl's only record. Use `.fields(fields!(..))` with the \ + borrowing form of the accessor if you need both.", + k = self.key.as_str(), + f = f, + ); + } + } + + /// Take the fields from the type's **value form** — a `#[prebindgen]` + /// accessor returning "this type's own accessors gathered into one struct" + /// — instead of restating them. + /// + /// `.fields(fields!(f))` is exactly `.field(...)` applied to each field of + /// that struct, so it has the same configurability (per-field overrides and + /// renames live on the [`FieldsDecl`]) and, crucially, the same + /// decomposition rule: **each field crosses by its own type's default + /// output boundary**. A field whose type has its own `expand_return!` is + /// decomposed by it (a `KeyExpr` field still crosses as its string, not as + /// a handle); a declared `data_class!` field expands into its fields; a + /// field behind `Option` / `Vec` stays one leaf. So swapping a hand-written + /// field list for `.fields(...)` keeps the boundary shape it already had — + /// what changes is that the list can no longer drift from the struct. + /// + /// ``` + /// // Instead of restating SampleStruct's fields one by one: + /// let _ = prebindgen::expand_return!(Sample) + /// .fields(prebindgen::fields!(sample_to_struct)); + /// ``` + /// + /// The accessor **borrows** its receiver (`f(v: &Self) -> SelfStruct`): + /// the struct is built from a borrow, so each field is cloned into it and + /// the leaves clone again out of it, and the value survives. It therefore + /// mixes freely — `.fields(...).field_self()` delivers the value form's + /// fields *and* the live handle. At most one value form per decl. + /// + /// Where the value is delivered **owned** — a callback argument + /// (`impl Fn(Sample)`), an owned return — and nothing else needs it, use + /// [`fields_self_into`](Self::fields_self_into) instead: those clones are being paid + /// on a value that is about to be dropped. + pub fn fields(mut self, decl: FieldsDecl) -> Self { + self.reject_beside_consuming("fields(..)"); + self.reject_second_value_form(&decl); + self.fields.push(LocalField::Fields(decl)); + self + } + + /// Like [`fields`](Self::fields), but the accessor **consumes** its + /// receiver (`f(v: Self) -> SelfStruct`): the value is moved in and each + /// field is moved *out* into its leaf. No clones at all. + /// + /// This is the same decision [`field_self`](Self::field_self) makes, one + /// step further: `.field_self()` hands the value over whole, + /// `.fields_self_into(...)` hands *the value itself* over as its parts, and + /// `.fields(...)` hands over a copy of its parts. Use it wherever the value + /// arrives owned and is not needed afterwards — the hot receive path this + /// whole declarator exists to make cheap. + /// + /// ``` + /// let _ = prebindgen::expand_return!(Sample) + /// .fields_self_into(prebindgen::fields!(sample_into_struct)); + /// ``` + /// + /// Because it gives the value away it must be the decl's **only** record — + /// a `.field_self()` or a sibling `.field(...)` would read a value that is + /// gone — which is a declaration-time panic either way round. It may still + /// be reached through *another* value form: the parent's field is handed to + /// it by move, since a hoisted value form is an owned struct and its fields + /// are disjoint. + /// + /// The declarator and the accessor's signature must agree; naming a + /// `&Self` accessor here (or a by-value one on [`fields`](Self::fields)) is + /// an error, so the declared intent cannot drift from the function it + /// names. At a **borrowed** delivery position there is no value to give up, + /// so the emitter clones once up front and consumes the clone — the same + /// cost the borrowing form would have paid, which keeps one declaration + /// usable by both owned and `&T` returns of the type. + pub fn fields_self_into(mut self, decl: FieldsDecl) -> Self { + self.reject_second_value_form(&decl); + assert!( + self.fields.is_empty(), + "expand_return!({k}).fields_self_into(fields!({f})): `.fields_self_into(..)` hands the value \ + ITSELF over as its fields, so it must be the decl's only record — the records \ + already declared would read a value that is gone. Use `.fields(fields!(..))` with \ + the borrowing form of the accessor if you need both.", + k = self.key.as_str(), + f = decl.func, + ); + self.fields.push(LocalField::Fields(decl.consuming())); + self + } + + fn reject_second_value_form(&self, decl: &FieldsDecl) { + assert!( + !self + .fields + .iter() + .any(|f| matches!(f, LocalField::Fields(_))), + "expand_return!({}): the decl already expands a value form (fields!({})) — \ + one value form states the whole field set", + self.key.as_str(), + decl.func + ); + } +} + +/// A **value-form expansion**: the accessor whose returned struct supplies the +/// fields, plus the per-field adjustments. Built with +/// [`fields!`](crate::fields) and handed to +/// [`ExpandReturnDecl::fields`]. +/// +/// Both adjusters key on the **Rust struct field name**, mirroring +/// [`FunctionDecl::expand_param`]'s Rust-parameter-name key: an unknown field +/// name or a repeated one is a hard error, so a field renamed upstream is +/// caught rather than silently ignored. +#[derive(Clone)] +pub struct FieldsDecl { + pub(crate) func: syn::Ident, + pub(crate) overrides: Vec<(String, ExpandReturnDecl)>, + pub(crate) names: Vec<(String, String)>, + /// Set by [`ExpandReturnDecl::fields_self_into`] — the accessor consumes its + /// receiver. Declared rather than read off the signature, because giving + /// the value away is a boundary decision; the two are cross-checked when + /// the records are resolved. + pub(crate) consuming: bool, +} + +impl FieldsDecl { + pub fn new(func: syn::Ident) -> Self { + Self { + func, + overrides: Vec::new(), + names: Vec::new(), + consuming: false, + } + } + + pub(crate) fn consuming(mut self) -> Self { + self.consuming = true; + self + } + + /// Replace **one** field's decomposition, with the same + /// [`ExpandReturnDecl`] a type-level default uses — so the complete-set + /// rule applies here too: the decl states that field's entire leaf set. + /// Use it where the field's type default is not what this boundary wants + /// (a lone `.field_self()` keeps the raw handle instead of decomposing it). + pub fn field(mut self, field: impl AsRef, decl: ExpandReturnDecl) -> Self { + let field = field.as_ref().to_string(); + assert!( + !self.overrides.iter().any(|(f, _)| *f == field), + "fields!({}).field(\"{}\", ...): field already has an override — declare its \ + complete field set in ONE decl", + self.func, + field + ); + self.overrides.push((field, decl)); + self + } + + /// Rename **one** field's leaf, overriding the name derived from the struct + /// field ident. The literal Kotlin name, like `fun!(f).name(...)`. + pub fn name(mut self, field: impl AsRef, kotlin_name: impl Into) -> Self { + let field = field.as_ref().to_string(); + let kotlin_name = kotlin_name.into(); + assert!( + !self.names.iter().any(|(f, _)| *f == field), + "fields!({}).name(\"{}\", ...): field is already renamed", + self.func, + field + ); + // The derived names of inlined nested fields are joined with `"__"`, so + // an author name carrying one would forge a nesting that isn't there. + // (Core rejects it for a `.field()` name; here the name never reaches + // that check, so it is made at the point of declaration.) + assert!( + !kotlin_name.contains("__"), + "fields!({}).name(\"{}\", \"{}\"): `__` is the reserved chain separator \ + and cannot appear in a leaf name", + self.func, + field, + kotlin_name, + ); + self.names.push((field, kotlin_name)); + self + } } /// Unifies the two boundary decls into one type so [`JniGen::expand`] can diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index 8c56da38..161a6394 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -1,6 +1,7 @@ //! Output-expansion delivery: unfold plans and leaf encoding. use super::*; +use crate::api::core::unfold::{steps_are_movable, PathStep}; /// Emit the output-expansion delivery body (output phase) for a function /// marked `.expand_output()`. The return value (`__out`) is decomposed by the @@ -310,21 +311,227 @@ pub(crate) fn cast_wire_to_jobject( } } -/// Reach a leaf's input by folding its accessor `path` over `base`, then hand -/// the reached expression to `body` (which renders the encode and yields -/// `JObject`). Every `Option`-returning nesting step becomes a `match`: its -/// `None` arm short-circuits the whole leaf to `JObject::null()` (the value is -/// absent ⇒ the leaf is null) — any number of `Option` steps on the path nest. +/// Compose one [`PathStep`] onto the reference expression reached so far. +/// A `Call` applies its accessor (origin-qualified); a `Field` reads the field +/// and re-borrows, so the result is a reference either way and steps chain +/// uniformly. +pub(crate) fn compose_step( + qualify: &dyn Fn(&syn::Ident) -> syn::Path, + step: &PathStep, + e: TokenStream, +) -> TokenStream { + match step { + PathStep::Call { ident, .. } => { + let m = qualify(ident); + quote!(#m::#ident(#e)) + } + PathStep::Field { ident, .. } => quote!(&(#e).#ident), + } +} + +/// Start a reach from `base`, projecting the **leading run of plain field +/// steps** directly (`&base.a.b`) instead of through a borrow of the base +/// (`&(&base).a.b`). Returns the expression and how many steps it consumed. +/// +/// The two forms name the same value, but the second borrows the base **as a +/// whole**, which the borrow checker rejects once a sibling leaf has moved a +/// different field out of it. Projecting directly makes each leaf's borrow +/// disjoint, so a consuming value form's field moves are order-independent +/// rather than compiling only while the borrowing leaves happen to be declared +/// first. +fn project_leading_fields( + base: &TokenStream, + base_is_ref: bool, + path: &[PathStep], +) -> (TokenStream, usize) { + if base_is_ref { + return (base.clone(), 0); + } + let n = path.iter().take_while(|s| s.is_plain_field()).count(); + if n == 0 { + return (quote!(&#base), 0); + } + let segs: Vec<&syn::Ident> = path[..n].iter().map(PathStep::ident).collect(); + (quote!(&#base #(.#segs)*), n) +} + +/// Fold a leaf's whole `path` over `base` with no optional-step handling, then +/// apply the terminal treatment its [`LeafSource`] calls for: a `Field` leaf is +/// **cloned** out of the place it reached, because its converter takes the field +/// type as written (owned); every other leaf keeps the borrow its converter +/// expects. +/// +/// This is the derivation the single-leaf [`Delivery::Return`] shortcut uses +/// (`emit/wrapper.rs`). It exists here, beside [`reach_leaf`], so the two are +/// read and changed together: they drifted once already — the shortcut was +/// missing the `Field` clone and handed `&F` to an `F` converter. +/// +/// [`Delivery::Return`]: crate::api::core::unfold::Delivery::Return +pub(crate) fn reach_leaf_flat( + qualify: &dyn Fn(&syn::Ident) -> syn::Path, + leaf: &crate::api::core::unfold::UnfoldLeaf, + path: &[PathStep], + base: TokenStream, + base_is_ref: bool, + consuming: bool, +) -> TokenStream { + use crate::api::core::unfold::LeafSource; + // An optional step BEFORE the last one needs a `match` whose `None` arm has + // somewhere to go. This derivation has none — it yields a plain Rust value, + // not a `JObject` that could be null — so the shape is refused here rather + // than composed into code that cannot type-check in the consumer's crate. + assert!( + !path.iter().rev().skip(1).any(PathStep::is_optional), + "jnigen unfold: leaf `{}` reaches through an optional step but is \ + delivered as a single return value, which has no `None` arm — this \ + shape needs callback delivery", + leaf.name, + ); + // Whether what this leaf reaches is OURS, and so is moved rather than + // borrowed or cloned. The two leaf kinds say it differently: + // + // * an IDENTITY leaf carries the answer in its `out_ty` — the plan resolved + // it to the owned type exactly when the value is the plan's to give away + // (`place_is_owned`: an owned root, or a field of a CONSUMING value form), + // and that is also what selected the owning converter, which boxes the + // move rather than cloning a borrow; + // * a FIELD leaf's `out_ty` is the field type as written, owned either way, + // so ownership is the enclosing form's: only a consuming one gives its + // fields away. + // + // A trailing `Option` step cannot arrive here at all: return delivery has + // no `None` arm for the absent case, so a nullable leaf is routed to + // callback delivery when the plan picks its `Delivery` — see + // `single_return` in `core/unfold.rs`. `is_plain_field` is what that rules + // out, and it stays as the local statement of the same fact. + let reached_is_ours = if leaf.identity { + !matches!(leaf.out_ty, syn::Type::Reference(_)) + } else { + consuming + }; + if reached_is_ours && path.iter().all(PathStep::is_plain_field) { + let segs: Vec<&syn::Ident> = path.iter().map(PathStep::ident).collect(); + return quote!(#base #(.#segs)*); + } + let (mut e, lead) = project_leading_fields(&base, base_is_ref, path); + for step in &path[lead..] { + e = compose_step(qualify, step, e); + } + if leaf.source == LeafSource::Field { + quote!((#e).clone()) + } else { + e + } +} + +/// Every value form on a plan, evaluated **once** and bound to a local +/// (`__vf0`, `__vf1`, …), so a struct is built once per delivery rather than +/// once per field. The bound prefixes come back with the statements, since +/// reaching a leaf means starting from the innermost local it sits under. +/// +/// Shared by both delivery paths — the multi-leaf encoder below and the +/// single-leaf `Delivery::Return` shortcut in `emit/wrapper.rs`. The shortcut +/// used to compose its reach straight off the raw value, which for a consuming +/// value form emitted `f(&v)` against a by-value receiver: ill-typed Rust in +/// the consumer's crate. One binder, so the two cannot disagree about what a +/// hoist is or who owns it. +pub(crate) struct Hoisted { + /// The `let __vfN = …;` bindings, outermost-first. + pub(crate) stmts: TokenStream, + /// Each hoist's path prefix and the local it was bound to. + bound: Vec<(Vec, syn::Ident)>, + /// Whether each bound hoist consumed the value it decomposed. + consuming: Vec, +} + +impl Hoisted { + /// The innermost bound local `path` sits under, with that prefix already + /// consumed, and whether that hoist gave its value away. `None` for a leaf + /// under no value form at all — a sibling `.field()` / `.field_self()`, + /// which still reaches from the value itself. + pub(crate) fn rebase(&self, path: &[PathStep]) -> Option<(syn::Ident, Vec, bool)> { + self.bound + .iter() + .enumerate() + .filter(|(_, (p, _))| p.len() < path.len() && path.starts_with(p)) + .max_by_key(|(_, (p, _))| p.len()) + .map(|(i, (p, id))| (id.clone(), path[p.len()..].to_vec(), self.consuming[i])) + } +} + +pub(crate) fn bind_hoists( + qualify: &dyn Fn(&syn::Ident) -> syn::Path, + hoists: &[crate::api::core::unfold::Hoist], + value: &TokenStream, + by_ref: bool, +) -> Hoisted { + let mut out = Hoisted { + stmts: TokenStream::new(), + bound: Vec::new(), + consuming: Vec::new(), + }; + // Value forms COMPOSE, so each hoist is built from the longest hoist that + // is already a proper prefix of it (they arrive outermost-first), and from + // `value` otherwise. + for (i, h) in hoists.iter().enumerate() { + let local = format_ident!("__vf{}", i); + let (from, mut expr) = match out.rebase(&h.prefix) { + // A NESTED consuming form is handed the parent's field by MOVE: a + // hoisted value form is an owned struct and its fields are + // disjoint, so moving one out leaves every sibling leaf readable. + // `compose_step` borrows (`&(e).f`), so the field run to that field + // is projected here instead of going through it. + Some((outer, rest, _)) if h.consuming => { + let last = h.prefix.len() - 1; + let lead = &rest[..rest.len() - 1]; + if lead.iter().all(PathStep::is_plain_field) { + let segs: Vec<&syn::Ident> = lead.iter().map(PathStep::ident).collect(); + (last, quote!(#outer #(.#segs)*)) + } else { + // Reached through an accessor call, so what is in hand is a + // borrow with nothing to give up — clone once and consume + // the clone, exactly as a borrowed root does below. + let mut e = quote!(&#outer); + for step in lead { + e = compose_step(qualify, step, e); + } + (last, quote!((#e).clone())) + } + } + Some((outer, rest, _)) => (h.prefix.len() - rest.len(), quote!(&#outer)), + // A CONSUMING value form is handed the value itself, so its fields + // move out instead of being cloned out of a borrow. A borrowed plan + // has no value to give up, so it clones first — the same cost the + // borrowing form of the accessor would have paid, which keeps one + // declaration usable by both owned and `&T` returns of the type. + None if h.consuming && by_ref => (0, quote!(#value.clone())), + None if h.consuming => (0, value.clone()), + None if by_ref => (0, value.clone()), + None => (0, quote!(&#value)), + }; + for step in &h.prefix[from..] { + expr = compose_step(qualify, step, expr); + } + out.stmts.extend(quote! { let #local = #expr; }); + out.bound.push((h.prefix.clone(), local)); + out.consuming.push(h.consuming); + } + out +} + +/// Reach a leaf's input by folding its `path` over `base`, then hand the +/// reached expression to `body` (which renders the encode and yields +/// `JObject`). Every optional nesting step becomes a `match`: its `None` arm +/// short-circuits the whole leaf to `JObject::null()` (the value is absent ⇒ +/// the leaf is null) — any number of optional steps on the path nest. /// With `unwrap_last == false` the final path element composes directly — a -/// non-identity leaf's converter takes the final accessor's **full** return -/// type (`Option` included), so only the steps *before* it are nesting. An +/// non-identity leaf's converter takes the final step's **full** type +/// (`Option` included), so only the steps *before* it are nesting. An /// identity leaf (`unwrap_last == true`) delivers the reached value itself, so /// a final `Option` step unwraps too. -#[allow(clippy::too_many_arguments)] fn reach_leaf( qualify: &dyn Fn(&syn::Ident) -> syn::Path, - path: &[syn::Ident], - returns_option: &dyn Fn(&syn::Ident) -> bool, + path: &[PathStep], base: TokenStream, base_is_ref: bool, unwrap_last: bool, @@ -336,28 +543,24 @@ fn reach_leaf( } else { path.len().saturating_sub(1) }; - let mut e = if base_is_ref { base } else { quote!(&#base) }; - match (0..limit).find(|&i| returns_option(&path[i])) { - // No (more) `Option` nesting steps: compose the rest plainly. + let (mut e, lead) = project_leading_fields(&base, base_is_ref, path); + match (lead..limit).find(|&i| path[i].is_optional()) { + // No (more) optional nesting steps: compose the rest plainly. None => { - for a in path { - let m = qualify(a); - e = quote!(#m::#a(#e)); + for step in &path[lead..] { + e = compose_step(qualify, step, e); } body(e) } Some(k) => { - for a in &path[..k] { - let m = qualify(a); - e = quote!(#m::#a(#e)); + for step in &path[lead..k] { + e = compose_step(qualify, step, e); } - let opt_acc = &path[k]; - let opt_m = qualify(opt_acc); + let opt_e = compose_step(qualify, &path[k], e); let nested = format_ident!("__n{}", depth); let inner = reach_leaf( qualify, &path[k + 1..], - returns_option, quote!(#nested), true, unwrap_last, @@ -365,7 +568,7 @@ fn reach_leaf( body, ); quote! { - match #opt_m::#opt_acc(#e) { + match #opt_e { ::core::option::Option::Some(#nested) => { #inner } ::core::option::Option::None => jni::objects::JObject::null(), } @@ -394,13 +597,6 @@ pub(crate) fn encode_plan_leaves( value: &TokenStream, fail: &dyn Fn(TokenStream) -> TokenStream, ) -> (TokenStream, Vec) { - // A decomposed **sum** is the one plan whose leaves are not independent: - // only one group is live per value, so the whole list is emitted as ONE - // `match` rather than per-leaf expressions. Same contract, different - // emitter — see [`encode_sum_leaves`]. - if is_sum_leaves(&plan.leaves) { - return encode_sum_leaves(ext, registry, plan, obj_idents, value, fail); - } // Per-fn origin qualification: each accessor call is prefixed with the // module of the crate that defines it (multi-source bindings). let qualify = |id: &syn::Ident| -> syn::Path { ext.fn_module(registry, id) }; @@ -422,25 +618,80 @@ pub(crate) fn encode_plan_leaves( } } - // True when accessor `acc`'s return type is `Option<…>` (a nullable nesting - // step on a leaf's path). - let returns_option = |acc: &syn::Ident| -> bool { - registry.functions.get(acc).is_some_and(|(f, _)| match &f.sig.output { - syn::ReturnType::Type(_, t) => matches!( - &**t, - syn::Type::Path(tp) if tp.path.segments.last().is_some_and(|s| s.ident == "Option") - ), - _ => false, + let hoisted = bind_hoists(&qualify, &plan.hoists, value, by_ref); + let mut stmts = hoisted.stmts.clone(); + + // Reach a leaf off the innermost value form it sits under, with that + // prefix's steps already consumed, and say whether that form CONSUMED its + // value — so the leaf owns its field and may move it out rather than clone + // it. A leaf under no value form at all (a sibling `.field()` / + // `.field_self()`) still reaches from the value itself. + let rebase = + |leaf: &crate::api::core::unfold::UnfoldLeaf| -> (TokenStream, bool, Vec, bool) { + match hoisted.rebase(&leaf.path) { + Some((local, rest, consuming)) => (quote!(#local), false, rest, consuming), + None => (value.clone(), by_ref, leaf.path.clone(), false), + } + }; + + // A decomposed **sum** is the one shape whose leaves are not independent: + // only one group is live per value, so its whole segment — the selector + // leaf plus the group leaves that follow it — is emitted as ONE `match` + // instead of per-leaf expressions. A plan may carry several: a sum that IS + // the returned value is the degenerate case of one segment covering + // everything, while a value form contributes one per sum-typed field. + let sum_segments: Vec> = (0..n) + .filter(|&i| plan.leaves[i].source == crate::api::core::unfold::LeafSource::SumTag) + .map(|start| { + let end = (start + 1..n) + .take_while(|&i| plan.leaves[i].group.is_some()) + .last() + .map_or(start + 1, |i| i + 1); + start..end }) - }; + .collect(); + for seg in &sum_segments { + let leaf = &plan.leaves[seg.start]; + let (base, base_is_ref, path, _) = rebase(leaf); + // The value to `match` on. The selector's own path reaches the sum + // (empty when the sum IS the value); no step on it is optional, since + // an optional sum is refused where the leaves are built. + // + // A plain field chain is borrowed DIRECTLY (`&base.a.b`) rather than + // through the base (`&(&base).a.b`). The two are the same value, but + // the second borrows the base as a whole, which the borrow checker + // rejects once a sibling leaf has moved another field out of it — and + // borrowing this field while sibling fields move is exactly what a + // consuming value form does. + let (mut matched, lead) = project_leading_fields(&base, base_is_ref, &path); + for step in &path[lead..] { + matched = compose_step(&qualify, step, matched); + } + let (group_stmts, group_args) = encode_sum_group( + ext, + registry, + &plan.leaves[seg.clone()], + &obj_idents[seg.clone()], + matched, + fail, + ); + stmts.extend(group_stmts); + for (k, e) in group_args.into_iter().enumerate() { + arg_exprs[seg.start + k] = e; + } + } - let mut stmts = TokenStream::new(); - let mut order: Vec = (0..n).filter(|&i| !plan.leaves[i].identity).collect(); - order.extend((0..n).filter(|&i| plan.leaves[i].identity)); + let in_sum = |i: usize| sum_segments.iter().any(|s| s.contains(&i)); + let mut order: Vec = (0..n) + .filter(|&i| !plan.leaves[i].identity && !in_sum(i)) + .collect(); + order.extend((0..n).filter(|&i| plan.leaves[i].identity && !in_sum(i))); for idx in order { let leaf = &plan.leaves[idx]; let obj_ident = &obj_idents[idx]; + let (value, by_ref, path, consuming) = rebase(leaf); + let value = &value; let out_entry = registry.output_entry(&leaf.out_ty).unwrap_or_else(|| { panic!( "jnigen unfold: leaf `{}` has no registered output converter", @@ -510,25 +761,68 @@ pub(crate) fn encode_plan_leaves( TypeKey::from_type(&leaf.out_ty) ) }); + // The place this handle lives, when it is OURS to give away — the + // owned root, or a field of a CONSUMING value form, which handed its + // value over so its handle fields move out like every other field + // rather than being cloned through the borrowed converter (which + // would also demand a `Clone` the type need not have). + // + // Which it is was decided in the plan, not here: an owned `out_ty` + // IS the statement that this leaf owns what it reaches, and it is + // what selected the owning converter. `steps_are_movable` then says + // how to project it — a plain-field run directly, a trailing + // `Option` through the nullable branch's `match`, which moves the + // whole `Option` in rather than borrowing it. + let owned_place: Option = + if !matches!(leaf.out_ty, syn::Type::Reference(_)) && steps_are_movable(&path) { + let segs: Vec<&syn::Ident> = path.iter().map(PathStep::ident).collect(); + Some(quote!(#value #(.#segs)*)) + } else { + None + }; match proj.kind { ProjectionKind::Handle => { let handle_ident = format_ident!("__h{}", idx); - if leaf.path.is_empty() && !by_ref { - // Owned root, non-nullable by construction (nullable - // comes from path nesting): move into a Box, raw jlong. + if let (Some(place), false) = (&owned_place, leaf.nullable) { + // Ours, and always present: move into a Box, raw jlong. stmts.extend(quote! { let #obj_ident: jni::sys::jvalue = jni::sys::jvalue { - j: std::boxed::Box::into_raw(std::boxed::Box::new(#value)) + j: std::boxed::Box::into_raw(std::boxed::Box::new(#place)) as jni::sys::jlong, }; }); + } else if let Some(place) = &owned_place { + // Ours, behind an `Option`: match the option BY VALUE so + // the present handle is moved into its Box, boxed + // `java.lang.Long` when present / JVM null when absent. + // Matching `&place` here is what used to clone it back + // through the borrowed converter. + let box_fail = fail(quote!(__e.to_string())); + stmts.extend(bind_obj( + obj_ident, + quote! {{ + match #place { + ::core::option::Option::Some(__n) => { + let #handle_ident: jni::sys::jlong = + std::boxed::Box::into_raw(std::boxed::Box::new(__n)) + as jni::sys::jlong; + match ::prebindgen::lang::box_jlong(&mut env, #handle_ident) { + ::core::result::Result::Ok(__o) => __o, + ::core::result::Result::Err(__e) => { + #box_fail + } + } + } + ::core::option::Option::None => jni::objects::JObject::null(), + } + }}, + )); } else if !leaf.nullable { // Reached non-null handle: clone via the converter, // raw jlong (no Option steps on the path). let expr = reach_leaf( &qualify, - &leaf.path, - &returns_option, + &path, value.clone(), by_ref, true, @@ -551,8 +845,7 @@ pub(crate) fn encode_plan_leaves( let box_fail = fail(quote!(__e.to_string())); let expr = reach_leaf( &qualify, - &leaf.path, - &returns_option, + &path, value.clone(), by_ref, true, @@ -582,14 +875,13 @@ pub(crate) fn encode_plan_leaves( jni::sys::jvalue { j: #enc_ident } }} }; - if leaf.path.is_empty() && !by_ref { + if path.is_empty() && !by_ref { let expr = encode(value.clone()); stmts.extend(quote! { let #obj_ident: jni::sys::jvalue = #expr; }); } else if !leaf.nullable { let expr = reach_leaf( &qualify, - &leaf.path, - &returns_option, + &path, value.clone(), by_ref, true, @@ -601,8 +893,7 @@ pub(crate) fn encode_plan_leaves( let box_fail = fail(quote!(__e.to_string())); let expr = reach_leaf( &qualify, - &leaf.path, - &returns_option, + &path, value.clone(), by_ref, true, @@ -637,20 +928,38 @@ pub(crate) fn encode_plan_leaves( use crate::api::core::unfold::LeafSource; let reach = |body: &dyn Fn(TokenStream) -> TokenStream| -> TokenStream { match &leaf.source { - LeafSource::Accessor => reach_leaf( + LeafSource::Accessor => { + reach_leaf(&qualify, &path, value.clone(), by_ref, false, 0, body) + } + // Under a CONSUMING value form the leaf owns its field, so it + // is **moved** out; the whole point of consuming is that this + // clone disappears. Each field is read by exactly one leaf, so + // the partial moves are disjoint — but nothing may then borrow + // the local as a whole, which is why the reach below projects + // the field directly rather than through `&(&local)`. + LeafSource::Field if consuming && path.iter().all(PathStep::is_plain_field) => { + let segs: Vec<&syn::Ident> = path.iter().map(PathStep::ident).collect(); + body(quote!(#value #(.#segs)*)) + } + LeafSource::Field if path.iter().all(PathStep::is_plain_field) => { + let segs: Vec<&syn::Ident> = path.iter().map(PathStep::ident).collect(); + body(quote!(#value #(.#segs)*.clone())) + } + // A `.fields()` leaf reaches its field through the value-form + // accessor, so the path can be mixed: compose it like an + // accessor leaf — the final step composes directly, since the + // converter takes the field type as written (`Option` and all) + // — and clone the reached borrow, which is what a field leaf + // delivers. + LeafSource::Field => reach_leaf( &qualify, - &leaf.path, - &returns_option, + &path, value.clone(), by_ref, false, 0, - body, + &|reached| body(quote!((#reached).clone())), ), - LeafSource::Field => { - let segs = &leaf.path; - body(quote!(#value #(.#segs)*.clone())) - } // Group leaves never reach this walk: a plan carrying them is // routed to `encode_sum_leaves` at the top of this function, // because a variant payload has no path — it is bound by a diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index f8b274a7..da7b6be6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -83,11 +83,11 @@ pub(crate) fn synth_value_struct_leaves( ext: &JniGen, registry: &Registry, s: &syn::ItemStruct, - path_prefix: &[syn::Ident], + path_prefix: &[crate::api::core::unfold::PathStep], name_prefix: &str, depth: usize, ) -> Option> { - use crate::api::core::unfold::{LeafSource, UnfoldLeaf}; + use crate::api::core::unfold::{LeafSource, PathStep, UnfoldLeaf}; if depth > 16 { return None; } @@ -105,7 +105,10 @@ pub(crate) fn synth_value_struct_leaves( format!("{name_prefix}__{camel}") }; let mut path = path_prefix.to_vec(); - path.push(fname); + // The synthesizer declines `Option`-wrapped nesting below, so an + // intermediate step is never optional; a TERMINAL `Option` field is not + // a nesting step either (its own converter carries the nullability). + path.push(PathStep::field(fname, false)); // A projection field (opaque handle) or an enum field // is delivered with a transform the fixed builder can't forward yet. diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index 1dcca925..f7263255 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -50,14 +50,17 @@ pub(crate) fn synth_sum_leaves( }; let spec = SumSpec::from_item_enum(item_enum); - // The selector rides ahead of the groups it chooses between. Its `out_ty` - // documents the wire (`i32` → `jint` → Kotlin `Int`); nothing looks up a - // converter for it, because there is no value to convert — the emitter - // assigns the tag literal per arm. + // The selector rides ahead of the groups it chooses between, and carries + // **which sum** it selects over as its `out_ty` — that is how the emitter + // finds the enum to `match` when the sum is a field rather than the whole + // returned value. Nothing looks up a converter for it (`has_converter()` is + // false for a `SumTag`): there is no value to convert, the emitter assigns + // the tag literal per arm. Its wire is a `jint` by definition. + let enum_ident = &item_enum.ident; let mut leaves = vec![UnfoldLeaf { name: SUM_TAG_LEAF.to_string(), path: Vec::new(), - out_ty: syn::parse_quote!(i32), + out_ty: syn::parse_quote!(#enum_ident), identity: false, nullable: false, source: LeafSource::SumTag, @@ -96,27 +99,38 @@ pub(crate) fn is_sum_leaves(leaves: &[crate::api::core::unfold::UnfoldLeaf]) -> /// pattern's payload bindings, every other group from the same wire defaults an /// absent `Option` uses. /// +/// `leaves` is ONE sum's segment — its [`LeafSource::SumTag`] selector followed +/// by that selector's group leaves — with `obj_idents` the matching slice of +/// slot locals. `matched` is the expression to `match` on (a reference to the +/// value), which is the whole returned value when the sum IS the return, and +/// the reached field when a value form carries it. +/// /// The signature mirrors [`encode_plan_leaves`](super::encode_plan_leaves), and /// the two are interchangeable at the call site: both bind `obj_idents` and /// return the per-leaf `jvalue` argument expressions in leaf order. What differs /// is that a leaf here is not an independent expression — its slot exists in /// every arm and only one arm computes it. -pub(crate) fn encode_sum_leaves( +pub(crate) fn encode_sum_group( ext: &JniGen, registry: &Registry, - plan: &crate::api::core::unfold::UnfoldPlan, + leaves: &[crate::api::core::unfold::UnfoldLeaf], obj_idents: &[syn::Ident], - value: &TokenStream, + matched: TokenStream, fail: &dyn Fn(TokenStream) -> TokenStream, ) -> (TokenStream, Vec) { use crate::api::core::unfold::LeafSource; - let leaves = &plan.leaves; - // Qualified path of the source enum, for the arm patterns. - let ident = bare_path_ident(&plan.source).unwrap_or_else(|| { + // Which sum this is comes from the selector leaf, not from the plan's + // source: the plan's source is the *containing* value when the sum is a + // field of a value form. + let tag_leaf = leaves + .iter() + .find(|l| l.source == LeafSource::SumTag) + .expect("a sum segment carries its selector leaf"); + let ident = bare_path_ident(&tag_leaf.out_ty).unwrap_or_else(|| { panic!( - "jnigen sum unfold: source `{}` is not a path type", - TypeKey::from_type(&plan.source) + "jnigen sum unfold: selector type `{}` is not a path type", + TypeKey::from_type(&tag_leaf.out_ty) ) }); let module = ext.fn_module(registry, &ident); @@ -268,14 +282,6 @@ pub(crate) fn encode_sum_leaves( }) .collect(); - // A borrowed value is matched as-is; an owned one is matched by reference - // so each payload can be cloned out of it (the same reach a struct field - // leaf uses). - let matched = if plan.by_ref { - value.clone() - } else { - quote!(&#value) - }; let stmts = quote! { #decls match #matched { #(#arms)* } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 0106e385..0686777c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -210,13 +210,23 @@ pub(crate) fn emit_jni_function_wrapper_with_callee( let uplan = unfold_plan.expect("is_convert ⇒ plan"); let leaf = &uplan.leaves[0]; let by_ref = uplan.by_ref; + // One derivation, shared with the multi-leaf encoder — the value forms + // are bound by the same [`bind_hoists`] and the leaf reached by the + // same [`reach_leaf_flat`]. Deriving either a second time here is what + // let the two drift apart: this shortcut used to compose its reach + // straight off the raw value, which for a value form declared with + // `.fields_self_into(..)` emitted `f(&v)` against a by-value receiver. + let qualify = |id: &syn::Ident| -> syn::Path { ext.fn_module(registry, id) }; let compose = |base: TokenStream, base_is_ref: bool| -> TokenStream { - let mut e = if base_is_ref { base } else { quote!(&#base) }; - for a in &leaf.path { - let m = ext.fn_module(registry, a); - e = quote!(#m::#a(#e)); - } - e + let hoisted = bind_hoists(&qualify, &uplan.hoists, &base, base_is_ref); + let stmts = &hoisted.stmts; + let reached = match hoisted.rebase(&leaf.path) { + Some((local, rest, consuming)) => { + reach_leaf_flat(&qualify, leaf, &rest, quote!(#local), false, consuming) + } + None => reach_leaf_flat(&qualify, leaf, &leaf.path, base, base_is_ref, false), + }; + quote!({ #stmts #reached }) }; match &uplan.shape { UnfoldShape::Optional((), _) => { diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index accf5e6b..94b19d28 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -22,7 +22,7 @@ //! determinism is a checked invariant rather than a convention. use super::*; -use crate::api::core::unfold::{dedup_names, DeconId, UnfoldPlan}; +use crate::api::core::unfold::{dedup_names, DeconId, LeafSource, UnfoldPlan}; /// The JVM-visible single method name of every generated callback interface. pub(crate) const IFACE_METHOD: &str = "run"; @@ -1141,15 +1141,54 @@ pub(crate) fn callback_iface_spec( }); } else { // Accessor-plan arg: each leaf is its own passthrough group, so - // the user callback still sees the flattened leaves (unchanged). - for n in &leaf_names { - groups.push(GroupDesc { - name: n.clone(), - typed: None, - reassemble: None, - imports: Vec::new(), - leaf_count: 1, - }); + // the user callback still sees the flattened leaves (unchanged) + // — EXCEPT a sum segment, whose selector and group slots are one + // value and collapse into a single typed parameter rebuilt by a + // `when` over the tag. Handing those slots over raw would defeat + // the `sealed_class!` the sum was declared as. + let mut k = 0usize; + while k < plan.leaves.len() { + let leaf = &plan.leaves[k]; + let seg = if leaf.source == LeafSource::SumTag { + (k + 1..plan.leaves.len()) + .take_while(|&j| plan.leaves[j].group.is_some()) + .last() + .map_or(k + 1, |j| j + 1) + } else { + k + 1 + }; + if leaf.source == LeafSource::SumTag { + any_fixed = true; + let fqn = ext.kotlin_fqn(&TypeKey::from_type(&leaf.out_ty))?; + let (reassemble, imports) = fixed_reassembly( + ext, + registry, + &leaf.out_ty, + &plan.leaves[k..seg], + &fqn, + ); + groups.push(GroupDesc { + // The tag leaf is named `__tag`; the value it + // selects over is that field. + name: leaf_names[k] + .strip_suffix(&format!("__{SUM_TAG_LEAF}")) + .unwrap_or(&leaf_names[k]) + .to_string(), + typed: Some(kt::KtType::cls(fqn.to_string())), + reassemble: Some(reassemble), + imports, + leaf_count: seg - k, + }); + } else { + groups.push(GroupDesc { + name: leaf_names[k].clone(), + typed: None, + reassemble: None, + imports: Vec::new(), + leaf_count: 1, + }); + } + k = seg; } } } else { diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs index c3e136de..13f1b2e6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs @@ -33,6 +33,7 @@ mod niches; mod sealed; mod snapshots; mod symbols; +mod value_form; mod values; /// Build a `TypeEntry` for use in tests. The function body is not diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs index 5e92634d..3b789fe9 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs @@ -1125,6 +1125,115 @@ fn sum_return_group_can_own_a_handle() { ); } +/// The same handle payload as a **data-class field** — the position the +/// return/callback coverage above does not reach, and the one +/// `ReplyStruct { result: ReplyResult, .. }` needs (both `ReplyResult` +/// alternatives carry handles). +/// +/// The group slot stays the raw `jlong` and the live arm wraps it into the +/// typed handle class, exactly as in return position — the parent's own +/// `fromParts` inlining the `when`. +/// +/// Two consequences of this position, both asserted below so they cannot +/// change silently: +/// +/// * The container is **not** `AutoCloseable` — `destructible()` matches only +/// a `Projection` field, never a `Sum` one. That follows the documented +/// ownership rule for sum payloads ("who closes a handle payload: the +/// receiver"), but it does differ from a plain handle field, which *does* +/// make its class closeable and cascades. The handle stays reachable and +/// closeable through the variant (`(h.outcome as Lookup.Found).v0.close()`); +/// what is absent is the cascade. +/// * A sum field takes its parent off the fixed-builder path onto the +/// whole-value `fromParts` bridge (`synth_value_struct_leaves` declines +/// `TypeKind::Sum`), so the value costs a JVM object — a slower shape, not a +/// broken one. +#[test] +fn a_data_class_field_may_be_a_sum_carrying_a_handle() { + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct Probe { + value: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Enum(syn::parse_quote!( + pub enum Lookup { + Absent, + Found(Probe), + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct Holder { + pub id: i64, + pub outcome: Lookup, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn holder_new(id: i64) -> Holder { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new().set_package_prefix("io.test.jni").package( + crate::package!() + .class(crate::ptr_class!(Probe)) + .class(crate::sealed_class!(Lookup)) + .class(crate::data_class!(Holder)) + .fun(crate::fun!(holder_new)), + ); + let dir = unique_test_dir("jnigen_sum_handle_field"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + let kotlin = gen + .write_kotlin(&dir.join("kotlin")) + .expect("write_kotlin") + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + + // The tag keeps the `__` marker; a group slot is prefixed with its field + // by the single-underscore nesting convention (`mode_periodicQueries_period`). + assert!( + kotlin.contains("outcome__tag: Int") && kotlin.contains("outcome_found_v0: Long"), + "the selector plus a raw-pointer group slot, both prefixed by the field:\n{kotlin}" + ); + assert!( + kotlin.contains("Lookup.Found(Probe(outcome_found_v0))"), + "the parent's fromParts inlines the `when` and wraps the pointer:\n{kotlin}" + ); + assert!( + kotlin.contains("public data class Holder(val id: Long, val outcome: Lookup)"), + "the field surfaces as the typed sum:\n{kotlin}" + ); + assert!( + !kotlin.contains("Holder(val id: Long, val outcome: Lookup) : AutoCloseable"), + "a sum-carried handle is the RECEIVER's to close — the container does \ + not cascade, unlike a plain handle field:\n{kotlin}" + ); + assert!( + rust.contains("Lookup::Found") && rust.contains("Lookup::Absent"), + "Rust matches the field's sum, filling every group's slots:\n{rust}" + ); +} + /// TWO sums in one callback signature: each contributes its own selector, so /// the signature-wide dedup renames the second to `tag2` — and the reassembly /// expressions must follow it, including the `$tag` Kotlin string template in diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs new file mode 100644 index 00000000..712edd0f --- /dev/null +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -0,0 +1,1626 @@ +//! `expand_return!(T).fields(fields!(t_to_struct))` — deriving a type's output +//! boundary from its **value form** (the struct gathering its own accessors) +//! instead of restating the field list, which is how the two drift apart. +//! +//! The contract each test below pins down: `.fields()` is `.field()` applied to +//! every struct field, so a field still crosses by **its own type's** default +//! output boundary. + +use super::*; + +/// The value-form fixture: a `ZSample` handle whose fields cover the shapes +/// that decide behaviour — a handle field with its own `expand_return!` +/// (decomposed, not handed over raw), a scalar, an `Option`, a +/// non-optional nested data class (inlined), and an `Option`. +fn value_form_items() -> Vec<(syn::Item, crate::SourceLocation)> { + let loc = myflat_loc(); + vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZStamp { + pub secs: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZOrigin { + pub node: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZSampleStruct { + pub key_expr: ZKeyExpr, + pub payload: ZBytes, + pub express: bool, + pub stamp: Option, + pub origin: ZOrigin, + pub attachment: Option, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_to_struct(s: &ZSample) -> ZSampleStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_keyexpr_as_str(k: &ZKeyExpr) -> &str { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_sub(cb: impl Fn(ZSample) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc, + ), + ] +} + +/// Build the fixture through `JniGen`, letting the caller adjust the +/// `ZSample` boundary decl. Returns the generated Rust + the joined Kotlin. +fn value_form_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> (String, String) { + let registry = Registry::::from_items(value_form_items()).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .class(crate::ptr_class!(ZKeyExpr)) + .class(crate::ptr_class!(ZBytes)) + .class(crate::data_class!(ZStamp)) + .class(crate::data_class!(ZOrigin)) + .fun(crate::fun!(z_sample_sub)), + ) + // A KeyExpr crosses as its string, never as a handle — the rule a + // `.fields()` expansion has to keep honouring for the `key_expr` field. + .expand(crate::expand_return!(ZKeyExpr).field(crate::fun!(z_keyexpr_as_str))) + .expand(decl); + + let dir = unique_test_dir(tag); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + let kotlin = gen + .write_kotlin(&dir.join("kotlin")) + .expect("write_kotlin") + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + (rust, kotlin) +} + +/// The headline: `.fields(fields!(f))` produces the leaves the fields' own +/// boundaries dictate, NOT one leaf per field. +/// +/// Read the expected signature field by field — each is a different rule: +/// `key_expr` splices `ZKeyExpr`'s own decl (a String, not a handle); +/// `express` is a scalar; `stamp` behind `Option` stays one data-class leaf; +/// `origin` is a non-optional data class and INLINES (`origin__node`); +/// `payload` / `attachment` have no decl of their own, so they stay handles. +#[test] +fn fields_expand_by_each_field_s_own_boundary() { + let (_, kotlin) = value_form_gen( + "jnigen_vf_basic", + crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_struct)), + ); + assert!( + kotlin.contains("keyExpr__zKeyexprAsStr: String"), + "a field whose type has its own expand_return! is decomposed by it, \ + not handed over as a handle:\n{kotlin}" + ); + assert!( + kotlin.contains("express: Boolean"), + "a scalar field is one leaf:\n{kotlin}" + ); + assert!( + kotlin.contains("stamp: ZStamp?"), + "an Option field stays ONE leaf (its converter builds it):\n{kotlin}" + ); + assert!( + kotlin.contains("origin__node: Long"), + "a non-optional nested data class INLINES into its own fields:\n{kotlin}" + ); + assert!( + kotlin.contains("payload: ZBytes") && kotlin.contains("attachment: ZBytes?"), + "a handle field with no boundary decl stays a handle, nullable under Option:\n{kotlin}" + ); +} + +/// The value form is called ONCE per delivery. Without the hoist each field +/// would rebuild the whole struct — cloning every field once per leaf — which +/// is exactly the per-message cost `expand_return` exists to avoid. +#[test] +fn the_value_form_accessor_is_called_once() { + let (rust, _) = value_form_gen( + "jnigen_vf_hoist", + crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_struct)), + ); + let calls = rust.matches("z_sample_to_struct").count(); + assert_eq!( + calls, 1, + "the value form is bound to one local and every leaf reaches off it; \ + found {calls} calls in:\n{rust}" + ); +} + +/// An `Option` field with nothing decomposed below it crosses **whole** — its +/// own converter takes the `Option`. Unwrapping it to reach the inner value +/// would hand `ZStamp` to a converter typed `Option`: a mismatch the +/// Kotlin signature cannot show, because it reads `ZStamp?` either way. +#[test] +fn an_optional_field_reaches_its_converter_whole() { + let (rust, _) = value_form_gen( + "jnigen_vf_opt", + crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_struct)), + ); + for field in ["stamp", "attachment"] { + assert!( + rust.contains(&format!(".{field}.clone()")), + "`{field}` must be cloned whole, not matched open:\n{rust}" + ); + assert!( + !rust.contains(&format!(".{field} {{")), + "`{field}` has nothing decomposed below it, so it is not a nesting \ + step — no `match` on it:\n{rust}" + ); + } +} + +/// A hand-written field list and the derived one are the SAME leaves — the +/// property that makes adopting `.fields()` a no-op on the wire, and the whole +/// reason it can be trusted to replace a list that has drifted. +#[test] +fn deriving_matches_the_equivalent_hand_written_list() { + let items = value_form_items(); + let accessors: Vec<(syn::Item, crate::SourceLocation)> = { + let loc = myflat_loc(); + vec![ + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_key_expr(s: &ZSample) -> &ZKeyExpr { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_express(s: &ZSample) -> bool { + unimplemented!() + } + )), + loc, + ), + ] + }; + + let leaves_of = |decl: crate::lang::ExpandReturnDecl, + extra: Vec<(syn::Item, crate::SourceLocation)>| + -> Vec<(String, String)> { + let mut all = items.clone(); + all.extend(extra); + let registry = Registry::::from_items(all).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .class(crate::ptr_class!(ZKeyExpr)) + .class(crate::ptr_class!(ZBytes)) + .class(crate::data_class!(ZStamp)) + .class(crate::data_class!(ZOrigin)) + .fun(crate::fun!(z_sample_sub)), + ) + .expand(crate::expand_return!(ZKeyExpr).field(crate::fun!(z_keyexpr_as_str))) + .expand(decl); + let gen = registry.resolve(jni).expect("resolve"); + gen.registry() + .callback_arg_plans + .values() + .flat_map(|p| p.leaves.iter()) + .map(|l| (l.name.clone(), l.out_ty.to_token_stream().to_string())) + .collect() + }; + + // The two fields the hand-written list can state with real accessors. + let derived = leaves_of( + crate::expand_return!(ZSample).fields( + crate::fields!(z_sample_to_struct) + .name("key_expr", "keyExpr") + .name("express", "express"), + ), + vec![], + ); + let by_hand = leaves_of( + crate::expand_return!(ZSample) + .field(crate::fun!(z_sample_key_expr).name("keyExpr")) + .field(crate::fun!(z_sample_express).name("express")), + accessors, + ); + + let take = |v: &[(String, String)], n: &str| -> Option<(String, String)> { + v.iter().find(|(name, _)| name.starts_with(n)).cloned() + }; + for prefix in ["keyExpr", "express"] { + assert_eq!( + take(&derived, prefix).map(|(n, _)| n), + take(&by_hand, prefix).map(|(n, _)| n), + "derived and hand-written leaves must agree on `{prefix}`\n\ + derived: {derived:?}\nby hand: {by_hand:?}" + ); + } +} + +/// A per-field override replaces that field's type default wholesale — here +/// keeping the raw `ZKeyExpr` handle instead of its declared string form. +#[test] +fn a_per_field_override_replaces_the_type_default() { + let (_, kotlin) = value_form_gen( + "jnigen_vf_override", + crate::expand_return!(ZSample).fields( + crate::fields!(z_sample_to_struct) + .field("key_expr", crate::expand_return!(ZKeyExpr).field_self()), + ), + ); + assert!( + kotlin.contains("keyExpr: ZKeyExpr"), + "the override wins over ZKeyExpr's type-level decl:\n{kotlin}" + ); + assert!( + !kotlin.contains("keyExpr__zKeyexprAsStr"), + "the overridden field must NOT also carry the type default:\n{kotlin}" + ); +} + +/// A rename keys on the Rust field ident and reaches an inlined nested field +/// through its dotted path. +#[test] +fn a_field_can_be_renamed_including_a_nested_one() { + let (_, kotlin) = value_form_gen( + "jnigen_vf_rename", + crate::expand_return!(ZSample).fields( + crate::fields!(z_sample_to_struct) + .name("express", "fast") + .name("origin.node", "nodeId"), + ), + ); + assert!( + kotlin.contains("fast: Boolean"), + "a renamed field uses the literal name:\n{kotlin}" + ); + assert!( + kotlin.contains("origin__nodeId: Long"), + "a nested field is renamed through its dotted path, keeping the prefix:\n{kotlin}" + ); +} + +/// `.fields()` mixes with the other declarators — the value form's fields +/// *and* the live handle, which is the `Query`-style shape. +#[test] +fn fields_mixes_with_field_self() { + let (_, kotlin) = value_form_gen( + "jnigen_vf_mixed", + crate::expand_return!(ZSample) + .fields(crate::fields!(z_sample_to_struct)) + .field_self(), + ); + assert!( + kotlin.contains("express: Boolean") && kotlin.contains("handle: ZSample"), + "the derived fields and the identity leaf are delivered together:\n{kotlin}" + ); +} + +/// A **sum** field decomposes into its selector plus one group per +/// alternative, right there among its sibling fields — a sum has no +/// whole-value converter, so this is the only way it can cross at all. The +/// `ReplyStruct { result: ReplyResult, .. }` shape. +fn sum_field_gen(tag: &str) -> (String, String) { + let loc = myflat_loc(); + let mut items = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum ZOutcome { + Empty, + Ok(ZBytes), + Failed(String), + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZReplyStruct { + pub result: ZOutcome, + pub seq: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_reply_to_struct(r: &ZReply) -> ZReplyStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + items.push(( + syn::Item::Fn(syn::parse_quote!( + pub fn z_reply_sub(cb: impl Fn(ZReply) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc, + )); + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZReply)) + .class(crate::ptr_class!(ZBytes)) + .class(crate::sealed_class!(ZOutcome)) + .fun(crate::fun!(z_reply_sub)), + ) + .expand(crate::expand_return!(ZReply).fields(crate::fields!(z_reply_to_struct))); + + let dir = unique_test_dir(tag); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + let kotlin = gen + .write_kotlin(&dir.join("kotlin")) + .expect("write_kotlin") + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + (rust, kotlin) +} + +#[test] +fn a_sum_field_crosses_as_its_selector_and_groups() { + let (rust, kotlin) = sum_field_gen("jnigen_vf_sum"); + assert!( + kotlin.contains("result__tag: Int"), + "the sum field contributes its selector, prefixed by the field:\n{kotlin}" + ); + assert!( + kotlin.contains("result__ok_v0: Long") && kotlin.contains("result__failed_v0: String?"), + "one group slot per alternative payload, object slots nullable \ + (an inert group arrives as null):\n{kotlin}" + ); + assert!( + kotlin.contains("seq: Long"), + "a sibling field is unaffected:\n{kotlin}" + ); + assert!( + kotlin.contains("ZOutcome.Ok(") && kotlin.contains("ZOutcome.Failed("), + "the receiver rebuilds the live alternative from the tag:\n{kotlin}" + ); + assert!( + rust.contains("myflat::ZOutcome::Ok") && rust.contains("myflat::ZOutcome::Failed"), + "Rust matches the sum once, filling every group's slots:\n{rust}" + ); +} + +/// A sum's slots sit at a FIXED position in the leaf list, so the two shapes +/// that would move or repeat them are refused by name rather than mis-emitted: +/// `Vec` has variable arity, and `Option` would need a present flag +/// beside the tag that an output leaf list cannot carry. +#[test] +fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { + let loc = myflat_loc(); + let build = |field_ty: syn::Type| { + let items = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum ZOutcome { + Empty, + Failed(String), + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZReplyStruct { + pub result: #field_ty, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_reply_to_struct(r: &ZReply) -> ZReplyStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_reply_sub(cb: impl Fn(ZReply) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZReply)) + .class(crate::sealed_class!(ZOutcome)) + .fun(crate::fun!(z_reply_sub)), + ) + .expand(crate::expand_return!(ZReply).fields(crate::fields!(z_reply_to_struct))); + let dir = unique_test_dir("jnigen_vf_sum_reject"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let _ = registry + .resolve(jni) + .map(|g| g.write_rust(dir.join("g.rs"))); + }; + + for (ty, want) in [ + (syn::parse_quote!(Vec), "variable arity"), + (syn::parse_quote!(Option), "present flag"), + ] { + let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| build(ty))) + .expect_err("a sum behind Option/Vec must be rejected"); + let msg = err + .downcast_ref::() + .cloned() + .or_else(|| err.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_default(); + assert!(msg.contains(want), "expected `{want}` in: {msg}"); + assert!( + msg.contains("ZReplyStruct.result"), + "the message names the offending field: {msg}" + ); + } +} + +/// Naming a field the value form does not have is the very drift this +/// declarator exists to catch, so it is an error rather than a silent no-op. +#[test] +fn an_adjustment_naming_an_unknown_field_is_an_error() { + let build = |decl: crate::lang::FieldsDecl| { + let registry = Registry::::from_items(value_form_items()).expect("index"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .class(crate::ptr_class!(ZKeyExpr)) + .class(crate::ptr_class!(ZBytes)) + .class(crate::data_class!(ZStamp)) + .class(crate::data_class!(ZOrigin)) + .fun(crate::fun!(z_sample_sub)), + ) + .expand(crate::expand_return!(ZKeyExpr).field(crate::fun!(z_keyexpr_as_str))) + .expand(crate::expand_return!(ZSample).fields(decl)); + let dir = unique_test_dir("jnigen_vf_unknown"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let _ = registry + .resolve(jni) + .map(|g| g.write_rust(dir.join("g.rs"))); + }; + + for decl in [ + crate::fields!(z_sample_to_struct).name("kex", "kex"), + crate::fields!(z_sample_to_struct) + .field("kex", crate::expand_return!(ZKeyExpr).field_self()), + ] { + let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| build(decl))) + .expect_err("an unknown field name must be rejected"); + let msg = err + .downcast_ref::() + .cloned() + .or_else(|| err.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_default(); + assert!(msg.contains("kex"), "the message names the field: {msg}"); + assert!( + msg.contains("ZSampleStruct"), + "the message names the value form: {msg}" + ); + } +} + +/// One value form states the whole field set: a second `.fields()` would make +/// the leaf order depend on declaration order for no gain. +#[test] +#[should_panic(expected = "already expands a value form")] +fn a_second_value_form_is_an_error() { + let _ = crate::expand_return!(ZSample) + .fields(crate::fields!(z_sample_to_struct)) + .fields(crate::fields!(z_sample_to_struct)); +} + +/// Repeating an adjustment for one field is a declaration bug — the complete +/// set rule, same as `.expand_param` / `.field`. +#[test] +#[should_panic(expected = "already has an override")] +fn a_repeated_override_is_an_error() { + let _ = crate::fields!(z_sample_to_struct) + .field("key_expr", crate::expand_return!(ZKeyExpr).field_self()) + .field("key_expr", crate::expand_return!(ZKeyExpr).field_self()); +} + +/// `"__"` is the reserved chain separator, so an author-supplied rename may +/// not smuggle one in and forge a nesting that isn't there. +#[test] +#[should_panic(expected = "reserved")] +fn a_rename_may_not_contain_the_chain_separator() { + let _ = crate::fields!(z_sample_to_struct).name("express", "a__b"); +} + +// ── Review findings on #221 ────────────────────────────────────────────────── + +/// A **single-leaf** value form takes the `Delivery::Return` shortcut, whose +/// reach is composed separately from the multi-leaf encoder +/// (`emit/wrapper.rs`'s `is_convert` path). That path must still give a plain +/// field leaf the owned value its converter is typed for: composing the field +/// as a borrow feeds `&i64` to the `i64` converter, and for a non-`Copy` field +/// borrows out of the temporary the value-form call returned. +#[test] +fn a_single_leaf_value_form_delivers_an_owned_field() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZOneStruct { + pub label: String, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_one_to_struct(o: &ZOne) -> ZOneStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_one_make(n: i64) -> ZOne { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZOne)) + .fun(crate::fun!(z_one_make)), + ) + .expand(crate::expand_return!(ZOne).fields(crate::fields!(z_one_to_struct))); + let dir = unique_test_dir("jnigen_vf_single"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + rust.contains(".label).clone()"), + "the single leaf is CLONED out of the value form, matching the owned \ + `String` its converter takes — composing it as a borrow would feed \ + `&String` to a `String` converter:\n{rust}" + ); +} + +/// The same shortcut with a CONSUMING form. It composed its reach straight off +/// the raw value and never looked at the plan's hoists, so it emitted +/// `z_one_into_struct(&__cvsrc)` against a by-value receiver — Rust that does +/// not compile in the consumer's crate — and then cloned a field it owns. Both +/// paths now bind hoists with the same `bind_hoists`, so neither can drift from +/// the other again. +#[test] +fn a_single_leaf_consuming_value_form_moves_its_field() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZOneStruct { + pub label: String, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_one_into_struct(o: ZOne) -> ZOneStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_one_make(n: i64) -> ZOne { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZOne)) + .fun(crate::fun!(z_one_make)), + ) + .expand(crate::expand_return!(ZOne).fields_self_into(crate::fields!(z_one_into_struct))); + let dir = unique_test_dir("jnigen_vf_single_consume"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + rust.contains("z_one_into_struct(__cvsrc)") && !rust.contains("z_one_into_struct(&"), + "the by-value accessor is handed the value, not a borrow of it:\n{rust}" + ); + assert!( + rust.contains(".label") && !rust.contains(".label).clone()"), + "and the field it owns is MOVED out, not cloned:\n{rust}" + ); +} + +/// A handle field of a consuming value form is the value form's field like any +/// other: the form gave its value away, so the handle **moves** into its Box +/// rather than being cloned through the borrowed-opaque converter — which also +/// stops `.fields_self_into(..)` from silently requiring a `Clone` the handle type +/// need not have. +/// +/// The identity branch computed `consuming` and then returned before using it, +/// so every reached handle took the clone arm; only a handle at the owned ROOT +/// (empty path) moved. +#[test] +fn a_handle_field_of_a_consuming_value_form_moves() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZEnvelopeStruct { + pub child: ZChild, + pub tag: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_envelope_into_struct(e: ZEnvelope) -> ZEnvelopeStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_envelope_sub(cb: impl Fn(ZEnvelope) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZEnvelope)) + .class(crate::ptr_class!(ZChild)) + .fun(crate::fun!(z_envelope_sub)), + ) + .expand(crate::expand_return!(ZChild).field_self()) + .expand( + crate::expand_return!(ZEnvelope) + .fields_self_into(crate::fields!(z_envelope_into_struct)), + ); + let dir = unique_test_dir("jnigen_vf_handle_field_consume"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + rust.contains("Box::new(__vf0.child)"), + "the handle field is MOVED into its Box:\n{rust}" + ); + assert!( + !rust.contains("&__vf0.child"), + "and is not handed to the borrowed-opaque converter, which would clone \ + it:\n{rust}" + ); +} + +/// The cross-product of the two above: a value form whose SOLE field is a +/// handle. That takes the single-leaf `Delivery::Return` shortcut with an +/// *identity* leaf, so neither the multi-leaf handle test (which is a callback +/// plan) nor the single-leaf `String` test (a `Field` leaf) covered it, and the +/// shortcut handed the borrowed converter `&__vf0.child`. +/// +/// The fix is in the PLAN, not in the shortcut: an identity leaf under a +/// consuming form resolves its `out_ty` to the OWNED type, which both selects +/// the owning converter and tells every emitter it may move. +#[test] +fn a_sole_handle_field_of_a_consuming_value_form_moves() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZSingleEnvelopeStruct { + pub child: ZChild, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_single_envelope_into_struct(e: ZSingleEnvelope) -> ZSingleEnvelopeStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_single_envelope_make() -> ZSingleEnvelope { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSingleEnvelope)) + .class(crate::ptr_class!(ZChild)) + .fun(crate::fun!(z_single_envelope_make)), + ) + .expand(crate::expand_return!(ZChild).field_self()) + .expand( + crate::expand_return!(ZSingleEnvelope) + .fields_self_into(crate::fields!(z_single_envelope_into_struct)), + ); + let dir = unique_test_dir("jnigen_vf_sole_handle_consume"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + rust.contains("z_single_envelope_into_struct(__cvsrc)"), + "the by-value accessor is handed the value:\n{rust}" + ); + assert!( + rust.contains("__vf0.child") && !rust.contains("&__vf0.child"), + "and the sole handle field is MOVED out, not borrowed into the \ + cloning converter:\n{rust}" + ); +} + +/// An `Option` field is the same claim behind an `Option` — and the +/// commonest shape there is (`SampleStruct.attachment`). The consuming form +/// owns the whole `Option`, so it is matched BY VALUE and the present handle +/// moves into its Box; only the *reach* differs from the non-optional case, not +/// the ownership. +#[test] +fn an_optional_handle_field_of_a_consuming_value_form_moves() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZOptionalEnvelopeStruct { + pub child: Option, + pub tag: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_optional_envelope_into_struct( + e: ZOptionalEnvelope, + ) -> ZOptionalEnvelopeStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_optional_envelope_sub( + cb: impl Fn(ZOptionalEnvelope) + Send + Sync + 'static, + ) { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZOptionalEnvelope)) + .class(crate::ptr_class!(ZChild)) + .fun(crate::fun!(z_optional_envelope_sub)), + ) + .expand(crate::expand_return!(ZChild).field_self()) + .expand( + crate::expand_return!(ZOptionalEnvelope) + .fields_self_into(crate::fields!(z_optional_envelope_into_struct)), + ); + let dir = unique_test_dir("jnigen_vf_optional_handle_consume"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + rust.contains("match __vf0.child") && !rust.contains("&__vf0.child"), + "the `Option` is matched BY VALUE, not borrowed:\n{rust}" + ); + assert!( + rust.contains("Box::new(__n)"), + "and the present handle is MOVED into its Box rather than cloned \ + through the borrowed converter:\n{rust}" + ); +} + +/// The cross-product of the two above: a value form whose SOLE field is an +/// `Option`. Delivery was chosen on leaf COUNT alone, so this landed on +/// the flat `Delivery::Return` path — which has no `None` arm, and whose +/// `convert_out_ty` names the leaf's own type rather than an optional of it, so +/// it composed `&(&__vf0).child` into a converter typed for `ZChild`. +/// +/// A nullable leaf now goes to callback delivery, which has that arm already. +/// Absence is a delivery question, not an ownership one — making `out_ty` owned +/// says who frees the handle, not whether there is one. +#[test] +fn a_sole_optional_handle_field_takes_callback_delivery() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZOptionalSingleStruct { + pub child: Option, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_optional_single_into_struct(e: ZOptionalSingle) -> ZOptionalSingleStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_optional_single_make() -> ZOptionalSingle { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZOptionalSingle)) + .class(crate::ptr_class!(ZChild)) + .fun(crate::fun!(z_optional_single_make)), + ) + .expand(crate::expand_return!(ZChild).field_self()) + .expand( + crate::expand_return!(ZOptionalSingle) + .fields_self_into(crate::fields!(z_optional_single_into_struct)), + ); + let dir = unique_test_dir("jnigen_vf_sole_optional_handle"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + !rust.contains("&(&__vf0).child") && !rust.contains("&__vf0.child"), + "the optional field is never composed as a borrow — that is what the \ + flat return path did, handing `&Option` to a `ZChild` \ + converter:\n{rust}" + ); + assert!( + rust.contains("match __vf0.child") && rust.contains("Box::new(__n)"), + "it takes callback delivery, whose `None` arm exists, and the present \ + handle still moves:\n{rust}" + ); +} + +/// A ROOT identity leaf owns its value with no value form in sight — a plain +/// `-> ZChild` return under the type-level `expand_return!(ZChild).field_self()` +/// that exists so the same boundary can be spliced as a value-form field. The +/// flat return path tied its move to the rebased hoist's `consuming` flag, +/// which is `false` when there is no hoist, so it emitted `&__cvsrc` into the +/// owning `ZChild`-to-jlong converter. +/// +/// Ownership is not "a consuming form gave it to me" — that is one of its two +/// sources. For an identity leaf the plan already states it in `out_ty`, so the +/// emitter reads it there rather than re-deriving it from the hoist. The +/// `Option` return is the same value inside a `map` closure. +#[test] +fn an_owned_root_identity_moves_without_any_value_form() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_root_child_make() -> ZChild { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_root_child_maybe() -> Option { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZChild)) + .fun(crate::fun!(z_root_child_make)) + .fun(crate::fun!(z_root_child_maybe)), + ) + .expand(crate::expand_return!(ZChild).field_self()); + let dir = unique_test_dir("jnigen_vf_root_identity"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + !rust.contains("&__cvsrc") && !rust.contains("&__inner"), + "an owned root is MOVED into its converter, not borrowed — the owning \ + converter takes `ZChild`, not `&ZChild`:\n{rust}" + ); +} + +/// A per-field `.field(name, expand_return!(T))` override states the field's +/// type, so it has to be checked against the field. Otherwise the override +/// silently survives an upstream field-type change — which is exactly the drift +/// this declarator exists to catch — and two same-shaped handle types are +/// interchangeable by accident. +#[test] +fn a_per_field_override_must_name_the_field_s_own_type() { + let build = || { + let registry = Registry::::from_items(value_form_items()).expect("index"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .class(crate::ptr_class!(ZKeyExpr)) + .class(crate::ptr_class!(ZBytes)) + .class(crate::data_class!(ZStamp)) + .class(crate::data_class!(ZOrigin)) + .fun(crate::fun!(z_sample_sub)), + ) + .expand(crate::expand_return!(ZKeyExpr).field(crate::fun!(z_keyexpr_as_str))) + .expand( + crate::expand_return!(ZSample).fields( + // `key_expr` is a `ZKeyExpr`, not a `ZBytes`. + crate::fields!(z_sample_to_struct) + .field("key_expr", crate::expand_return!(ZBytes).field_self()), + ), + ); + let dir = unique_test_dir("jnigen_vf_ovr_ty"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let _ = registry + .resolve(jni) + .map(|g| g.write_rust(dir.join("g.rs"))); + }; + let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(build)) + .expect_err("a mistyped override must be rejected"); + let msg = err + .downcast_ref::() + .cloned() + .or_else(|| err.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_default(); + assert!( + msg.contains("key_expr"), + "the message names the field: {msg}" + ); + assert!( + msg.contains("ZBytes") && msg.contains("ZKeyExpr"), + "the message names the declared type and the real one: {msg}" + ); +} + +/// The "called once per delivery" contract has to survive **composition**: when +/// a value form's field splices a child type whose own boundary is also derived +/// from a value form, the child accessor is a second hoist, not a call repeated +/// once per child leaf. +#[test] +fn a_nested_value_form_is_hoisted_too() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZInnerStruct { + pub a: i64, + pub b: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZOuterStruct { + pub inner: ZInner, + pub tag: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_inner_to_struct(i: &ZInner) -> ZInnerStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_outer_to_struct(o: &ZOuter) -> ZOuterStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_outer_sub(cb: impl Fn(ZOuter) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZOuter)) + .class(crate::ptr_class!(ZInner)) + .fun(crate::fun!(z_outer_sub)), + ) + .expand(crate::expand_return!(ZInner).fields(crate::fields!(z_inner_to_struct))) + .expand(crate::expand_return!(ZOuter).fields(crate::fields!(z_outer_to_struct))); + let dir = unique_test_dir("jnigen_vf_nested"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + let kotlin = gen + .write_kotlin(&dir.join("kotlin")) + .expect("write_kotlin") + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + + assert!( + kotlin.contains("inner__a: Long") && kotlin.contains("inner__b: Long"), + "the child value form's fields splice in, prefixed:\n{kotlin}" + ); + for f in ["z_outer_to_struct", "z_inner_to_struct"] { + let calls = rust.matches(f).count(); + assert_eq!( + calls, 1, + "`{f}` is bound to one local and every leaf below it reaches off \ + that local; found {calls} calls in:\n{rust}" + ); + } +} + +/// A consuming value form reached **through another one** is handed the +/// parent's field by MOVE. A hoisted value form is an owned struct and its +/// fields are disjoint, so giving one field away leaves every sibling leaf +/// readable — which is why this shape needs no clone and is not refused. +/// +/// Checked under both parents: what makes the move legal is that the hoist +/// local is owned, and a borrowing form's returned struct is owned just as much +/// as a consuming one's. +#[test] +fn a_nested_consuming_value_form_moves_the_parent_s_field() { + let loc = myflat_loc(); + let items = |outer_by_value: bool| -> Vec<(syn::Item, crate::SourceLocation)> { + let outer: syn::Item = if outer_by_value { + syn::Item::Fn(syn::parse_quote!( + pub fn z_outer_into_struct(o: ZOuter) -> ZOuterStruct { + unimplemented!() + } + )) + } else { + syn::Item::Fn(syn::parse_quote!( + pub fn z_outer_to_struct(o: &ZOuter) -> ZOuterStruct { + unimplemented!() + } + )) + }; + vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZInnerStruct { + pub a: i64, + pub b: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZOuterStruct { + pub inner: ZInner, + pub tag: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_inner_into_struct(i: ZInner) -> ZInnerStruct { + unimplemented!() + } + )), + loc.clone(), + ), + (outer, loc.clone()), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_outer_sub(cb: impl Fn(ZOuter) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ] + }; + + for (tag, outer_by_value, outer) in [ + ( + "borrow", + false, + crate::expand_return!(ZOuter).fields(crate::fields!(z_outer_to_struct)), + ), + ( + "consume", + true, + crate::expand_return!(ZOuter).fields_self_into(crate::fields!(z_outer_into_struct)), + ), + ] { + let registry = + Registry::::from_items(items(outer_by_value)).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZOuter)) + .class(crate::ptr_class!(ZInner)) + .fun(crate::fun!(z_outer_sub)), + ) + .expand( + crate::expand_return!(ZInner).fields_self_into(crate::fields!(z_inner_into_struct)), + ) + .expand(outer); + let dir = unique_test_dir(&format!("jnigen_vf_nested_consume_{tag}")); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + + assert!( + rust.contains("z_inner_into_struct(__vf0.inner)"), + "[{tag}] the parent's field is MOVED into the nested form, not borrowed \ + or cloned:\n{rust}" + ); + assert!( + rust.contains("__vf0.tag") && !rust.contains("(&__vf0)"), + "[{tag}] and a sibling leaf still reads its own field off the parent local, \ + projected directly — borrowing the partially-moved local as a whole would \ + not compile:\n{rust}" + ); + assert!( + !rust.contains("__vf1.a.clone()"), + "[{tag}] the nested form's own fields move out too:\n{rust}" + ); + } +} + +// A compact fixture shared by the two follow-up review regressions. +fn nested_review_items() -> Vec<(syn::Item, crate::SourceLocation)> { + let loc = myflat_loc(); + vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZReviewInnerStruct { + pub value: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZReviewOuterStruct { + pub optional: Option, + pub items: Vec, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_review_inner_to_struct(i: &ZReviewInner) -> ZReviewInnerStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_review_outer_to_struct(o: &ZReviewOuter) -> ZReviewOuterStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_review_outer_sub(cb: impl Fn(ZReviewOuter) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc, + ), + ] +} + +fn nested_review_jni(outer: crate::lang::ExpandReturnDecl) -> JniGen { + JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZReviewOuter)) + .class(crate::ptr_class!(ZReviewInner)) + .fun(crate::fun!(z_review_outer_sub)), + ) + .expand( + crate::expand_return!(ZReviewInner).fields(crate::fields!(z_review_inner_to_struct)), + ) + .expand(outer) +} + +/// A nested value form below an `Option` cannot be emitted as an +/// unconditional hoist: its accessor takes `&Inner`, not `&Option`. +/// Reject it during planning until conditional hoists can share one `Some` +/// scope across every descendant leaf. +#[test] +fn an_optional_nested_value_form_is_rejected_before_emission() { + let registry = Registry::::from_items(nested_review_items()).expect("index items"); + let jni = nested_review_jni( + crate::expand_return!(ZReviewOuter).fields(crate::fields!(z_review_outer_to_struct)), + ); + let err = match registry.resolve(jni) { + Ok(_) => panic!("an optional nested value form must be rejected"), + Err(e) => e, + }; + let msg = err.to_string(); + assert!( + msg.contains("z_review_inner_to_struct") && msg.contains("Option"), + "the error names the unsupported conditional hoist: {msg}" + ); +} + +/// Override records are applied to a `Vec` field as a whole; a fixed leaf +/// list cannot apply `T`'s deconstructor once per element. The declaration +/// check must compare against `Vec`, not peel it to `T`. +#[test] +fn a_vec_field_override_must_name_the_whole_vec_type() { + let build = || { + let registry = + Registry::::from_items(nested_review_items()).expect("index items"); + let jni = nested_review_jni( + crate::expand_return!(ZReviewOuter).fields( + crate::fields!(z_review_outer_to_struct) + .field("items", crate::expand_return!(ZReviewInner).field_self()), + ), + ); + let _ = registry.resolve(jni); + }; + + let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(build)) + .expect_err("an element-typed override on a Vec field must be rejected"); + let msg = err + .downcast_ref::() + .cloned() + .or_else(|| err.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_default(); + assert!( + msg.contains("items") && msg.contains("Vec") && msg.contains("ZReviewInner"), + "the error names the field, its whole Vec type, and the declared element type: {msg}" + ); +} + +// ── Consuming value forms ──────────────────────────────────────────────────── + +/// A value form whose accessor takes its receiver **by value** destroys the +/// object into its parts, so nothing needs cloning. Fixture mirrors the +/// borrowing one; `zc_owned` / `zc_borrowed` give an owned and a `&T` plan of +/// the same type, since one declaration serves both. +fn consuming_items() -> Vec<(syn::Item, crate::SourceLocation)> { + let loc = myflat_loc(); + vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZCarrierStruct { + pub label: String, + pub count: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn zc_into_struct(c: ZCarrier) -> ZCarrierStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn zc_to_struct(c: &ZCarrier) -> ZCarrierStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn zc_sub(cb: impl Fn(ZCarrier) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc, + ), + ] +} + +fn consuming_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> String { + let registry = Registry::::from_items(consuming_items()).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZCarrier)) + .fun(crate::fun!(zc_sub)), + ) + .expand(decl); + let dir = unique_test_dir(tag); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust") +} + +/// The headline: a consuming value form is handed the value itself, and each +/// field is **moved** into its leaf. The clones the borrowing form pays — one +/// per field, on a value it is about to drop — simply are not emitted. +#[test] +fn a_consuming_value_form_moves_its_fields() { + let rust = consuming_gen( + "jnigen_vf_consume", + crate::expand_return!(ZCarrier).fields_self_into(crate::fields!(zc_into_struct)), + ); + assert!( + rust.contains("zc_into_struct(__cb_arg0)"), + "the value is passed BY MOVE, not borrowed:\n{rust}" + ); + assert!( + rust.contains("__vf0.label") && rust.contains("__vf0.count"), + "each field is read off the one hoisted local:\n{rust}" + ); + assert!( + !rust.contains("__vf0.label.clone()") && !rust.contains("__vf0.count.clone()"), + "and MOVED out of it — a consuming form exists precisely to drop these \ + clones:\n{rust}" + ); +} + +/// The borrowing form is untouched: same declaration shape, still borrows, still +/// clones. Consuming-ness is inferred per accessor, so one does not disturb the +/// other. +#[test] +fn the_borrowing_value_form_still_clones() { + let rust = consuming_gen( + "jnigen_vf_borrow", + crate::expand_return!(ZCarrier).fields(crate::fields!(zc_to_struct)), + ); + assert!( + rust.contains("zc_to_struct(&__cb_arg0)"), + "a `&T` accessor is still handed a borrow:\n{rust}" + ); + assert!( + rust.contains("__vf0.label.clone()"), + "and its fields are still cloned out:\n{rust}" + ); +} + +/// One declaration is reached by BOTH owned and borrowed plans of the same type +/// (records are type-level, `by_ref` is per-function). A borrowed plan has no +/// value to give up, so it clones once up front rather than being rejected — +/// the same cost the borrowing form of the accessor would have paid. +#[test] +fn a_borrowed_plan_clones_before_consuming() { + let loc = myflat_loc(); + let mut items = consuming_items(); + items.push(( + syn::Item::Fn(syn::parse_quote!( + pub fn zc_borrowed(v: &ZVault) -> Option<&ZCarrier> { + unimplemented!() + } + )), + loc, + )); + let registry = Registry::::from_items(items).expect("index items"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZCarrier)) + .class(crate::ptr_class!(ZVault)) + .fun(crate::fun!(zc_sub)) + .fun(crate::fun!(zc_borrowed)), + ) + .expand(crate::expand_return!(ZCarrier).fields_self_into(crate::fields!(zc_into_struct))); + let dir = unique_test_dir("jnigen_vf_consume_ref"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = registry.resolve(jni).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) + .expect("read rust"); + assert!( + rust.contains("zc_into_struct(__inner.clone())"), + "a borrowed plan clones the value, then consumes the clone:\n{rust}" + ); +} + +/// `.fields_self_into(..)` gives the value away, so anything else reading it is +/// broken by construction. Refused **where it is declared** — the collision is +/// visible in the decl itself, so it does not need a resolve to be found. +/// +/// `.field_self()` beside it would deliver the handle the form just consumed. +#[test] +#[should_panic(expected = "only record")] +fn a_consuming_value_form_rejects_a_following_sibling() { + let _ = crate::expand_return!(ZCarrier) + .fields_self_into(crate::fields!(zc_into_struct)) + .field_self(); +} + +/// And the other way round — the decl is a builder, so both orders must be +/// caught or the rule holds only for the order someone happened to write. +#[test] +#[should_panic(expected = "only record")] +fn a_consuming_value_form_rejects_a_preceding_sibling() { + let _ = crate::expand_return!(ZCarrier) + .field_self() + .fields_self_into(crate::fields!(zc_into_struct)); +} + +/// Any sibling record, not just the identity one. +#[test] +#[should_panic(expected = "only record")] +fn a_consuming_value_form_rejects_a_plain_field_sibling() { + let _ = crate::expand_return!(ZCarrier) + .fields_self_into(crate::fields!(zc_into_struct)) + .field(crate::fun!(zc_to_struct)); +} + +/// The declarator states whether the value is given away and the accessor's +/// signature has to agree — otherwise the emitted call would not compile in the +/// consumer's crate, and a boundary would silently stop being the one declared. +/// Both directions are errors; the fixture has one accessor of each kind. +#[test] +fn the_declarator_and_the_accessor_s_receiver_must_agree() { + let build = |decl: crate::lang::ExpandReturnDecl| -> String { + let registry = Registry::::from_items(consuming_items()).expect("index"); + let jni = JniGen::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZCarrier)) + .fun(crate::fun!(zc_sub)), + ) + .expand(decl); + match registry.resolve(jni) { + Ok(_) => String::new(), + Err(e) => e.to_string(), + } + }; + + let msg = build(crate::expand_return!(ZCarrier).fields_self_into(crate::fields!(zc_to_struct))); + assert!( + msg.contains("CONSUMING") && msg.contains("zc_to_struct"), + "`.fields_self_into` on a borrowing accessor must be refused, naming it: {msg:?}" + ); + + let msg = build(crate::expand_return!(ZCarrier).fields(crate::fields!(zc_into_struct))); + assert!( + msg.contains("BORROWING") && msg.contains("zc_into_struct"), + "`.fields` on a by-value accessor must be refused, naming it: {msg:?}" + ); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 930a2c30..4e0f88b6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -967,8 +967,11 @@ impl Prebindgen for JniGen { /// Assembled on demand — field names (member inheritance) resolve here, /// against the complete declaration set (see /// [`JniGen::build_deconstructors`]). - fn deconstructors(&self) -> Option { - Some(self.build_deconstructors()) + fn deconstructors( + &self, + registry: &Registry, + ) -> Option { + Some(self.build_deconstructors(registry)) } /// Synthesize a field-decomposition for every `.data_class` type whose diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index 60e3d6ea..b20d2425 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -42,8 +42,9 @@ pub use jni::{ box_jboolean, box_jbyte, box_jchar, box_jdouble, box_jfloat, box_jint, box_jlong, box_jshort, decode_byte_array, decode_string, encode_byte_array, encode_string, matching, null_byte_array, null_string, CachedIfaceMethod, ClassDecl, ConstDecl, ConvertDecl, ConvertSourceDecl, - DataClassDecl, EnumClassDecl, ExpandDecl, ExpandParamDecl, ExpandReturnDecl, FunctionDecl, - IgnoreDecl, JniBindingError, JniGen, PackageDecl, PtrClassDecl, SealedClassDecl, VariantDecl, + DataClassDecl, EnumClassDecl, ExpandDecl, ExpandParamDecl, ExpandReturnDecl, FieldsDecl, + FunctionDecl, IgnoreDecl, JniBindingError, JniGen, PackageDecl, PtrClassDecl, SealedClassDecl, + VariantDecl, }; // Kotlin emission types now live in the standalone generator module diff --git a/prebindgen/src/lib.rs b/prebindgen/src/lib.rs index 74c139f2..47c8e44a 100644 --- a/prebindgen/src/lib.rs +++ b/prebindgen/src/lib.rs @@ -142,7 +142,7 @@ //! [`try_from!`](crate::try_from), [`into!`](crate::into), //! [`try_into!`](crate::try_into) //! - Boundary expansion: [`expand_param!`](crate::expand_param), -//! [`expand_return!`](crate::expand_return) +//! [`expand_return!`](crate::expand_return), [`fields!`](crate::fields) //! //! **Syntax helpers** produce a bare `syn` node — `Type` / `Path` / `Expr` / //! `Signature` / `Ident` — to hand to a declaration method that requires one. @@ -286,10 +286,18 @@ macro_rules! ident { /// [`lang::JniGen`]. The C / cbindgen proof of concept is available separately /// with the `unstable-cbindgen` feature. pub mod core { + /// The prebindgen **source language**: the parser from captured + /// `#[prebindgen]` records to [`language::Element`]s, and the element model + /// itself. Not to be confused with [`crate::lang`], the *destination* + /// adapters. + pub use crate::api::core::language; + /// [`Language`] and [`Element`] sit here too, next to [`Registry`]: they are + /// what a build script names, and the rest of the element model stays in + /// [`mod@language`] where an adapter reaches for it. pub use crate::api::core::{ - ConverterImpl, Direction, DomainScalar, Generation, Gravestone, NicheSlot, Niches, - Prebindgen, Registry, RepresentationDomain, ScalarValue, ScanError, Stage, Transmute, - TypeEntry, TypeKey, WriteRustError, + ConverterImpl, Direction, DomainScalar, Element, Generation, Gravestone, Language, + NicheSlot, Niches, Prebindgen, Registry, RepresentationDomain, ScalarValue, ScanError, + Stage, Transmute, TypeEntry, TypeKey, WriteRustError, }; } @@ -322,8 +330,8 @@ pub mod lang { box_jshort, decode_byte_array, decode_string, encode_byte_array, encode_string, matching, null_byte_array, null_string, CachedIfaceMethod, ClassDecl, ConstDecl, ConvertDecl, ConvertSourceDecl, DataClassDecl, EnumClassDecl, ExpandDecl, ExpandParamDecl, - ExpandReturnDecl, FunctionDecl, IgnoreDecl, JniBindingError, JniGen, KotlinFile, - PackageDecl, PtrClassDecl, SealedClassDecl, VariantDecl, WriteKotlinError, + ExpandReturnDecl, FieldsDecl, FunctionDecl, IgnoreDecl, JniBindingError, JniGen, + KotlinFile, PackageDecl, PtrClassDecl, SealedClassDecl, VariantDecl, WriteKotlinError, }; } From 989010eb346f02b61026fa8d4053e14a5907fb90 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 30 Jul 2026 09:36:21 +0200 Subject: [PATCH 03/52] Flat: a resolved model with direct access, not a stream of elements (#232) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Rename the module to flat: these are the flat API's elements `core::language` modelled one thing and was named for another. What it parses is the **flat API** — the single flat namespace a `#[prebindgen]` crate exports — so `Language` becomes `Flat` and `api/core/language/` becomes `api/core/flat/`. Mechanical, and separated from the model changes that follow so those arrive as a readable diff. The boundary ledger's skipped-path constant and header move with the directory; the count does not change. Co-Authored-By: Claude Opus 5 * Element is a function, a type, or a constant `Element` mixed two levels: `Function | Struct | Variant | Enum | Const` set type declarations beside functions and constants, when the kinds a binding distinguishes are a function, a type, and a constant. Types now group under `Element::Type`, and the type *reference* — which held the name `Type` — becomes `TypeRef`, so a declaration and a use site stop sharing a word. `Opaque` becomes the entity for a type whose contents do not cross, and it arrives two ways: * `#[prebindgen] pub type X = path;` — this **reverses** #227, where a marked alias was `Unsupported`. It is now how a handle enters the flat API deliberately: a foreign or crate-private type gets a name here without any claim about its contents. That is what makes the API closable, and it is the prerequisite for requiring references to resolve. * a marked tuple struct, whose fields no adapter has ever crossed — unchanged acceptance, now named for what it always meant. So `Struct::fields` drops its `Option`. `None` was the opaque case; an empty list now means the source wrote a struct with no fields, which is a different thing. `MaybeUninit` joins the grammar as `TypeKind::Uninit`. It is a boundary concept — an out-parameter whose slot the caller supplies and the callee fills — and cbindgen already models it as exactly that, so this moves a classification out of the adapter and into the frontend, per #211. It is also the one foreign generic that no alias could name, a generic alias being a generic binder. Co-Authored-By: Claude Opus 5 * Flat resolves its references and answers by name Two changes that belong together, because the first is what makes the second decidable. **The model is addressed by name, not iterated.** `FlatBuilder` collects and `build` hands over a `Flat` — `function(name)`, `declared_type(name)`, `constant(name)`, `element(name)`, plus iterators over each kind. Names are unique across the whole model, so a name is a complete address, and that is what every later stage wants: an adapter asks what a declared name *is* rather than scanning a list. L1 carried this as a checklist bullet; it is really a property of the model. Two types rather than one, because a half-built model should not be the same type as a resolved one — `Source::builder()` sets the precedent. **References resolve at parse time.** A third pass walks every `TypeRef` — through `Option`, `Vec`, `&`, `Result`, arrays, callback arguments and generic arguments alike — and an item naming a type the flat API does not declare becomes `Element::Unsupported` with `ItemError::UnresolvedType`. Deferred, not fatal, like every other refusal: an item no binding declares stays harmless. This is what a marked type alias bought. A dangling name previously surfaced far downstream as an unresolved *converter*, from whichever adapter happened to look first — the "one fact, several authorities" #211 exists to end. Note the two remain distinct: resolution here says a name denotes something, while an adapter's resolver still decides whether it supplied a converter for it. A path-qualified name gets its own diagnosis, since `#[prebindgen] pub type foreign::Option = ..` is not a spelling that exists — marked items live in one flat namespace of bare names. Also: `Item::Type` no longer reaches the registry's passthrough. An opaque declaration states something about the API's surface and is not code to copy into the binding; its target is routinely crate-private, so re-emitting it would not compile. Co-Authored-By: Claude Opus 5 * Close the example flat APIs, and assert they stay closed Every type a marked signature named had to become a declaration for resolution to mean anything. Two idioms, chosen by what the type actually is rather than by its Rust shape: **A handle gets a marked alias.** `Storage`, the three callback handlers, `Token`, `TokenGc`, `Summary`, `Archive`, `Report`, `EscapeProbe`, `StorageError`, and example-flat's `Calculator` move into a private `handles` module, with `#[prebindgen] pub type X = handles::X;` at the top level. The alias is transparent, so every signature still says `Storage`. `Error` in both crates was already an alias and only needed the attribute — which is exactly the shape zenoh-flat's 26 zenoh re-exports will take. **A public newtype stays a marked struct.** `Millis`, `Celsius`, `Percent` and `Label` are not handles: they cross by `convert!`, and covertest-helpers both constructs them and reads `.0`. Hiding them behind an alias broke that downstream, which is the useful signal — a type alias names the type, not the tuple-struct constructor, and the constructor lives in the value namespace where the struct is defined. In-crate construction of the relocated handlers is qualified `handles::PayloadHandler(..)` for the same reason. Marking these as structs rather than aliases matters for a second reason: a marked struct enters `registry.structs`, and `write.rs` emits `on_struct` for any declared type there — so marking the *handles* as structs would have changed generated output. The alias route is invisible to the registry, which is why the goldens hold. **And the closure is asserted, not assumed.** covertest-kotlin's build script now runs `Flat` over both sources and fails if anything is unsupported. It is the right place: only there do the helper crate's references to perftest-flat's types resolve, since it cannot mark them itself. Verified by deliberately unmarking `Storage` — the build fails naming all twelve referencing functions and the fix. Generation is byte-identical (`examples/regen-check.sh`) and the JVM covertest passes all 47 sections. Co-Authored-By: Claude Opus 5 * Record L0.5 in the stage map The model is now indexed and resolved, which takes two bullets off L1 — elements indexed by name, and the entry point that shares one parser — and adds a prerequisite L0 did not have: the flat API has to be closed for resolution to mean anything. Also records what is left open: zenoh-flat and its two consumers are separate repos whose 28 unmarked types need the same treatment, and `Cow<'_, [u8]>` has no alias spelling. Co-Authored-By: Claude Opus 5 * Take a slice, not a Vec reference, in the resolution pass `clippy::ptr_arg` under CI's no-default-features run: the pass only mutates elements in place, so a slice is the honest signature. My local checks used --all-features only; CI runs three clippy configurations. Co-Authored-By: Claude Opus 5 * An out-parameter is a mode of borrowing, not a type `TypeKind::Uninit` wrapped a type, but uninitialized-ness is a property of the **borrow**: my own doc said `MaybeUninit` is "only meaningful behind a `&mut`", which is the argument against modelling it as a type at all. So `Ref` carries the mode, and the `MaybeUninit` is absorbed into it: Ref { mode: RefMode, inner: Box } enum RefMode { Shared, Exclusive, Out } `&T`, `&mut T`, `&mut MaybeUninit` — one axis, three values, and `inner` is always the borrowed *value's* type. One variant fewer than the `mutable` flag plus a wrapper, and the combinations that mean nothing at a boundary can no longer be written down: uninitialized storage owned, returned or in a field promises nothing a destination language can use, and `&MaybeUninit` promises a readable `T` that may not be one. Both are refused, each naming why. `Out` rather than `Uninit` because it names the boundary role every destination language has — C's `T *out` — which is the fact an adapter acts on. Co-Authored-By: Claude Opus 5 * Address review: transitive closure, generic aliases, goldens, real index Four findings, all valid; two were mine in this PR. **Refusal was not transitive.** `resolve_references` snapshotted the initial declarations and validated everything against that fixed set, so refusing a type stranded its dependents: pub struct Broken { pub field: Missing } // refused pub fn use_broken(value: Broken) {} // survived anyway `Flat::resolve` then returned `None` for `use_broken`'s parameter, contradicting the one invariant the model promises. It now runs to a fixed point: each round drops the declarations it refused, and stops when a round refuses nothing. Chains of any length collapse, in either declaration order, because the declared set only ever shrinks — which is also why it terminates. Regressions cover the direct case both ways round, a four-link chain both ways round, a sound chain that must be left alone, and the invariant itself: every `Named` reachable from a surviving element resolves. **A generic type alias bypassed the binder refusal.** The `Item::Type` arm built an `Opaque` without calling `reject_generic_params`, so `pub type Handle = hidden::Handle;` was accepted as one declaration that `Handle` then resolved against — losing exactly the scoped-parameter distinction every other item kind refuses, and contradicting this PR's own argument that `MaybeUninit` needed grammar support *because* a generic alias is a binder. Type and const parameters are now refused; a lifetime binder stays accepted, as on every other kind. **The aarch64 goldens carried unrelated all-features output.** `git add -A examples` in the migration commit swept in pre-existing working-tree drift — `unstable_field`, `calculator_reset`, a non-empty feature guard — which is exactly the state 95fd753 had reverted, because committed aarch64 goldens represent a plain build. Restored from the base, and verified: a plain `cargo build --release -p example-cbindgen` on arm64 reproduces the base files byte-for-byte. CI is x86_64 and cannot see this pair, so it needed catching by hand. My "byte-identical" claim was wrong for that reason, not for the model changes. **`Flat` was not actually indexed.** It stored only a `Vec` and `element()` did `iter().find`, so every typed accessor and `resolve()` scanned — quadratic once later stages resolve in a loop, and not the "indexed by name" criterion L0.5 claims. Now a `HashMap` beside the elements: positions, so there is one copy of each element and source order stays available for iteration. Built after resolution, since refusing an item changes its kind but never its name. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- docs/language-integration.md | 37 +- examples/covertest-kotlin/build.rs | 27 + examples/example-flat/src/lib.rs | 21 +- examples/perftest-flat/src/ext.rs | 98 ++- examples/perftest-flat/src/lib.rs | 51 +- .../api/core/{language => flat}/array_len.rs | 0 .../core/{language => flat}/boundary.ledger | 4 +- .../api/core/{language => flat}/boundary.rs | 8 +- .../api/core/{language => flat}/element.rs | 166 +++-- .../src/api/core/{language => flat}/mod.rs | 419 ++++++++++--- .../src/api/core/{language => flat}/origin.rs | 0 .../src/api/core/{language => flat}/spell.rs | 0 .../{language => flat}/tests/acceptance.rs | 581 ++++++++++++++++-- .../api/core/{language => flat}/tests/mod.rs | 99 ++- .../{language => flat}/tests/roundtrip.rs | 40 +- .../src/api/core/{language => flat}/ty.rs | 172 +++++- prebindgen/src/api/core/mod.rs | 4 +- prebindgen/src/api/core/registry.rs | 7 +- prebindgen/src/lib.rs | 21 +- 19 files changed, 1435 insertions(+), 320 deletions(-) rename prebindgen/src/api/core/{language => flat}/array_len.rs (100%) rename prebindgen/src/api/core/{language => flat}/boundary.ledger (95%) rename prebindgen/src/api/core/{language => flat}/boundary.rs (98%) rename prebindgen/src/api/core/{language => flat}/element.rs (70%) rename prebindgen/src/api/core/{language => flat}/mod.rs (67%) rename prebindgen/src/api/core/{language => flat}/origin.rs (100%) rename prebindgen/src/api/core/{language => flat}/spell.rs (100%) rename prebindgen/src/api/core/{language => flat}/tests/acceptance.rs (61%) rename prebindgen/src/api/core/{language => flat}/tests/mod.rs (52%) rename prebindgen/src/api/core/{language => flat}/tests/roundtrip.rs (95%) rename prebindgen/src/api/core/{language => flat}/ty.rs (73%) diff --git a/docs/language-integration.md b/docs/language-integration.md index 57e02c59..6a6adc67 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -95,6 +95,7 @@ moves it. | Stage | Owns | State | |---|---|---| | L0 | `Language` + `Element` + the ledger | **done** — [#227](https://github.com/milyin/prebindgen/pull/227) | +| L0.5 | `Flat`: the model, indexed and resolved | **done** — this branch | | L1 | `Registry` consumes elements | not started | | L2 | `api/core` stops classifying source syntax | not started | | L3 | `Cbindgen` consumes elements | not started | @@ -123,12 +124,43 @@ may mark items no binding uses. Only a duplicate name — which no declaration c disambiguate — fails the parse. Tuple-struct fields stay unmodelled for the same reason. +### L0.5 — `Flat`: the model, indexed and resolved — **done** + +L0 produced a `Vec`, which nobody could ask anything. This stage makes it +a model, and takes two bullets off L1 in the process. + +- [x] `core::language` → `core::flat`, `Language` → `Flat`: the thing being + modelled is the **flat API** +- [x] `Element = Function | Type | Constant | Unsupported`, with `Struct`, + `Variant`, `Enum` and `Opaque` under `Type`; the type *reference* becomes + `TypeRef` +- [x] `Opaque` is an entity, declared by `#[prebindgen] pub type X = ..` — the way + a foreign or crate-private handle gets a **name** in the flat API. This is + the prerequisite for everything below it +- [x] `FlatBuilder` collects, `Flat` answers by name: `function`, + `declared_type`, `constant`, `element`, the per-kind iterators, `resolve` +- [x] **References resolve at parse time.** An item naming an undeclared type is + `Element::Unsupported` with `ItemError::UnresolvedType` — so a dangling name + is reported here, by name, instead of surfacing downstream as an unresolved + *converter* from whichever adapter looked first +- [x] `MaybeUninit` becomes `TypeKind::Uninit`: a boundary concept the adapter + was classifying, and the one foreign generic no alias can name +- [x] The example flat APIs are closed, and covertest-kotlin's build script + asserts they stay closed across both its sources +- [x] **Did not move**: every generated artifact byte-identical + +**Still open**: `zenoh-flat` and its two consumers are separate repos. Their 28 +unmarked types (26 zenoh aliases, plus `Duration` and `Cow<'_, [u8]>`) need the +same treatment before they parse. `Cow<'_, [u8]>` has no alias spelling — generic +and lifetime-bearing — so `zbytes_to_bytes` needs either the `MaybeUninit` +treatment or a signature change. + ### L1 — `Registry` consumes elements The seam that makes the direction real. Adapters must not need touching. -- [ ] `Registry::from_elements(Vec)`; `from_items` becomes - `Language::parse` + `from_elements`, so both entry points share one parser +- [ ] `Registry::from_flat(&Flat)`; `from_items` becomes `Flat::builder` + + `from_flat`, so both entry points share one parser - [ ] The `functions` / `structs` / `enums` / `consts` / `passthrough` maps are rebuilt from each element's retained `syntax` — a projection, not a second source of truth @@ -137,7 +169,6 @@ The seam that makes the direction real. Adapters must not need touching. declaring such an item is what raises it - [ ] `ScanError`'s per-item variants map onto `ItemError`, so one authority produces the message -- [ ] Elements are indexed by name so L2–L4 can ask for them - [ ] **Must not move**: every generated artifact byte-identical (`examples/regen-check.sh`) diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index 6aa69154..2243d3c5 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -132,6 +132,33 @@ fn main() { .crate_name("cov_helpers") .build(); + // The flat API must be CLOSED: every type a marked signature names has to be + // declared here too, as a struct/enum or as `#[prebindgen] pub type X = ..` + // for a handle. `Flat` proves that across BOTH sources at once, which is the + // only way the helper crate's references to `perftest-flat`'s types can + // resolve — it cannot mark them itself. + // + // Asserted rather than merely computed: without this, an unmarked type would + // go unnoticed until the adapters consume elements (L1+), and then surface as + // a late unresolved-converter error instead of naming the missing marker. + let flat = prebindgen::core::Flat::builder() + .items(source.items_all()) + .items(helpers.items_all()) + .build() + .expect("the flat API parses"); + let unresolved: Vec = flat + .unsupported() + .map(|u| match &u.name { + Some(name) => format!(" {name}: {}", u.error), + None => format!(" {}", u.error), + }) + .collect(); + assert!( + unresolved.is_empty(), + "the flat API is not closed:\n{}", + unresolved.join("\n") + ); + let jni = JniGen::new() .set_package_prefix("io.prebindgen.covertest") .set_jni_native_init("io.prebindgen.covertest.NativeLibrary.ensureLoaded()") diff --git a/examples/example-flat/src/lib.rs b/examples/example-flat/src/lib.rs index d03bd177..8ff3b57d 100644 --- a/examples/example-flat/src/lib.rs +++ b/examples/example-flat/src/lib.rs @@ -31,6 +31,11 @@ pub const FEATURES: &str = features!(); /// Boxed error type, mirroring zenoh-flat's `Error`. It is the `E` of every /// fallible `Result` and never crosses the FFI boundary as a value; the adapter /// marshals it to C as a `char*` message obtained from [`error_get_message`]. +/// +/// Marked, because that is how a type whose contents do not cross gets a name in +/// the flat API: the alias declares `Error` as an opaque handle, which is what +/// lets every `Result<_, Error>` below resolve. +#[prebindgen] pub type Error = Box; /// Render an error as its display string. Wired into the C adapter as the @@ -286,11 +291,21 @@ pub fn drawing_get_shape(d: Drawing) -> Shape { /// A stateful accumulator. This is a plain Rust type used as an opaque handle: /// the binding holds it behind a pointer and frees it with `calculator_drop`. -pub struct Calculator { - value: f64, - history: Vec, +/// +/// The definition lives in a private module and the flat API exports a marked +/// alias to it. That is how a handle whose contents never cross gets a name here +/// — the same shape zenoh-flat uses for the Zenoh types it re-exports — and the +/// alias is transparent, so everything below still says `Calculator`. +mod calculator { + pub struct Calculator { + pub(super) value: f64, + pub(super) history: Vec, + } } +#[prebindgen] +pub type Calculator = calculator::Calculator; + /// Build a fresh accumulator initialized to zero. #[prebindgen] pub fn calculator_new() -> Calculator { diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index fbcb2cbe..ed3b7d39 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -22,7 +22,56 @@ //! milliseconds, with `Option` using an invalid representation as //! an allocation-free niche. -pub use std::time::Duration; +/// Marked, so `Duration` is a name the flat API declares rather than one it +/// merely mentions. +#[prebindgen] +pub type Duration = std::time::Duration; + +/// The handle types this module's flat API exports. +/// +/// Definitions live here and the flat API exports marked aliases to them, so +/// each has a **name** every signature can resolve against without declaring +/// its fields a boundary surface. See `lib.rs`'s `handles` for the same shape. +mod handles { + use super::{Lookup, Reading, Stamp, Storage}; + + #[derive(Clone)] + + pub struct Summary { + pub(super) count: i64, + pub(super) total: f64, + } + + pub struct Archive { + pub(super) latest: Option, + /// A sum the archive OWNS, so it can hand one back **borrowed** (`&Reading`) + /// — the return shape whose encoder must match on the value behind the + /// reference rather than moving it. + pub(super) reading: Reading, + /// The same, optional, for the `Option<&Reading>` shape. + pub(super) fallback: Option, + } + + pub struct Report { + pub(super) summary: Summary, + pub(super) taken: Option, + pub(super) origin: Stamp, + pub(super) outcome: Lookup, + pub(super) label: String, + } + + pub struct EscapeProbe { + pub(super) value: i64, + } + + #[derive(Debug)] + + pub struct StorageError { + pub(super) message: String, + } + + pub struct StorageHandler(pub(super) Box); +} use prebindgen_proc_macro::prebindgen; @@ -427,10 +476,8 @@ pub fn stamp_series(count: i64) -> Vec { /// Failure value for the fallible storage constructor. Never crosses as a /// value: the binding peels the `Result`, renders the message through /// [`storage_error_message`], and delivers it to the caller's `onError`. -#[derive(Debug)] -pub struct StorageError { - message: String, -} +#[prebindgen] +pub type StorageError = handles::StorageError; /// Render a [`StorageError`] as its message (the error's flatten-output /// **accessor**, fed to `onError`). @@ -497,11 +544,8 @@ pub fn storage_try_from_stamp(s: Stamp, tag: [u8; 2]) -> Result`) /// and the JVM binding's only sound lowering of a borrowed handle is a clone /// into a fresh owned handle. -#[derive(Clone)] -pub struct Summary { - count: i64, - total: f64, -} +#[prebindgen] +pub type Summary = handles::Summary; /// Construct a [`Summary`] from its parts (declared a **constructor** / /// companion factory, and the build-from **variant** of the flatten-input). @@ -654,6 +698,7 @@ pub fn storage_with_payload(payload: Payload) -> Storage { /// marking it would make the Kotlin emitter try to render this tuple struct as a /// data class. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[prebindgen] pub struct Millis(pub u64); /// Sum two durations (exercises the custom wrapper on both a **parameter** and @@ -772,6 +817,7 @@ pub fn hold_policy_echo(p: HoldPolicy) -> HoldPolicy { /// A temperature. Crosses via its `From`/`Into` impls /// (`convert!(Celsius).input_from(ty!(i32)).output_into(ty!(i32))`). #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[prebindgen] pub struct Celsius(pub i32); impl From for Celsius { @@ -796,6 +842,7 @@ pub fn celsius_double(c: Celsius) -> Celsius { /// `TryFrom` on input (out-of-range i32 from the JVM → the caller's /// error handler) and an infallible `Into` on output. #[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[prebindgen] pub struct Percent(pub u8); impl TryFrom for Percent { @@ -840,6 +887,7 @@ pub fn percent_invalid_output() -> Option { /// crate** (`convert!(Label).input_with(ty!(String), path!(crate::label_in))…`) /// — no `#[prebindgen]` marking anywhere in the conversion. #[derive(Clone, Debug, PartialEq, Eq)] +#[prebindgen] pub struct Label(pub String); /// Reverse a label's characters (exercises the binding-local conversion on @@ -1210,12 +1258,13 @@ pub fn storage_shards_opt(count: i64, each: i64) -> Option> { /// by value). Unlike [`PayloadHandler`] (whose arg is a flattened data class), /// the handle crosses as a raw pointer and the generated Kotlin proxy wraps it /// into a typed `Storage` and `close()`s it after `run` (close-unless-taken). -pub struct StorageHandler(Box); +#[prebindgen] +pub type StorageHandler = handles::StorageHandler; /// Wrap a `Fn(Storage)` closure into a reusable [`StorageHandler`]. #[prebindgen] pub fn storage_handler_new(f: impl Fn(Storage) + Send + Sync + 'static) -> StorageHandler { - StorageHandler(Box::new(f)) + handles::StorageHandler(Box::new(f)) } /// Build a synthetic storage of `n` payloads and hand **ownership** of it to @@ -1233,15 +1282,8 @@ pub fn storage_emit(n: i64, h: &StorageHandler) { /// **borrowed** — the shape zenoh-flat's `z_*` accessors use for the C tier's /// zero-copy borrows — which the JVM binding lowers by **cloning** into a fresh /// owned handle (the JVM keeps its handle past the call). -pub struct Archive { - latest: Option, - /// A sum the archive OWNS, so it can hand one back **borrowed** (`&Reading`) - /// — the return shape whose encoder must match on the value behind the - /// reference rather than moving it. - reading: Reading, - /// The same, optional, for the `Option<&Reading>` shape. - fallback: Option, -} +#[prebindgen] +pub type Archive = handles::Archive; impl Default for Archive { fn default() -> Self { @@ -1365,9 +1407,8 @@ pub fn cover_tag_runtime() -> String { /// extern is mangled to an underscored method name — so its `freePtr` /// destructor and accessor symbols only resolve at runtime if the generator /// applies the JNI spec's `_1` escaping. -pub struct EscapeProbe { - value: i64, -} +#[prebindgen] +pub type EscapeProbe = handles::EscapeProbe; /// Construct an [`EscapeProbe`] (its covertest constructor). #[prebindgen] @@ -1399,13 +1440,8 @@ pub fn escape_probe_value(p: &EscapeProbe) -> i64 { /// | `origin` | a non-optional declared `data class` ⇒ INLINES into its own fields | /// | `outcome` | a `sealed_class!` ⇒ its selector plus one group per alternative, with a handle payload | /// | `label` | a plain leaf | -pub struct Report { - summary: Summary, - taken: Option, - origin: Stamp, - outcome: Lookup, - label: String, -} +#[prebindgen] +pub type Report = handles::Report; /// The value form of [`Report`]: its fields as data, handles staying handles. #[prebindgen] diff --git a/examples/perftest-flat/src/lib.rs b/examples/perftest-flat/src/lib.rs index 1ea42088..2911f9dd 100644 --- a/examples/perftest-flat/src/lib.rs +++ b/examples/perftest-flat/src/lib.rs @@ -78,11 +78,36 @@ pub struct Payload { /// boundary — the adapter boxes it and emits a typed destructor. (Not /// `#[prebindgen]` and not `#[repr(C)]`: it is a boxed handle, like `Calculator` /// in `example-flat`.) -#[derive(Default)] -pub struct Storage { - payloads: Vec, +mod handles { + use super::Payload; + + #[derive(Default)] + pub struct Storage { + pub(super) payloads: Vec, + } + + pub struct PayloadHandler(pub(super) Box); + + pub struct PayloadVecHandler(pub(super) Box); + + pub struct Token { + pub(super) value: i64, + } + + pub struct TokenGc { + pub(super) value: i64, + } } +/// The handle types the flat API exports. +/// +/// Each is a marked alias to a definition in the private `handles` module: that +/// is how a type whose contents never cross gets a **name** in the flat API, +/// which is what lets every signature below resolve. Marking the structs +/// themselves would instead declare their fields a boundary surface. +#[prebindgen] +pub type Storage = handles::Storage; + /// An opaque, reusable handle wrapping a **prepared** `Fn(&Payload)` callback. /// The foreign-side trampoline (e.g. the JNI global ref + method lookup that turn a /// JVM callback into a Rust closure) is built **once** when the handle is created @@ -91,14 +116,16 @@ pub struct Storage { /// once, deliver events to it (cf. zenoh's `session_declare_subscriber` → /// `Subscriber`). Like [`Storage`], it is a boxed handle (not `#[prebindgen]`/ /// `#[repr(C)]`); the adapter emits a typed destructor. -pub struct PayloadHandler(Box); +#[prebindgen] +pub type PayloadHandler = handles::PayloadHandler; /// Like [`PayloadHandler`], but its callback receives the **whole batch at once** as a /// slice (`Fn(&[Payload])`) rather than one payload at a time. Fired by /// [`storage_callback_vec`]. Across the C ABI the slice is delivered **by reference** /// (`const payload_t *` + `size_t` — zero-copy, no per-element materialization); in /// Kotlin it arrives as a `List`. -pub struct PayloadVecHandler(Box); +#[prebindgen] +pub type PayloadVecHandler = handles::PayloadVecHandler; /// Create a new, empty storage handle. #[prebindgen] @@ -191,7 +218,7 @@ pub fn storage_get_into_uninit(s: &Storage, payload: &mut MaybeUninit) /// built here, amortized over every later delivery). #[prebindgen] pub fn payload_handler_new(f: impl Fn(&Payload) + Send + Sync + 'static) -> PayloadHandler { - PayloadHandler(Box::new(f)) + handles::PayloadHandler(Box::new(f)) } /// Invoke the prepared `handler` once **per stored payload** with a borrow of each @@ -246,7 +273,7 @@ pub fn storage_get_vec(s: &Storage) -> Option> { pub fn payload_vec_handler_new( f: impl Fn(&[Payload]) + Send + Sync + 'static, ) -> PayloadVecHandler { - PayloadVecHandler(Box::new(f)) + handles::PayloadVecHandler(Box::new(f)) } /// Invoke the prepared `handler` **once** with the whole stored batch as a slice @@ -278,9 +305,8 @@ pub fn string_len(s: &String) -> usize { /// benchmarked head-to-head to price the GC-cleanup machinery (atomic cell, /// Cleaner registration, CAS release ticket) per handle. A boxed handle like /// [`Storage`] (not `#[prebindgen]`, not `#[repr(C)]`). -pub struct Token { - value: i64, -} +#[prebindgen] +pub type Token = handles::Token; /// Create a plain benchmark token. #[prebindgen] @@ -296,9 +322,8 @@ pub fn token_value(t: &Token) -> i64 { /// GC-managed twin of [`Token`] — identical shape and cost on the Rust side; /// the Kotlin binding declares this one `.gc_managed()`. -pub struct TokenGc { - value: i64, -} +#[prebindgen] +pub type TokenGc = handles::TokenGc; /// Create a gc-managed benchmark token. #[prebindgen] diff --git a/prebindgen/src/api/core/language/array_len.rs b/prebindgen/src/api/core/flat/array_len.rs similarity index 100% rename from prebindgen/src/api/core/language/array_len.rs rename to prebindgen/src/api/core/flat/array_len.rs diff --git a/prebindgen/src/api/core/language/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger similarity index 95% rename from prebindgen/src/api/core/language/boundary.ledger rename to prebindgen/src/api/core/flat/boundary.ledger index 3a3b8fed..c3b1e4c3 100644 --- a/prebindgen/src/api/core/language/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -1,6 +1,6 @@ # prebindgen source-syntax boundary ledger — issue #211. # -# One line per file OUTSIDE `api/core/language/`, counting how many times a +# One line per file OUTSIDE `api/core/flat/`, counting how many times a # variant of a watched syn syntax enum is named in production code: # # syn::Type::Reference(r) => ... <- one site @@ -20,7 +20,7 @@ # To change it deliberately: # # UPDATE_BOUNDARY_LEDGER=1 cargo test -p prebindgen boundary_ledger -# git diff prebindgen/src/api/core/language/boundary.ledger +# git diff prebindgen/src/api/core/flat/boundary.ledger # # A count going DOWN is the goal (a classifier reads elements instead) and is # still a ledger edit, so the win shows up in the diff. diff --git a/prebindgen/src/api/core/language/boundary.rs b/prebindgen/src/api/core/flat/boundary.rs similarity index 98% rename from prebindgen/src/api/core/language/boundary.rs rename to prebindgen/src/api/core/flat/boundary.rs index e2bc6215..d1bc6572 100644 --- a/prebindgen/src/api/core/language/boundary.rs +++ b/prebindgen/src/api/core/flat/boundary.rs @@ -80,14 +80,14 @@ use proc_macro2::{Delimiter, TokenStream, TokenTree}; const WATCHED: &[&str] = &["Type", "Expr"]; /// Ledger location, relative to `src/`. -const LEDGER: &str = "api/core/language/boundary.ledger"; +const LEDGER: &str = "api/core/flat/boundary.ledger"; /// Regenerated verbatim on every write, so the contract cannot drift from the /// numbers underneath it. const HEADER: &str = "\ # prebindgen source-syntax boundary ledger — issue #211. # -# One line per file OUTSIDE `api/core/language/`, counting how many times a +# One line per file OUTSIDE `api/core/flat/`, counting how many times a # variant of a watched syn syntax enum is named in production code: # # syn::Type::Reference(r) => ... <- one site @@ -107,7 +107,7 @@ const HEADER: &str = "\ # To change it deliberately: # # UPDATE_BOUNDARY_LEDGER=1 cargo test -p prebindgen boundary_ledger -# git diff prebindgen/src/api/core/language/boundary.ledger +# git diff prebindgen/src/api/core/flat/boundary.ledger # # A count going DOWN is the goal (a classifier reads elements instead) and is # still a ledger edit, so the win shows up in the diff. @@ -155,7 +155,7 @@ fn scan_tree(src_root: &Path) -> BTreeMap { continue; } let rel = rel_key(src_root, &path); - if rel.starts_with("api/core/language/") { + if rel.starts_with("api/core/flat/") { continue; } let text = fs::read_to_string(&path).expect("source file is UTF-8"); diff --git a/prebindgen/src/api/core/language/element.rs b/prebindgen/src/api/core/flat/element.rs similarity index 70% rename from prebindgen/src/api/core/language/element.rs rename to prebindgen/src/api/core/flat/element.rs index 5ab3e8fc..c928cd6d 100644 --- a/prebindgen/src/api/core/language/element.rs +++ b/prebindgen/src/api/core/flat/element.rs @@ -9,29 +9,26 @@ //! lives in [`spell`](super::spell), so the shape of an element says nothing //! about the language it came from. -use super::{origin::Origin, ty::Type}; +use super::{origin::Origin, ty::TypeRef}; use crate::SourceLocation; -/// One structure of the prebindgen source language. +/// One member of the flat API. /// -/// The five modelled kinds, plus [`Element::Unsupported`] for anything the -/// language cannot express. There is no verbatim-passthrough variant: a -/// `#[prebindgen]` crate marks the items that cross the boundary, and the -/// supporting code around them is the consumer crate's job — the proc-macro -/// enforces that already, refusing to mark a `use`, `mod`, `impl` or -/// `macro_rules!` at all. +/// Three modelled kinds — a function, a type, a constant — plus +/// [`Element::Unsupported`] for anything the language cannot express. There is no +/// verbatim-passthrough variant: a `#[prebindgen]` crate marks the items that +/// cross the boundary, and the supporting code around them is the consumer +/// crate's job — the proc-macro enforces that already, refusing to mark a `use`, +/// `mod`, `impl` or `macro_rules!` at all. #[derive(Clone, Debug)] pub enum Element { Function(Function), - Struct(Struct), - /// An enum whose alternatives carry payloads — a sum type. - Variant(Variant), - /// An enum whose every alternative is fieldless — a named set of integers. - Enum(Enum), - Const(Const), + /// A type declaration: a struct, either enum shape, or an opaque handle. + Type(Type), + Constant(Constant), /// An item the language cannot express — a parameter type outside the - /// grammar, a `self` receiver, or a whole item kind it does not model such - /// as a `union`. + /// grammar, a `self` receiver, a reference to a type the flat API never + /// declares, or a whole item kind it does not model such as a `union`. /// /// Inert: it is indexed under its name so nothing else can claim it, and /// the diagnosis rides along, to be raised by whatever declares it. See the @@ -49,10 +46,8 @@ impl Element { pub fn name(&self) -> Option<&syn::Ident> { let named = match self { Element::Function(f) => Some(&f.name), - Element::Struct(s) => Some(&s.name), - Element::Variant(v) => Some(&v.name), - Element::Enum(e) => Some(&e.name), - Element::Const(c) => Some(&c.name), + Element::Type(t) => Some(t.name()), + Element::Constant(c) => Some(&c.name), Element::Unsupported(u) => u.name.as_ref(), }; named.filter(|id| *id != "_") @@ -65,10 +60,8 @@ impl Element { pub fn location(&self) -> &SourceLocation { match self { Element::Function(f) => &f.origin.location, - Element::Struct(s) => &s.origin.location, - Element::Variant(v) => &v.origin.location, - Element::Enum(e) => &e.origin.location, - Element::Const(c) => &c.origin.location, + Element::Type(t) => t.location(), + Element::Constant(c) => &c.origin.location, Element::Unsupported(u) => &u.origin.location, } } @@ -77,15 +70,83 @@ impl Element { pub fn syntax(&self) -> syn::Item { match self { Element::Function(f) => syn::Item::Fn(f.origin.syntax.clone()), - Element::Struct(s) => syn::Item::Struct(s.origin.syntax.clone()), - Element::Variant(v) => syn::Item::Enum(v.origin.syntax.clone()), - Element::Enum(e) => syn::Item::Enum(e.origin.syntax.clone()), - Element::Const(c) => syn::Item::Const(c.origin.syntax.clone()), + Element::Type(t) => t.syntax(), + Element::Constant(c) => syn::Item::Const(c.origin.syntax.clone()), Element::Unsupported(u) => u.origin.syntax.clone(), } } } +/// A type the flat API declares. +/// +/// Four shapes, and the classification is what a destination language acts on: a +/// product of fields, a sum, a named set of integers, or a handle whose contents +/// do not cross. +#[derive(Clone, Debug)] +pub enum Type { + Struct(Struct), + /// An enum whose alternatives carry payloads — a sum type. + Variant(Variant), + /// An enum whose every alternative is fieldless — a named set of integers. + Enum(Enum), + Opaque(Opaque), +} + +impl Type { + pub fn name(&self) -> &syn::Ident { + match self { + Type::Struct(s) => &s.name, + Type::Variant(v) => &v.name, + Type::Enum(e) => &e.name, + Type::Opaque(o) => &o.name, + } + } + + pub fn location(&self) -> &SourceLocation { + self.location_rc() + } + + /// The shared location itself, for building a sibling node's [`Origin`]. + pub(super) fn location_rc(&self) -> &std::rc::Rc { + match self { + Type::Struct(s) => &s.origin.location, + Type::Variant(v) => &v.origin.location, + Type::Enum(e) => &e.origin.location, + Type::Opaque(o) => &o.origin.location, + } + } + + /// The whole item as the source wrote it. + pub fn syntax(&self) -> syn::Item { + match self { + Type::Struct(s) => syn::Item::Struct(s.origin.syntax.clone()), + Type::Variant(v) => syn::Item::Enum(v.origin.syntax.clone()), + Type::Enum(e) => syn::Item::Enum(e.origin.syntax.clone()), + Type::Opaque(o) => o.origin.syntax.clone(), + } + } +} + +/// A type whose contents do not cross the boundary — a handle. +/// +/// Two spellings declare one thing, because the model records the *fact* rather +/// than the Rust shape that carried it: +/// +/// * `#[prebindgen] pub type X = path::To;` — the way to give a foreign or +/// crate-private type a name in the flat API. This is how a handle is declared +/// deliberately. +/// * `#[prebindgen] pub struct X(..);` — a tuple struct, whose fields no adapter +/// has ever crossed. +/// +/// Either way the adapter decides what the handle becomes: an opaque pointer, a +/// `ptr_class`, a `convert!` target. +#[derive(Clone, Debug)] +pub struct Opaque { + pub name: syn::Ident, + /// The declaring item — a type alias or a tuple struct. + pub origin: Origin, +} + /// A `#[prebindgen]` free function. #[derive(Clone, Debug)] pub struct Function { @@ -96,7 +157,7 @@ pub struct Function { /// [`TypeKind::Unit`](super::TypeKind), exactly as a written `-> ()` is: /// they mean the same thing, differ only in spelling, and every consumer /// today already normalizes one to the other on the spot. - pub ret: Type, + pub ret: TypeRef, /// The whole item: attributes, `cfg`, doc comments, body. pub origin: Origin, } @@ -105,39 +166,27 @@ pub struct Function { #[derive(Clone, Debug)] pub struct Param { pub name: syn::Ident, - pub ty: Type, + pub ty: TypeRef, /// The parameter as written — `mode: Mode`. pub origin: Origin, } -/// A `#[prebindgen]` struct: a product of fields, or an opaque one. +/// A `#[prebindgen]` struct: a product of fields that cross the boundary. +/// +/// A struct whose contents do *not* cross is an [`Opaque`], not a `Struct` with +/// nothing in it — so `fields` is a plain list, and empty means the source wrote +/// a struct with no fields. +/// +/// Whether the fields are named or positional is not recorded: a [`Field`] +/// already knows its own address, and the delimiters are spelling, read off the +/// syntax by [`spell::fields`](super::spell::fields). #[derive(Clone, Debug)] pub struct Struct { pub name: syn::Ident, - /// The fields, when they are a boundary surface — `Some(vec![])` for a - /// struct with none. - /// - /// `None` means **opaque**: the contents are not part of the boundary and - /// are deliberately not lowered, so a field type outside the grammar is not - /// an error. That is today's tuple struct — usable as a handle, its fields - /// never crossed by any adapter — and lowering them would turn types that - /// are ignored now into refusals. - /// - /// Whether a shape has named or positional fields is not recorded here: a - /// [`Field`] already knows its own address, and the delimiters are - /// spelling, read off `syntax` by - /// [`spell::fields`](super::spell::fields). - pub fields: Option>, + pub fields: Vec, pub origin: Origin, } -impl Struct { - /// The modelled fields — empty when the struct is opaque. - pub fn fields(&self) -> &[Field] { - self.fields.as_deref().unwrap_or(&[]) - } -} - /// A `#[prebindgen]` enum whose alternatives carry payloads — a sum type. /// /// Distinct from [`Enum`], which is the fieldless shape, because the two are @@ -259,20 +308,21 @@ pub struct Field { /// does not need it: it is addressed by name, so this is available rather /// than used — the same way it carries its item's location. pub index: usize, - pub ty: Type, + pub ty: TypeRef, /// The field as written — `pub id: u64`, attributes and docs included. pub origin: Origin, } -/// A `#[prebindgen]` const. +/// A `#[prebindgen]` constant. /// /// Also the home of the unnamed `const _` feature guard each source injects: it -/// is a const, so it is modelled as one, and [`Element::name`] returning `None` -/// for `_` is what keeps several of them from colliding in the flat namespace. +/// is a constant, so it is modelled as one, and [`Element::name`] returning +/// `None` for `_` is what keeps several of them from colliding in the flat +/// namespace. #[derive(Clone, Debug)] -pub struct Const { +pub struct Constant { pub name: syn::Ident, - pub ty: Type, + pub ty: TypeRef, /// The whole item — the initializer expression included, which is where a /// consumer that re-emits the value reads it from. pub origin: Origin, diff --git a/prebindgen/src/api/core/language/mod.rs b/prebindgen/src/api/core/flat/mod.rs similarity index 67% rename from prebindgen/src/api/core/language/mod.rs rename to prebindgen/src/api/core/flat/mod.rs index 6a018aac..730a2b02 100644 --- a/prebindgen/src/api/core/language/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -6,13 +6,13 @@ //! > (C, JNI). They are opposite ends of the pipeline. //! //! ```text -//! Source(s) ──items──> Language ──Elements──> Registry ──> adapters +//! Source(s) ──items──> Flat ──Elements──> Registry ──> adapters //! raw records parse + indexes classify off `kind` //! (syn::Item) validate elements spell off `origin` //! ``` //! -//! [`Language::source`] folds the first arrow in for the common case, so a build -//! script names one directory and gets elements; [`Language::items`] keeps the +//! [`Flat::source`] folds the first arrow in for the common case, so a build +//! script names one directory and gets elements; [`Flat::items`] keeps the //! arrow itself, for a stream that needs shaping first. //! //! # What an element is @@ -56,6 +56,8 @@ //! | `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(..)` | [`Opaque`] | a handle; contents do not cross | +//! | `&mut MaybeUninit` | [`RefMode::Out`] | an out-param slot the caller supplies | //! | no `->`, `-> ()` | [`TypeKind::Unit`] | the same function | //! | `*const T` | *rejected* | a source crate is idiomatic Rust; the adapter owns pointers | //! @@ -106,9 +108,18 @@ //! There is **no verbatim passthrough**, 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 an item kind this module does -//! not model is a `union` or a type alias, and it is diagnosed like any other -//! thing the language cannot express. +//! `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. +//! +//! # Declaring a handle +//! +//! `#[prebindgen] pub type X = path::To;` declares an [`Opaque`]: 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 //! @@ -147,19 +158,19 @@ use self::{array_len::ConstIndex, ty::lower_type}; pub use self::{ array_len::{ArrayExtent, ArrayLenReason, ConstId, ExtentSource, UnsupportedArrayLen}, element::{ - Alternative, Const, Element, Enum, EnumValue, Field, Function, Param, Struct, Unsupported, - Variant, + Alternative, Constant, Element, Enum, EnumValue, Field, Function, Opaque, Param, Struct, + Type, Unsupported, Variant, }, origin::Origin, - ty::{ScalarKind, Type, TypeId, TypeKind, UnsupportedType, UnsupportedTypeReason}, + ty::{RefMode, ScalarKind, TypeId, TypeKind, TypeRef, UnsupportedType, UnsupportedTypeReason}, }; use crate::SourceLocation; -/// The parser for the prebindgen source language. +/// Collects what to parse, then hands over the model. /// -/// Carries no configuration about what it *accepts* — that is a property of the -/// language, not of the call site. What it does carry is **what to parse**: -/// collect the inputs, then [`parse`](Self::parse) once. +/// Carries no configuration about what the language *accepts* — that is a +/// property of the language, not of the call site. What it carries is **what to +/// parse**: feed the inputs, then [`build`](Self::build) once. /// /// # Reading a source directory /// @@ -169,11 +180,12 @@ use crate::SourceLocation; /// /// ``` /// # prebindgen::Source::init_doctest_simulate(); -/// use prebindgen::core::Language; +/// use prebindgen::core::Flat; /// -/// let elements = Language::new().source("source_ffi").parse()?; -/// assert_eq!(elements.len(), 2); -/// # Ok::<_, prebindgen::core::language::ParseError>(()) +/// let flat = Flat::builder().source("source_ffi").build()?; +/// assert!(flat.function("test_function").is_some()); +/// assert!(flat.declared_type("TestStruct").is_some()); +/// # Ok::<_, prebindgen::core::flat::ParseError>(()) /// ``` /// /// # Reading a stream @@ -185,34 +197,31 @@ use crate::SourceLocation; /// /// ``` /// # prebindgen::Source::init_doctest_simulate(); -/// use prebindgen::{core::Language, Source}; +/// use prebindgen::{core::Flat, Source}; /// /// // A dependency renamed in Cargo.toml needs the name THIS crate uses, so it /// // is configured rather than named by directory. /// let helpers = Source::builder("source_ffi").crate_name("helpers").build(); -/// let elements = Language::new() +/// let flat = Flat::builder() /// .items(helpers.items_in_groups(&["functions"])) -/// .parse()?; -/// assert_eq!(elements.len(), 1); -/// # Ok::<_, prebindgen::core::language::ParseError>(()) +/// .build()?; +/// assert_eq!(flat.functions().count(), 1); +/// # Ok::<_, prebindgen::core::flat::ParseError>(()) /// ``` /// /// # Why accumulate, rather than parse each input /// /// The rules that make a parse fail are **whole-stream** rules: one flat /// namespace across every ingested crate, one const index an array length may -/// reach into, one set of source modules to normalize paths against. None can be +/// reach into, one set of source modules to normalize paths against, and every +/// type reference resolving against every declaration. None can be /// decided per input, so every input is in hand before any of it is classified. #[derive(Debug, Default)] -pub struct Language { +pub struct FlatBuilder { items: Vec<(syn::Item, SourceLocation)>, } -impl Language { - pub fn new() -> Self { - Self::default() - } - +impl FlatBuilder { /// Every `#[prebindgen]` item captured in `dir`. /// /// Sugar for [`Self::items`] over [`Source::items_all`](crate::Source::items_all), @@ -226,13 +235,12 @@ impl Language { /// /// ``` /// # prebindgen::Source::init_doctest_simulate(); - /// use prebindgen::core::Language; + /// use prebindgen::core::Flat; /// - /// let elements = Language::new().source("source_ffi").parse().unwrap(); - /// let mut names: Vec = - /// elements.iter().filter_map(|e| e.name()).map(|n| n.to_string()).collect(); - /// names.sort(); - /// assert_eq!(names, ["TestStruct", "test_function"]); + /// let flat = Flat::builder().source("source_ffi").build()?; + /// assert!(flat.function("test_function").is_some()); + /// assert!(flat.declared_type("TestStruct").is_some()); + /// # Ok::<_, prebindgen::core::flat::ParseError>(()) /// ``` pub fn source>(self, dir: P) -> Self { let source = crate::Source::new(dir); @@ -247,14 +255,14 @@ impl Language { /// /// ``` /// # prebindgen::Source::init_doctest_simulate(); - /// use prebindgen::{core::Language, Source}; + /// use prebindgen::{core::Flat, Source}; /// /// let source = Source::new("source_ffi"); - /// let elements = Language::new() + /// let flat = Flat::builder() /// .items(source.items_in_groups(&["structs"])) - /// .parse() - /// .unwrap(); - /// assert_eq!(elements.len(), 1); + /// .build()?; + /// assert_eq!(flat.types().count(), 1); + /// # Ok::<_, prebindgen::core::flat::ParseError>(()) /// ``` pub fn items(mut self, items: I) -> Self where @@ -264,16 +272,16 @@ impl Language { self } - /// Parse everything collected so far into elements. + /// Parse everything collected so far into the model. /// - /// **Transactional**: an `Err` yields no elements at all, so a refused - /// stream cannot leave a half-built model behind. + /// **Transactional**: an `Err` yields no model at all, so a refused stream + /// cannot leave a half-built one behind. /// - /// Order-independent: source modules are gathered, and consts indexed, - /// before anything is lowered — so a cross-source type reference and an - /// array length may both name something declared later, in this input or - /// another. - pub fn parse(self) -> Result, ParseError> { + /// Order-independent: source modules are gathered, consts indexed, and every + /// item lowered before any reference is resolved — so a type reference, an + /// array length and a cross-source mention may each name something declared + /// later, in this input or another. + pub fn build(self) -> Result { let mut items = self.items; // Pass 0: normalize every item's types to the canonical flat spelling @@ -312,7 +320,7 @@ impl Language { })); // Pass 2: lower, checking the flat namespace as we go. - let mut out: Vec = Vec::with_capacity(items.len()); + let mut elements: Vec = Vec::with_capacity(items.len()); let mut seen: Vec<(syn::Ident, SourceLocation)> = Vec::new(); for (item, loc) in items { let element = lower_item(item, loc, &consts); @@ -326,10 +334,233 @@ impl Language { } seen.push((name.clone(), element.location().clone())); } - out.push(element); + elements.push(element); + } + + // Pass 3: resolve references, now that every declaration is in hand. + resolve_references(&mut elements); + + // Indexed after resolution, because refusing an item can change its kind + // (a `Type` becomes `Unsupported`) though never its name. + let by_name = elements + .iter() + .enumerate() + .filter_map(|(i, e)| e.name().map(|n| (n.to_string(), i))) + .collect(); + Ok(Flat { elements, by_name }) + } +} + +/// The flat API: every `#[prebindgen]` item from every ingested source, parsed, +/// indexed by name, and with every type reference resolved. +/// +/// # Direct access, not a stream +/// +/// Names are unique across the whole model — a duplicate is a +/// [`ParseError::DuplicateName`] — so a name is a complete address, and the +/// model answers by it. That is what every later stage needs: an adapter asks +/// what a declared name *is*, rather than scanning a list for it. +/// +/// # References are already resolved +/// +/// Every [`TypeKind::Named`] in a surviving element denotes a [`Type`] this model +/// holds, and [`Self::resolve`] hands it over. An item that named something the +/// flat API does not declare is [`Element::Unsupported`] with +/// [`ItemError::UnresolvedType`] — inert until an adapter declares it, exactly +/// like every other refusal, so an item no binding uses stays harmless. +/// +/// Resolving here rather than in the adapters is the point of #211: a dangling +/// name used to surface much later as an unresolved-converter error, from +/// whichever adapter happened to look first. +#[derive(Debug)] +pub struct Flat { + /// Source order, so iteration reports items as the sources were fed. + elements: Vec, + /// Name → position in [`Self::elements`]. + /// + /// A map rather than a scan because every typed accessor and every + /// [`Self::resolve`] routes through it, and later stages resolve references + /// in a loop — a linear scan would make that quadratic in the size of the + /// API. Positions rather than clones, so there is one copy of each element + /// and source order stays available. + by_name: std::collections::HashMap, +} + +impl Flat { + /// Start collecting what to parse. + pub fn builder() -> FlatBuilder { + FlatBuilder { items: Vec::new() } + } + + /// Every element, in the order the sources were fed. + pub fn elements(&self) -> impl Iterator { + self.elements.iter() + } + + /// The element with this name, whatever kind it is — including an + /// [`Element::Unsupported`], which still holds its name against the + /// namespace. + pub fn element(&self, name: &str) -> Option<&Element> { + self.elements.get(*self.by_name.get(name)?) + } + + pub fn function(&self, name: &str) -> Option<&Function> { + match self.element(name)? { + Element::Function(f) => Some(f), + _ => None, + } + } + + /// The type declared under this name. + /// + /// Named `declared_type` because `type` is a keyword; it is the accessor a + /// resolved [`TypeKind::Named`] reference leads to, and [`Self::resolve`] is + /// the same lookup taking a [`TypeId`]. + pub fn declared_type(&self, name: &str) -> Option<&Type> { + match self.element(name)? { + Element::Type(t) => Some(t), + _ => None, + } + } + + pub fn constant(&self, name: &str) -> Option<&Constant> { + match self.element(name)? { + Element::Constant(c) => Some(c), + _ => None, + } + } + + pub fn functions(&self) -> impl Iterator { + self.elements.iter().filter_map(|e| match e { + Element::Function(f) => Some(f), + _ => None, + }) + } + + pub fn types(&self) -> impl Iterator { + self.elements.iter().filter_map(|e| match e { + Element::Type(t) => Some(t), + _ => None, + }) + } + + pub fn constants(&self) -> impl Iterator { + self.elements.iter().filter_map(|e| match e { + Element::Constant(c) => Some(c), + _ => None, + }) + } + + /// Every item the language could not express, with its diagnosis. + /// + /// An adapter raises one of these when it declares the item; until then they + /// are inert. See the [module docs](self) on where acceptance is enforced. + pub fn unsupported(&self) -> impl Iterator { + self.elements.iter().filter_map(|e| match e { + Element::Unsupported(u) => Some(u), + _ => None, + }) + } + + /// The declaration a reference denotes. + /// + /// Infallible in practice for any reference reached from a surviving element: + /// [`FlatBuilder::build`] made unresolvable references into + /// [`ItemError::UnresolvedType`], so what is left resolves. + pub fn resolve(&self, id: &TypeId) -> Option<&Type> { + self.declared_type(&id.name) + } +} + +/// Turn every element that names an undeclared type into an +/// [`Element::Unsupported`], **transitively**. +/// +/// Runs once every declaration is in hand, so the order sources were fed in does +/// not matter and a reference may point forward or across crates. +/// +/// # Why this iterates +/// +/// Refusing a type *removes a declaration*, which can strand its dependents: +/// +/// ```ignore +/// pub struct Broken { pub field: Missing } // refused: `Missing` undeclared +/// pub fn use_broken(value: Broken) {} // `Broken` is now gone too +/// ``` +/// +/// A single pass against a snapshot of the initial declarations would keep +/// `use_broken`, and [`Flat::resolve`] would then return `None` for its parameter +/// — breaking the invariant that a surviving element's references all resolve. +/// So this runs to a fixed point: each round drops the declarations it refused, +/// and stops when a round refuses nothing. Chains of any length collapse, in +/// either declaration order, because the set only ever shrinks. +fn resolve_references(elements: &mut [Element]) { + let mut declared: std::collections::HashSet = elements + .iter() + .filter_map(|e| match e { + Element::Type(t) => Some(t.name().to_string()), + _ => None, + }) + .collect(); + + loop { + let mut refused = Vec::new(); + for (i, element) in elements.iter().enumerate() { + if let Some(unresolved) = first_unresolved(element, &declared) { + refused.push((i, unresolved)); + } + } + if refused.is_empty() { + return; + } + for (i, unresolved) in refused { + // A refused type stops being a declaration, which is what lets the + // next round see its dependents as unresolved. + if let Element::Type(t) = &elements[i] { + declared.remove(&t.name().to_string()); + } + let element = &mut elements[i]; + let name = element.name().cloned(); + let origin = Origin::new( + element.syntax(), + Rc::clone(match element { + Element::Function(f) => &f.origin.location, + Element::Type(t) => t.location_rc(), + Element::Constant(c) => &c.origin.location, + Element::Unsupported(u) => &u.origin.location, + }), + ); + *element = Element::Unsupported(Unsupported { + name, + error: Box::new(ItemError::UnresolvedType { name: unresolved }), + origin, + }); + } + } +} + +/// The first type this element names that the flat API does not declare. +fn first_unresolved( + element: &Element, + declared: &std::collections::HashSet, +) -> Option { + let mut refs: Vec<&TypeRef> = Vec::new(); + match element { + Element::Function(f) => { + refs.extend(f.params.iter().map(|p| &p.ty)); + refs.push(&f.ret); } - Ok(out) + Element::Constant(c) => refs.push(&c.ty), + Element::Type(Type::Struct(s)) => refs.extend(s.fields.iter().map(|f| &f.ty)), + Element::Type(Type::Variant(v)) => refs.extend( + v.alternatives + .iter() + .flat_map(|a| a.fields.iter().map(|f| &f.ty)), + ), + // An enum names nothing, an opaque hides what it names, and an + // unsupported item already has a diagnosis worth keeping. + Element::Type(Type::Enum(_) | Type::Opaque(_)) | Element::Unsupported(_) => {} } + refs.into_iter().find_map(|r| r.first_unresolved(declared)) } /// If `ty` is `impl Fn(T1, T2, ...) + Send + Sync + 'static`, return the `Fn` @@ -476,6 +707,13 @@ pub enum ItemError { /// `a type parameter` / `a const generic parameter`. kind: &'static str, }, + /// The item names a type the flat API does not declare. + /// + /// The flat API is closed over its own names: a handle enters it through + /// `#[prebindgen] pub type X = ..`, so a name with no declaration is either a + /// missing marker or a typo. Reporting it here replaces discovering it much + /// later as an unresolved converter, from whichever adapter looked first. + UnresolvedType { name: String }, /// A whole item kind the language does not model — a `union`, a type alias. /// /// The proc-macro refuses to mark a `use`, `mod`, `impl` or `macro_rules!` @@ -524,6 +762,19 @@ impl fmt::Display for ItemError { of the same name and no destination language can express it — write the \ concrete types, one marked item per instantiation (a newtype is the usual way)" ), + ItemError::UnresolvedType { name } if name.contains("::") => write!( + f, + "names the type `{name}`, which the flat API does not declare \u{2014} and being \ + path-qualified it never could, because marked items live in one flat namespace \ + of bare names. Give the type a name here with `#[prebindgen] pub type = \ + {name};` and refer to that" + ), + ItemError::UnresolvedType { name } => write!( + f, + "names the type `{name}`, which the flat API does not declare \u{2014} mark its \ + declaration `#[prebindgen]`, or, for a foreign or crate-private type used as a \ + handle, give it a name here with `#[prebindgen] pub type {name} = ..;`" + ), ItemError::UnsupportedItemKind { kind } => write!( f, "is {kind}; the prebindgen source language models functions, structs, enums and \ @@ -548,19 +799,35 @@ fn lower_item(item: syn::Item, loc: SourceLocation, consts: &ConstIndex) -> Elem Err(error) => unsupported(f.sig.ident.clone(), syn::Item::Fn(f), &at, error), }, syn::Item::Struct(s) => match lower_struct(&s, &at, consts) { - Ok(st) => Element::Struct(st), + Ok(ty) => Element::Type(ty), Err(error) => unsupported(s.ident.clone(), syn::Item::Struct(s), &at, error), }, syn::Item::Enum(e) => match lower_enum(&e, &at, consts) { - Ok(element) => element, + Ok(ty) => Element::Type(ty), Err(error) => unsupported(e.ident.clone(), syn::Item::Enum(e), &at, error), }, + // `#[prebindgen] pub type X = path;` DECLARES an opaque type: it gives a + // foreign or crate-private type a name in the flat API, without claiming + // anything about its contents. That is the only way a handle enters the + // API deliberately, and the reason references can be required to resolve. + syn::Item::Type(t) => match reject_generic_params(&t.generics) { + // `Opaque` has no binder and no arity, so a generic alias would be + // accepted as one declaration that `Handle` then resolves against + // — losing exactly the scoped-parameter distinction every other item + // kind refuses. It is also why `MaybeUninit` needed grammar support + // rather than an alias. + Err(error) => unsupported(t.ident.clone(), syn::Item::Type(t), &at, error), + Ok(()) => Element::Type(Type::Opaque(Opaque { + name: t.ident.clone(), + origin: Origin::new(syn::Item::Type(t), at), + })), + }, // Including the unnamed `const _` each source injects as its feature // guard: it is a const, so it is one here. `Element::name` returns // `None` for `_`, which is what keeps several sources' guards from // colliding in the flat namespace. syn::Item::Const(c) => match lower_type(&c.ty, consts, &at) { - Ok(ty) => Element::Const(Const { + Ok(ty) => Element::Constant(Constant { name: c.ident.clone(), ty, origin: Origin::new(c, at), @@ -572,15 +839,14 @@ fn lower_item(item: syn::Item, loc: SourceLocation, consts: &ConstIndex) -> Elem ItemError::ConstType { source }, ), }, - // An item kind the language does not model. The proc-macro accepts - // only six kinds, so in practice this is a `union` or a type alias — - // both named, neither ever written by a source crate. It is diagnosed - // rather than carried: a `#[prebindgen]` crate marks what crosses the - // boundary, and the code around that belongs to the consumer. + // An item kind the language does not model. The proc-macro accepts only + // six kinds and the five above cover the rest, so in practice this is a + // `union` — never written by any source crate. It is diagnosed rather + // than carried: a `#[prebindgen]` crate marks what crosses the boundary, + // and the code around that belongs to the consumer. other => { let (name, kind) = match &other { syn::Item::Union(u) => (Some(u.ident.clone()), "a union"), - syn::Item::Type(t) => (Some(t.ident.clone()), "a type alias"), _ => (None, "an item kind"), }; unsupported(name, other, &at, ItemError::UnsupportedItemKind { kind }) @@ -657,7 +923,7 @@ fn lower_fn( // says so once, here, instead of leaving every consumer to normalize one to // the other — which is what they all do today, in eight separate copies. let ret = match &f.sig.output { - syn::ReturnType::Default => Type { + syn::ReturnType::Default => TypeRef { kind: TypeKind::Unit, origin: Origin::new(syn::parse_quote!(()), Rc::clone(at)), }, @@ -673,11 +939,16 @@ fn lower_fn( }) } +/// Lower a `struct` item to whichever of the two shapes it is. +/// +/// A **tuple struct** is an [`Opaque`]: no adapter has ever crossed its fields, +/// so they are deliberately not lowered and a field type outside the grammar is +/// not an error. Anything else is a product of fields that do cross. fn lower_struct( s: &syn::ItemStruct, at: &Rc, consts: &ConstIndex, -) -> Result { +) -> Result { reject_generic_params(&s.generics)?; let fields = match &s.fields { syn::Fields::Named(named) => { @@ -695,18 +966,22 @@ fn lower_struct( origin: Origin::new(f.clone(), Rc::clone(at)), }); } - Some(out) + out } - // Opaque: a tuple struct's contents are not a boundary surface, so they - // are not lowered and a field type outside the grammar is not an error. - syn::Fields::Unnamed(_) => None, - syn::Fields::Unit => Some(Vec::new()), + // Its contents are not a boundary surface, so nothing is lowered. + syn::Fields::Unnamed(_) => { + return Ok(Type::Opaque(Opaque { + name: s.ident.clone(), + origin: Origin::new(syn::Item::Struct(s.clone()), Rc::clone(at)), + })) + } + syn::Fields::Unit => Vec::new(), }; - Ok(Struct { + Ok(Type::Struct(Struct { name: s.ident.clone(), fields, origin: Origin::new(s.clone(), Rc::clone(at)), - }) + })) } /// Lower an `enum` item to whichever of the two shapes it is. @@ -723,13 +998,13 @@ fn lower_enum( e: &syn::ItemEnum, at: &Rc, consts: &ConstIndex, -) -> Result { +) -> Result { reject_generic_params(&e.generics)?; if e.variants.iter().any(|v| !v.fields.is_empty()) { - return Ok(Element::Variant(lower_variant(e, at, consts)?)); + return Ok(Type::Variant(lower_variant(e, at, consts)?)); } - Ok(Element::Enum(lower_c_enum(e, at))) + Ok(Type::Enum(lower_c_enum(e, at))) } /// The payload-carrying shape. Position is the only numbering a sum has, so no diff --git a/prebindgen/src/api/core/language/origin.rs b/prebindgen/src/api/core/flat/origin.rs similarity index 100% rename from prebindgen/src/api/core/language/origin.rs rename to prebindgen/src/api/core/flat/origin.rs diff --git a/prebindgen/src/api/core/language/spell.rs b/prebindgen/src/api/core/flat/spell.rs similarity index 100% rename from prebindgen/src/api/core/language/spell.rs rename to prebindgen/src/api/core/flat/spell.rs diff --git a/prebindgen/src/api/core/language/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs similarity index 61% rename from prebindgen/src/api/core/language/tests/acceptance.rs rename to prebindgen/src/api/core/flat/tests/acceptance.rs index 39eee2ac..703d6b6c 100644 --- a/prebindgen/src/api/core/language/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -8,14 +8,21 @@ use super::*; /// Lower one type by putting it in a struct field, and report what the language /// made of it. The field path is used because a field is the position every /// consumer already agrees is a boundary surface. -fn lower(ty: proc_macro2::TokenStream) -> Result { +fn lower(ty: proc_macro2::TokenStream) -> Result { let item: syn::Item = syn::parse_quote!( pub struct S { pub f: #ty, } ); - match parse(vec![tag_len_const(), item]).remove(1) { - Element::Struct(s) => Ok(s.fields()[0].ty.clone()), + // The fixture types stand in for a declared type wherever the grammar needs + // a nominal one, so references resolve and the test is about the grammar. + let mut items = fixture_types(); + items.push(tag_len_const()); + items.push(opaque("Sample")); + let n = items.len(); + items.push(item); + match parse(items).remove(n) { + Element::Type(Type::Struct(s)) => Ok(s.fields[0].ty.clone()), Element::Unsupported(u) => match *u.error { ItemError::FieldType { source, .. } => Err(source), other => panic!("expected a field-type diagnosis, got {other}"), @@ -60,10 +67,10 @@ fn scalars_and_strings() { fn a_string_is_a_string_however_it_is_spelled() { assert!(matches!(kind(quote::quote!(str)), TypeKind::Str)); for spelling in [quote::quote!(&str), quote::quote!(&String)] { - let TypeKind::Ref { mutable, inner } = kind(spelling) else { + let TypeKind::Ref { mode, inner } = kind(spelling) else { panic!("a borrow"); }; - assert!(!mutable); + assert_eq!(mode, RefMode::Shared); assert!(matches!(inner.kind, TypeKind::Str)); } } @@ -113,21 +120,40 @@ fn a_qualified_builtin_is_a_named_type() { kind(quote::quote!(std::option::Option)), TypeKind::Optional(_) )); - let TypeKind::Named { id, .. } = kind(quote::quote!(foreign::Option)) else { - panic!("a named type"); + + // Not an `Option` — and, being path-qualified, it cannot name a flat-API item + // either, so it is refused as unresolved rather than silently retyped. + let element = { + let mut items = fixture_types(); + let n = items.len(); + items.push(syn::parse_quote!( + pub struct S { + pub f: foreign::Option, + } + )); + parse(items).remove(n) }; - assert_eq!(id.name, "foreign::Option"); + assert!(matches!( + as_unsupported(&element), + ItemError::UnresolvedType { name } if name == "foreign::Option" + )); } #[test] fn references() { assert!(matches!( kind(quote::quote!(&Sample)), - TypeKind::Ref { mutable: false, .. } + TypeKind::Ref { + mode: RefMode::Shared, + .. + } )); assert!(matches!( kind(quote::quote!(&mut Sample)), - TypeKind::Ref { mutable: true, .. } + TypeKind::Ref { + mode: RefMode::Exclusive, + .. + } )); } @@ -289,7 +315,7 @@ fn an_extent_may_name_a_const_declared_later() { tag_len_const(), ]); assert_eq!( - as_struct(&elements[0]).fields()[0] + as_struct(&elements[0]).fields[0] .ty .array_extent() .expect("an extent") @@ -320,7 +346,7 @@ fn the_three_extent_projections_are_independent() { pub const ALSO_FOUR: usize = 4; ), ]); - let fields = as_struct(&elements[0]).fields(); + let fields = &as_struct(&elements[0]).fields; let at = |i: usize| fields[i].ty.array_extent().expect("an extent"); let (by_const, by_other_const, by_literal, by_hex, longer) = (at(0), at(1), at(2), at(3), at(4)); @@ -396,8 +422,8 @@ fn a_computed_const_is_indexed_but_is_not_a_length() { /// A struct is a product of fields, or opaque. A tuple struct is the opaque /// one: usable as a handle, its fields deliberately not lowered, because no /// adapter has ever crossed them and lowering would turn types that are ignored -/// today into errors. A unit struct is the empty product, not a third shape — -/// the delimiters are spelling, and `spell` reads them off the syntax. +/// today into errors. A unit struct is the empty product, not a handle — the +/// delimiters are spelling, and `spell` reads them off the syntax. #[test] fn struct_shapes() { let named = parse_one(syn::parse_quote!( @@ -405,19 +431,22 @@ fn struct_shapes() { pub x: u8, } )); - assert_eq!(as_struct(&named).fields().len(), 1); + assert_eq!(as_struct(&named).fields.len(), 1); + // A tuple struct is a handle, so its fields are never lowered — which is why + // a field type outside the grammar is not an error here. let tuple = parse_one(syn::parse_quote!( pub struct B(SomethingUnexpressible<'_, dyn Trait>); )); - assert!(as_struct(&tuple).fields.is_none(), "opaque"); - assert!(as_struct(&tuple).fields().is_empty()); + assert_eq!(as_opaque(&tuple).name, "B"); let unit = parse_one(syn::parse_quote!( pub struct C; )); - assert!(as_struct(&unit).fields.is_some(), "empty, not opaque"); - assert!(as_struct(&unit).fields().is_empty()); + assert!( + as_struct(&unit).fields.is_empty(), + "an empty product, not a handle" + ); } /// A variant's index is its declaration order and is never its discriminant: @@ -544,40 +573,56 @@ fn an_unnamed_const_is_a_const_without_an_address() { const _: () = (); ), ]); - assert!(elements.iter().all(|e| matches!(e, Element::Const(_)))); + assert!(elements.iter().all(|e| matches!(e, Element::Constant(_)))); assert!(elements.iter().all(|e| e.name().is_none())); } /// An item kind the language does not model is diagnosed, not carried: a /// `#[prebindgen]` crate marks what crosses the boundary and leaves the code -/// around it to the consumer. The proc-macro refuses to mark a `use` at all, so -/// only a `union` or a type alias can reach here — and both keep their name, so -/// nothing else can claim it. +/// around it to the consumer. The proc-macro refuses to mark a `use` at all, and +/// a type alias now *declares* an opaque handle — so a `union` is the only kind +/// left that reaches here. #[test] fn an_unmodelled_item_kind_is_diagnosed() { - for (item, expected) in [ - ( - syn::parse_quote!( - pub union U { - a: u8, - } - ), - "a union", - ), - ( - syn::parse_quote!( - pub type Alias = u32; - ), - "a type alias", - ), - ] { - let element = parse_one(item); - assert!(element.name().is_some(), "keeps its address"); - assert!(matches!( - as_unsupported(&element), - ItemError::UnsupportedItemKind { kind } if *kind == expected - )); - } + let element = parse_one(syn::parse_quote!( + pub union U { + a: u8, + } + )); + assert!(element.name().is_some(), "keeps its address"); + assert!(matches!( + as_unsupported(&element), + ItemError::UnsupportedItemKind { kind } if *kind == "a union" + )); +} + +/// A marked type alias DECLARES an opaque handle — the way a foreign or +/// crate-private type gets a name in the flat API. That is what lets references +/// be required to resolve, so it is the keystone of the whole model. +#[test] +fn a_marked_alias_declares_an_opaque() { + let element = parse_one(syn::parse_quote!( + pub type Session = zenoh::Session; + )); + let o = as_opaque(&element); + assert_eq!(o.name, "Session"); + // The whole item survives, so a consumer can still read what it aliased. + assert_eq!( + tokens(&o.origin.syntax), + "pub type Session = zenoh :: Session ;" + ); + + // And it satisfies a reference, which is the point. + let mut items = fixture_types(); + items.push(syn::parse_quote!( + pub type Session = zenoh::Session; + )); + let n = items.len(); + items.push(syn::parse_quote!( + pub fn session_close(s: Session) {} + )); + let elements = parse(items); + assert!(matches!(elements[n], Element::Function(_))); } // ── Functions ────────────────────────────────────────────────────────── @@ -714,8 +759,8 @@ fn a_lifetime_binder_is_accepted() { } )); let s = as_struct(&element); - assert_eq!(s.fields().len(), 1); - assert_eq!(tokens(&s.fields()[0].ty.origin.syntax), "& 'a str"); + assert_eq!(s.fields.len(), 1); + assert_eq!(tokens(&s.fields[0].ty.origin.syntax), "& 'a str"); } /// `impl Trait` in argument position is an anonymous type parameter in Rust, but @@ -812,6 +857,435 @@ fn an_unsupported_item_is_indexed_not_refused() { assert!(matches!(elements[1], Element::Function(_))); } +// ── Resolution and access ────────────────────────────────────────────── + +/// The model answers by name, which is what every later stage needs. An +/// unsupported item is still reachable — it holds its slot in the namespace — +/// but it is not a type. +#[test] +fn the_model_is_addressed_by_name() { + let flat = Flat::builder() + .items( + vec![ + syn::parse_quote!( + pub type Session = zenoh::Session; + ), + syn::parse_quote!( + pub const LIMIT: usize = 4; + ), + syn::parse_quote!( + pub fn session_close(s: Session) {} + ), + syn::parse_quote!( + pub union U { + a: u8, + } + ), + ] + .into_iter() + .map(|i: syn::Item| (i, loc())), + ) + .build() + .expect("parses"); + + assert!(flat.function("session_close").is_some()); + assert!(flat.declared_type("Session").is_some()); + assert!(flat.constant("LIMIT").is_some()); + assert_eq!(flat.functions().count(), 1); + assert_eq!(flat.types().count(), 1); + assert_eq!(flat.constants().count(), 1); + + // Reachable, but not a type — it holds its name so nothing else can claim it. + assert!(flat.element("U").is_some()); + assert!(flat.declared_type("U").is_none()); + assert_eq!(flat.unsupported().count(), 1); + + // A name nobody declared is simply absent. + assert!(flat.element("nope").is_none()); +} + +/// A reference leads to the declaration it names. Resolving here is the point of +/// #211: a dangling name used to surface much later, as an unresolved converter +/// from whichever adapter looked first. +#[test] +fn a_reference_resolves_to_its_declaration() { + let flat = Flat::builder() + .items( + vec![ + syn::parse_quote!( + pub type Session = zenoh::Session; + ), + syn::parse_quote!( + pub fn session_close(s: Session) {} + ), + ] + .into_iter() + .map(|i: syn::Item| (i, loc())), + ) + .build() + .expect("parses"); + + let f = flat.function("session_close").expect("declared"); + let TypeKind::Named { id, .. } = &f.params[0].ty.kind else { + panic!("a nominal type"); + }; + let target = flat.resolve(id).expect("resolves"); + assert!(matches!(target, Type::Opaque(_))); + assert_eq!(target.name(), "Session"); +} + +/// Resolution runs once every declaration is in hand, so a reference may point +/// forward, or into another source entirely — which is how a helper crate names +/// types it cannot mark itself. +#[test] +fn resolution_spans_feeders_and_declaration_order() { + // Forward reference within one feeder. + let flat = Flat::builder() + .items( + vec![ + syn::parse_quote!( + pub fn session_close(s: Session) {} + ), + syn::parse_quote!( + pub type Session = zenoh::Session; + ), + ] + .into_iter() + .map(|i: syn::Item| (i, loc())), + ) + .build() + .expect("a forward reference resolves"); + assert!(flat.function("session_close").is_some()); + + // And across feeders: the declaration arrives in the second stream. + let flat = Flat::builder() + .items(vec![( + syn::parse_quote!( + pub fn session_close(s: Session) {} + ), + loc(), + )]) + .items(vec![( + syn::parse_quote!( + pub type Session = zenoh::Session; + ), + loc(), + )]) + .build() + .expect("a cross-feeder reference resolves"); + assert!(flat.function("session_close").is_some()); +} + +/// A name the flat API does not declare makes the *referencing* item +/// unsupported, inert until an adapter declares it — the same deferral every +/// other refusal uses, so an item no binding touches stays harmless. +#[test] +fn an_undeclared_reference_refuses_the_referencing_item() { + let flat = Flat::builder() + .items(vec![( + syn::parse_quote!( + pub fn session_close(s: Session) {} + ), + loc(), + )]) + .build() + .expect("a refusal is deferred, not fatal"); + + assert!(flat.function("session_close").is_none()); + let u = flat.unsupported().next().expect("one refusal"); + assert!(matches!( + &*u.error, + ItemError::UnresolvedType { name } if name == "Session" + )); + // It still holds its name, so nothing else can claim it. + assert_eq!(u.name.as_ref().expect("named"), "session_close"); + + // Reachable through every layer a reference can nest in. + for ty in [ + quote::quote!(Option), + quote::quote!(Vec), + quote::quote!(&Session), + quote::quote!(Result), + quote::quote!(impl Fn(Session) + Send + Sync + 'static), + quote::quote!([Session; 4]), + quote::quote!(Wrapper), + ] { + let flat = Flat::builder() + .items(vec![ + (opaque("Error"), loc()), + (opaque("Wrapper"), loc()), + ( + syn::parse_quote!( + pub fn f(s: #ty) {} + ), + loc(), + ), + ]) + .build() + .expect("deferred"); + assert_eq!( + flat.unsupported().count(), + 1, + "`Session` must be found inside {}", + ty + ); + } +} + +/// An out-parameter is a **mode of borrowing**, not a type. `&mut MaybeUninit` +/// says the caller supplies the slot and the callee fills it; the `MaybeUninit` is +/// absorbed into the mode, so `inner` is the value's own type. +/// +/// Uninitialized storage anywhere else promises nothing a destination language can +/// use, so the combinations that mean nothing cannot be written down. +#[test] +fn an_out_parameter_is_a_borrow_mode() { + let TypeKind::Ref { mode, inner } = kind(quote::quote!(&mut MaybeUninit)) else { + panic!("a borrow"); + }; + assert_eq!(mode, RefMode::Out); + // The `MaybeUninit` is gone from the type: it described the borrow. + let TypeKind::Named { id, .. } = &inner.kind else { + panic!("the value's own type"); + }; + assert_eq!(id.name, "Sample"); + + // The three modes are one axis. + assert_eq!( + [ + quote::quote!(&Sample), + quote::quote!(&mut Sample), + quote::quote!(&mut MaybeUninit), + ] + .map(|t| match kind(t) { + TypeKind::Ref { mode, .. } => mode, + other => panic!("a borrow, got {other:?}"), + }), + [RefMode::Shared, RefMode::Exclusive, RefMode::Out] + ); + + // Owned, or shared-borrowed, it means nothing. + assert_eq!( + reason(quote::quote!(MaybeUninit)), + UnsupportedTypeReason::OwnedUninit + ); + assert_eq!( + reason(quote::quote!(&MaybeUninit)), + UnsupportedTypeReason::SharedUninit + ); + + // And it still needs no declaration, unlike every other generic-bearing name. + let flat = Flat::builder() + .items(vec![ + (opaque("Sample"), loc()), + ( + syn::parse_quote!( + pub fn get(out: &mut MaybeUninit) -> bool {} + ), + loc(), + ), + ]) + .build() + .expect("parses"); + assert!(flat.function("get").is_some()); +} + +/// Refusing a type removes a *declaration*, so its dependents must be refused +/// too — otherwise a surviving element would hold a reference that resolves to +/// nothing, which is the one invariant the model promises. +/// +/// Checked in both declaration orders, because a single pass against a snapshot +/// of the initial declarations keeps the dependent whichever way round it is. +#[test] +fn refusal_is_transitive() { + let broken: syn::Item = syn::parse_quote!( + pub struct Broken { + pub field: Missing, + } + ); + let user: syn::Item = syn::parse_quote!( + pub fn use_broken(value: Broken) {} + ); + + for (label, items) in [ + ("declaration first", vec![broken.clone(), user.clone()]), + ("dependent first", vec![user, broken]), + ] { + let flat = Flat::builder() + .items(items.into_iter().map(|i| (i, loc()))) + .build() + .expect("deferred, not fatal"); + + assert!( + flat.declared_type("Broken").is_none(), + "{label}: `Missing` is undeclared" + ); + assert!( + flat.function("use_broken").is_none(), + "{label}: `Broken` is no longer a declaration either" + ); + assert_eq!(flat.unsupported().count(), 2, "{label}"); + // Both still hold their names against the namespace. + assert!(flat.element("Broken").is_some(), "{label}"); + assert!(flat.element("use_broken").is_some(), "{label}"); + } +} + +/// And through a chain of any length, in either direction — the fixed point only +/// ever shrinks the declared set, so it terminates and misses no hop. +#[test] +fn refusal_is_transitive_through_a_chain() { + let chain: Vec = vec![ + syn::parse_quote!( + pub struct A { + pub field: Missing, + } + ), + syn::parse_quote!( + pub struct B { + pub field: A, + } + ), + syn::parse_quote!( + pub struct C { + pub field: B, + } + ), + syn::parse_quote!( + pub fn takes_c(value: C) {} + ), + ]; + + for (label, items) in [ + ("forward", chain.clone()), + ("reversed", chain.into_iter().rev().collect()), + ] { + let flat = Flat::builder() + .items(items.into_iter().map(|i| (i, loc()))) + .build() + .expect("deferred"); + assert_eq!( + flat.types().count(), + 0, + "{label}: the whole chain collapses" + ); + assert_eq!(flat.functions().count(), 0, "{label}"); + assert_eq!(flat.unsupported().count(), 4, "{label}"); + } + + // A sound chain is untouched, so the fixed point is not just refusing + // everything reachable. + let flat = Flat::builder() + .items( + vec![ + opaque("Missing"), + syn::parse_quote!( + pub struct A { + pub field: Missing, + } + ), + syn::parse_quote!( + pub fn takes_a(value: A) {} + ), + ] + .into_iter() + .map(|i: syn::Item| (i, loc())), + ) + .build() + .expect("parses"); + assert_eq!(flat.unsupported().count(), 0); + assert!(flat.function("takes_a").is_some()); +} + +/// Every reference reachable from a *surviving* element resolves. That is what +/// the transitive pass buys, and what `resolve` relies on. +#[test] +fn every_surviving_reference_resolves() { + let flat = Flat::builder() + .items( + vec![ + opaque("Missing"), + syn::parse_quote!( + pub struct Held { + pub field: Missing, + } + ), + syn::parse_quote!( + pub fn takes_held(value: Held) -> Held {} + ), + // ... alongside a chain that does collapse. + syn::parse_quote!( + pub struct Broken { + pub field: Absent, + } + ), + syn::parse_quote!( + pub fn takes_broken(value: Broken) {} + ), + ] + .into_iter() + .map(|i: syn::Item| (i, loc())), + ) + .build() + .expect("deferred"); + + for f in flat.functions() { + for r in f.params.iter().map(|p| &p.ty).chain([&f.ret]) { + if let TypeKind::Named { id, .. } = &r.kind { + assert!( + flat.resolve(id).is_some(), + "`{}` must resolve from a surviving function", + id.name + ); + } + } + } + assert!(flat.function("takes_held").is_some()); + assert!(flat.function("takes_broken").is_none()); +} + +/// A generic alias is a generic binder like any other item's, and `Opaque` has no +/// binder or arity — so accepting one would let `Handle` resolve against a +/// declaration that says nothing about its parameter. It is also why +/// `MaybeUninit` needed grammar support rather than an alias. +#[test] +fn a_generic_alias_is_refused() { + for (item, param, kind_str) in [ + ( + syn::parse_quote!( + pub type Handle = hidden::Handle; + ), + "T", + "a type parameter", + ), + ( + syn::parse_quote!( + pub type Padded = hidden::Padded; + ), + "N", + "a const generic parameter", + ), + ] { + let element = parse_one(item); + let ItemError::UnsupportedGenericParam { param: got, kind } = as_unsupported(&element) + else { + panic!( + "expected a generic-parameter diagnosis, got {}", + describe(&element) + ); + }; + assert_eq!(got, param); + assert_eq!(*kind, kind_str); + } + + // A lifetime binder stays accepted, as it is on every other item kind: + // lifetimes are spelling and the spelling already travels. + let element = parse_one(syn::parse_quote!( + pub type Borrowed<'a> = hidden::Borrowed<'a>; + )); + assert_eq!(as_opaque(&element).name, "Borrowed"); +} + // ── The flat namespace ───────────────────────────────────────────────── /// The feeders accumulate, and the whole-stream rules span them. @@ -828,14 +1302,15 @@ fn the_feeders_accumulate_and_whole_stream_rules_span_them() { ); // A length in the first feeder naming a const from the second. - let elements = Language::new() + let flat = Flat::builder() .items(vec![(marker.clone(), loc())]) .items(vec![(tag_len_const(), loc())]) - .parse() + .build() .expect("the const is found across feeders"); + let elements: Vec = flat.elements().cloned().collect(); assert_eq!(elements.len(), 2); assert_eq!( - as_struct(&elements[0]).fields()[0] + as_struct(&elements[0]).fields[0] .ty .array_extent() .expect("an extent") @@ -844,10 +1319,10 @@ fn the_feeders_accumulate_and_whole_stream_rules_span_them() { ); // And a name colliding across feeders is still the one hard failure. - let err = Language::new() + let err = Flat::builder() .items(vec![(marker.clone(), loc())]) .items(vec![(marker, loc())]) - .parse() + .build() .expect_err("a duplicate across feeders is still a duplicate"); let ParseError::DuplicateName(d) = err; assert_eq!(d.name, "Marker"); diff --git a/prebindgen/src/api/core/language/tests/mod.rs b/prebindgen/src/api/core/flat/tests/mod.rs similarity index 52% rename from prebindgen/src/api/core/language/tests/mod.rs rename to prebindgen/src/api/core/flat/tests/mod.rs index 0967b404..f6b70884 100644 --- a/prebindgen/src/api/core/language/tests/mod.rs +++ b/prebindgen/src/api/core/flat/tests/mod.rs @@ -18,10 +18,35 @@ mod roundtrip; /// Parse one item, stamped with an origin crate so array extents can name /// `#[prebindgen]` consts from "their own" crate. +/// +/// The fixture types are declared alongside, so a test naming `Sample` or +/// `KeyExpr` is about whatever it is testing rather than about resolution. fn parse_one(item: syn::Item) -> Element { - let mut out = parse(vec![item]); - assert_eq!(out.len(), 1); - out.remove(0) + let mut items = fixture_types(); + let n = items.len(); + items.push(item); + let mut out = parse(items); + assert_eq!(out.len(), n + 1); + out.remove(n) +} + +/// Names that stand in for "a type exists" across the tests. Declared as opaque +/// handles, since none of them is the subject of the test that names it. +/// +/// Deliberately excludes `Sample`, which several tests *declare* themselves — +/// declaring it here too would be a duplicate name. `lower` adds it, because a +/// type-grammar test only ever references it. +fn fixture_types() -> Vec { + [ + "Error", + "KeyExpr", + "Foo", + "Whatever", + "SomethingUnexpressible", + ] + .into_iter() + .map(opaque) + .collect() } /// Parse a whole stream, all items stamped with the same origin crate. @@ -30,9 +55,10 @@ fn parse(items: Vec) -> Vec { } fn try_parse(items: Vec) -> Result, ParseError> { - Language::new() + Flat::builder() .items(items.into_iter().map(|i| (i, loc()))) - .parse() + .build() + .map(|flat| flat.elements().cloned().collect()) } fn loc() -> SourceLocation { @@ -51,6 +77,15 @@ fn tag_len_const() -> syn::Item { ) } +/// A marked alias declaring `name` as an opaque handle — the fixture for +/// "some type exists under this name", now that references must resolve. +fn opaque(name: &str) -> syn::Item { + let ident = quote::format_ident!("{name}"); + syn::parse_quote!( + pub type #ident = other::#ident; + ) +} + /// Whitespace-insensitive token comparison, so a test states what the tokens /// are rather than how they were spaced. fn tokens(t: &impl ToTokens) -> String { @@ -65,31 +100,45 @@ fn as_fn(e: &Element) -> &Function { } } -fn as_struct(e: &Element) -> &Struct { +fn as_type(e: &Element) -> &Type { match e { - Element::Struct(s) => s, - other => panic!("expected a struct, got {}", describe(other)), + Element::Type(t) => t, + other => panic!("expected a type, got {}", describe(other)), + } +} + +fn as_struct(e: &Element) -> &Struct { + match as_type(e) { + Type::Struct(s) => s, + other => panic!("expected a struct, got {}", describe_type(other)), } } fn as_enum(e: &Element) -> &Enum { - match e { - Element::Enum(en) => en, - other => panic!("expected a fieldless enum, got {}", describe(other)), + match as_type(e) { + Type::Enum(en) => en, + other => panic!("expected a fieldless enum, got {}", describe_type(other)), } } fn as_variant(e: &Element) -> &Variant { - match e { - Element::Variant(v) => v, - other => panic!("expected a sum, got {}", describe(other)), + match as_type(e) { + Type::Variant(v) => v, + other => panic!("expected a sum, got {}", describe_type(other)), } } -fn as_const(e: &Element) -> &Const { +fn as_opaque(e: &Element) -> &Opaque { + match as_type(e) { + Type::Opaque(o) => o, + other => panic!("expected an opaque, got {}", describe_type(other)), + } +} + +fn as_const(e: &Element) -> &Constant { match e { - Element::Const(c) => c, - other => panic!("expected a const, got {}", describe(other)), + Element::Constant(c) => c, + other => panic!("expected a constant, got {}", describe(other)), } } @@ -106,13 +155,21 @@ fn as_unsupported(e: &Element) -> &ItemError { fn describe(e: &Element) -> String { match e { Element::Function(f) => format!("function `{}`", f.name), - Element::Struct(s) => format!("struct `{}`", s.name), - Element::Variant(v) => format!("sum `{}`", v.name), - Element::Enum(en) => format!("enum `{}`", en.name), - Element::Const(c) => format!("const `{}`", c.name), + Element::Type(t) => describe_type(t), + Element::Constant(c) => format!("constant `{}`", c.name), Element::Unsupported(u) => match &u.name { Some(name) => format!("unsupported `{name}` ({})", u.error), None => format!("unsupported ({})", u.error), }, } } + +fn describe_type(t: &Type) -> String { + let kind = match t { + Type::Struct(_) => "struct", + Type::Variant(_) => "sum", + Type::Enum(_) => "enum", + Type::Opaque(_) => "opaque", + }; + format!("{kind} `{}`", t.name()) +} diff --git a/prebindgen/src/api/core/language/tests/roundtrip.rs b/prebindgen/src/api/core/flat/tests/roundtrip.rs similarity index 95% rename from prebindgen/src/api/core/language/tests/roundtrip.rs rename to prebindgen/src/api/core/flat/tests/roundtrip.rs index 8808137b..3eba87e3 100644 --- a/prebindgen/src/api/core/language/tests/roundtrip.rs +++ b/prebindgen/src/api/core/flat/tests/roundtrip.rs @@ -84,7 +84,7 @@ fn struct_field_slices_keep_attributes() { pub(crate) seq: u64, } )); - let fields = as_struct(&element).fields(); + let fields = &as_struct(&element).fields; assert_eq!(fields.len(), 2); assert!(tokens(&fields[0].origin.syntax).contains("The key it was published on.")); assert_eq!( @@ -108,12 +108,12 @@ fn an_item_and_its_components_share_one_location() { )); let s = as_struct(&element); let item = &s.origin.location; - for field in s.fields() { + for field in &s.fields { assert!(Rc::ptr_eq(item, &field.origin.location), "field"); assert!(Rc::ptr_eq(item, &field.ty.origin.location), "field type"); } // And down through a nested type's arguments. - let TypeKind::Sequence(elem) = &s.fields()[1].ty.kind else { + let TypeKind::Sequence(elem) = &s.fields[1].ty.kind else { panic!("a sequence"); }; assert!(Rc::ptr_eq(item, &elem.origin.location), "element type"); @@ -246,8 +246,7 @@ fn struct_delimiters_survive_and_spell() { let s = as_struct(&element); let name = &s.name; ( - s.fields.is_some(), - s.fields().len(), + s.fields.len(), s.spell(quote::quote!(#name), parts).to_string(), ) }; @@ -259,7 +258,7 @@ fn struct_delimiters_survive_and_spell() { ), &[] ), - (true, 0, "A".to_string()) + (0, "A".to_string()) ); assert_eq!( spell( @@ -268,7 +267,7 @@ fn struct_delimiters_survive_and_spell() { ), &[] ), - (true, 0, "B { }".to_string()) + (0, "B { }".to_string()) ); assert_eq!( spell( @@ -279,18 +278,17 @@ fn struct_delimiters_survive_and_spell() { ), &[quote::quote!(x: __f0)] ), - (true, 1, "C { x : __f0 }".to_string()) - ); - // Opaque: no modelled fields, and still spellable. - assert_eq!( - spell( - syn::parse_quote!( - pub struct D(Whatever<'_, dyn Trait>); - ), - &[quote::quote!(__f0)] - ), - (false, 0, "D (__f0)".to_string()) + (1, "C { x : __f0 }".to_string()) ); + + // A tuple struct is a handle, so it is an `Opaque` and has no field list to + // spell from — the delimiters still survive in its retained syntax. + let element = parse_one(syn::parse_quote!( + pub struct D(Whatever<'_, dyn Trait>); + )); + let o = as_opaque(&element); + assert_eq!(o.name, "D"); + assert!(tokens(&o.origin.syntax).contains("Whatever")); } /// A discriminant is two facts with two homes: the number is modelled, the @@ -425,7 +423,7 @@ fn array_extent_carries_number_const_and_spelling() { } ), ]); - let fields = as_struct(&elements[1]).fields(); + let fields = &as_struct(&elements[1]).fields; let named = fields[0].ty.array_extent().expect("an extent"); assert_eq!(named.value, 4); @@ -458,7 +456,9 @@ fn the_whole_item_survives() { #[test] fn an_unsupported_item_keeps_its_tokens() { let source: syn::Item = syn::parse_quote!( - pub type Alias = u32; + pub union U { + a: u8, + } ); let element = parse_one(source.clone()); assert!(matches!(element, Element::Unsupported(_))); diff --git a/prebindgen/src/api/core/language/ty.rs b/prebindgen/src/api/core/flat/ty.rs similarity index 73% rename from prebindgen/src/api/core/language/ty.rs rename to prebindgen/src/api/core/flat/ty.rs index e7c1c61f..73b9e23f 100644 --- a/prebindgen/src/api/core/language/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -1,6 +1,6 @@ //! Types: a closed classification paired with the syntax it was read from. //! -//! [`Type`] is the pattern the whole element model follows — `kind` says what +//! [`TypeRef`] is the pattern the whole element model follows — `kind` says what //! the type *means*, `syntax` is the tokens the source wrote. Consumers //! **classify off `kind` and spell off `syntax`**; see the [module docs](super) //! for why that split is the point. @@ -27,12 +27,12 @@ use crate::SourceLocation; /// outside Rust all survive there at zero modelling cost, so `kind` can stay /// language-neutral and small. #[derive(Clone, Debug)] -pub struct Type { +pub struct TypeRef { /// What the type means — the closed, destination-neutral classification. pub kind: TypeKind, /// The type as generated Rust must spell it — the source's own tokens, /// normalized to the flat namespace the generated crate can name (see - /// [`Language::parse`](super::Language::parse)) — plus the source they came + /// [`Flat::parse`](super::Flat::parse)) — plus the source they came /// from. /// /// The syntax can say strictly more than `kind` does — `Box` is a @@ -41,7 +41,7 @@ pub struct Type { pub origin: Origin, } -impl Type { +impl TypeRef { /// The extent of this type when it is an array, else `None`. pub fn array_extent(&self) -> Option<&ArrayExtent> { match &self.kind { @@ -61,6 +61,34 @@ impl Type { out } + /// The first nominal type reachable from here that `declared` does not hold. + /// + /// Recurses the same structure [`Self::collect_extents`] walks: what is + /// reachable is what a destination language will have to convert, so every + /// layer's inner reference counts. + pub(super) fn first_unresolved( + &self, + declared: &std::collections::HashSet, + ) -> Option { + match &self.kind { + TypeKind::Named { id, args } => { + if !declared.contains(&id.name) { + return Some(id.name.clone()); + } + args.iter().find_map(|a| a.first_unresolved(declared)) + } + TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => { + t.first_unresolved(declared) + } + TypeKind::Array { elem, .. } => elem.first_unresolved(declared), + TypeKind::Fallible { ok, err } => ok + .first_unresolved(declared) + .or_else(|| err.first_unresolved(declared)), + TypeKind::Callback { args } => args.iter().find_map(|a| a.first_unresolved(declared)), + TypeKind::Scalar(_) | TypeKind::Str | TypeKind::Unit => None, + } + } + fn collect_extents<'a>(&'a self, out: &mut Vec<&'a ArrayExtent>) { match &self.kind { TypeKind::Array { elem, extent } => { @@ -82,12 +110,12 @@ impl Type { } } -/// What a [`Type`] means. The variants are the accepted type grammar. +/// What a [`TypeRef`] means. The variants are the accepted type grammar. /// /// One Rust spelling per concept is **not** the rule here — several are. A /// concept earns a variant when a destination language would act on it; a /// spelling that changes nothing outside Rust folds into the concept it carries -/// and survives in [`Type::syntax`]: +/// and survives in [`TypeRef::origin`]: /// /// | Spelling | Kind | Why | /// |---|---|---| @@ -105,7 +133,7 @@ pub enum TypeKind { /// does by hand. Str, /// `Option`. - Optional(Box), + Optional(Box), /// A run of `T` — `Vec` owned, `[T]` behind a [`Ref`](TypeKind::Ref). /// /// One variant, because ownership is already the [`Ref`](TypeKind::Ref) @@ -113,9 +141,9 @@ pub enum TypeKind { /// second variant would encode ownership twice and let the two copies /// disagree. `[T; N]` is *not* this — a fixed extent is a different /// concept, see [`Array`](TypeKind::Array). - Sequence(Box), + Sequence(Box), /// `Result`. - Fallible { ok: Box, err: Box }, + Fallible { ok: Box, err: Box }, /// Any other named type: a `#[prebindgen]` struct or enum, or a foreign /// path. /// @@ -123,8 +151,8 @@ pub enum TypeKind { /// this module has to take a path apart to learn what a type is. The last /// segment's generic arguments live in `args`, and only the *type* /// arguments: a lifetime argument says nothing a destination language can - /// act on. The full spelling is in [`Type::syntax`] for whoever re-emits it. - Named { id: TypeId, args: Vec }, + /// act on. The full spelling is in [`TypeRef::origin`] for whoever re-emits it. + Named { id: TypeId, args: Vec }, /// `[T; N]` — a run of `T` whose length is known at compile time. /// /// Deliberately not a [`Sequence`](TypeKind::Sequence) with an optional @@ -132,22 +160,48 @@ pub enum TypeKind { /// crosses as a heap collection, and every adapter branches between the two /// at every site. Array { - elem: Box, + elem: Box, extent: ArrayExtent, }, - /// A borrow — `&T` / `&'a T` / `&mut T`. The lifetime is spelling, so it - /// lives in [`Type::syntax`] rather than here. + /// A borrow — `&T`, `&mut T`, or `&mut MaybeUninit`. The lifetime is + /// spelling, so it lives in [`TypeRef::origin`] rather than here. /// /// This is the ownership layer for every concept underneath it: `&str` is /// `Ref(Str)`, `&[T]` is `Ref(Sequence)`. A shared-ownership handle /// (`Arc`, `Rc`) belongs here too when the language accepts one. - Ref { mutable: bool, inner: Box }, + /// + /// `inner` is always the borrowed *value's* type, so an out-parameter's + /// `MaybeUninit` is absorbed into [`RefMode::Out`] rather than wrapping it: + /// uninitialized-ness is a property of the **borrow**, not of the type, and + /// it is meaningless anywhere else. + Ref { mode: RefMode, inner: Box }, /// `impl Fn(A, B, …) + Send + Sync + 'static` — the callback form. - Callback { args: Vec }, + Callback { args: Vec }, /// `()`. Unit, } +/// What a borrow permits, and what the callee owes. +/// +/// One axis with three values rather than a `mutable` flag plus a wrapper, so the +/// combinations that mean nothing at a boundary — a shared borrow of +/// uninitialized storage, an owned `MaybeUninit` — cannot be written down. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RefMode { + /// `&T` — the callee may read it and must not write it. + Shared, + /// `&mut T` — the callee may read and write an already-valid `T`. + Exclusive, + /// `&mut MaybeUninit` — an **out-parameter**: the caller supplies storage + /// only, and the callee's job is to make it a valid `T`. The callee may not + /// read it first. + /// + /// This is a boundary concept every destination language has (C's `T *out`), + /// which is why it is modelled here rather than left as a nominal + /// `MaybeUninit` for each adapter to recognise. + Out, +} + /// A nominal type's identity: a name, and nothing else. /// /// `#[prebindgen]` names live in one flat namespace — a duplicate is a @@ -258,6 +312,18 @@ pub enum UnsupportedTypeReason { /// lowered a tuple, so accepting one would defer the failure to a late /// "unresolved type" instead of naming it here. UnsupportedTuple, + /// `MaybeUninit` somewhere other than behind a `&mut`. + /// + /// Uninitialized storage is a property of a *borrow*, not of a type — see + /// [`RefMode::Out`]. Owned, returned or stored in a field it promises nothing + /// a destination language can use, and reading it would be undefined. + OwnedUninit, + /// `&MaybeUninit` — a shared borrow of uninitialized storage. + /// + /// A shared borrow promises a readable `T`, and this supplies storage that may + /// not be one. Only `&mut MaybeUninit` means anything: see + /// [`RefMode::Out`]. + SharedUninit, /// A path with a qualified self — `::Assoc`. /// /// The frontend never captures `impl` blocks, so it cannot know what an @@ -297,6 +363,20 @@ impl fmt::Display for UnsupportedType { components separately, or wrap them in a `#[prebindgen]` struct", self.offending ), + UnsupportedTypeReason::OwnedUninit => write!( + f, + "type `{}` is uninitialized storage outside an out-parameter. Only `&mut \ + MaybeUninit` means anything at a boundary \u{2014} it says the caller supplies \ + the slot and the callee fills it; owned or in a field it promises nothing, and \ + reading it would be undefined", + self.offending + ), + UnsupportedTypeReason::SharedUninit => write!( + f, + "type `{}` is a shared borrow of uninitialized storage: `&T` promises a readable \ + `T`, which this may not be. Use `&mut MaybeUninit` for an out-parameter", + self.offending + ), UnsupportedTypeReason::AssociatedType => write!( f, "type `{}` is an associated type; `#[prebindgen]` never captures `impl` \ @@ -328,7 +408,7 @@ pub(crate) fn lower_type( ty: &syn::Type, consts: &ConstIndex, at: &Rc, -) -> Result { +) -> Result { let fail = |reason| UnsupportedType { offending: ty.to_token_stream().to_string(), reason, @@ -340,10 +420,23 @@ pub(crate) fn lower_type( // spelling, which is the one a consumer wants to emit. syn::Type::Group(g) => return lower_type(&g.elem, consts, at), syn::Type::Paren(p) => return lower_type(&p.elem, consts, at), - syn::Type::Reference(r) => TypeKind::Ref { - mutable: r.mutability.is_some(), - inner: Box::new(lower_type(&r.elem, consts, at)?), - }, + // The mode is read off the borrow AND its target together, because + // `&mut MaybeUninit` is one concept — an out-parameter — rather than a + // mutable borrow of a distinct `MaybeUninit` type. + syn::Type::Reference(r) => { + let (mode, target) = match maybe_uninit_inner(&r.elem) { + Some(inner) if r.mutability.is_some() => (RefMode::Out, inner), + // `&MaybeUninit` promises a readable `T` and supplies storage + // that may not be one. Nothing at a boundary can use it. + Some(_) => return Err(fail(UnsupportedTypeReason::SharedUninit)), + None if r.mutability.is_some() => (RefMode::Exclusive, (*r.elem).clone()), + None => (RefMode::Shared, (*r.elem).clone()), + }; + TypeKind::Ref { + mode, + inner: Box::new(lower_type(&target, consts, at)?), + } + } // `[T]` is the borrowed spelling of the same concept `Vec` owns. syn::Type::Slice(s) => TypeKind::Sequence(Box::new(lower_type(&s.elem, consts, at)?)), _ if is_unit_type(ty) => TypeKind::Unit, @@ -373,7 +466,7 @@ pub(crate) fn lower_type( syn::Type::Path(tp) => lower_path(ty, tp, consts, at)?, _ => return Err(fail(UnsupportedTypeReason::UnsupportedForm)), }; - Ok(Type { + Ok(TypeRef { kind, origin: Origin::new(ty.clone(), Rc::clone(at)), }) @@ -403,9 +496,9 @@ fn lower_path( // Type arguments only. A lifetime argument is accepted and dropped: it is // part of the spelling (`Foo<'a>` is not `Foo`), and the spelling is in - // `Type::syntax`, so modelling it would be a second copy of one fact. + // `TypeRef::origin`, so modelling it would be a second copy of one fact. let mut has_lifetime_arg = false; - let args: Vec = match &last.arguments { + let args: Vec = match &last.arguments { syn::PathArguments::None => Vec::new(), syn::PathArguments::AngleBracketed(ab) => { let mut out = Vec::new(); @@ -466,10 +559,14 @@ fn lower_path( arity(1)?; return Ok(TypeKind::Sequence(Box::new(args.remove(0)))); } + // Reached here, it is not behind a `&mut`, so it is not an + // out-parameter — see `RefMode::Out`, which is the only place + // uninitialized storage means anything. + "MaybeUninit" => return Err(fail(UnsupportedTypeReason::OwnedUninit)), // `Box` **is** `T`: an owned value either way, and no // destination language can tell the two apart. So it carries no // kind of its own and classifies as whatever it wraps — the - // `Box` survives in `Type::syntax`, which is what generated + // `Box` survives in `TypeRef::origin`, which is what generated // Rust spells. (A shared-ownership handle would classify as a // `Ref` for the same reason, when the language accepts one.) "Box" => { @@ -489,6 +586,29 @@ fn lower_path( Ok(named(tp, args)) } +/// If `ty` is a bare `MaybeUninit`, the `T` it holds storage for. +/// +/// Bare, for the reason every builtin generic is: `normalize_type` has already +/// reduced the real std paths at ingest, so anything still carrying a prefix is a +/// foreign type that merely shares the name. +fn maybe_uninit_inner(ty: &syn::Type) -> Option { + let syn::Type::Path(tp) = ty else { return None }; + if tp.qself.is_some() || tp.path.leading_colon.is_some() || tp.path.segments.len() != 1 { + return None; + } + let seg = &tp.path.segments[0]; + if seg.ident != "MaybeUninit" { + return None; + } + let syn::PathArguments::AngleBracketed(ab) = &seg.arguments else { + return None; + }; + match ab.args.first() { + Some(syn::GenericArgument::Type(t)) if ab.args.len() == 1 => Some(t.clone()), + _ => None, + } +} + /// True when `ty` is the unit type `()`. /// /// The language's one answer to that question: [`lower_type`] classifies it as @@ -506,7 +626,7 @@ pub(crate) fn is_unit_type(ty: &syn::Type) -> bool { /// `Named` with the identity read off the path: every segment joined, minus the /// generic arguments, which are already in `args`. -fn named(tp: &syn::TypePath, args: Vec) -> TypeKind { +fn named(tp: &syn::TypePath, args: Vec) -> TypeKind { let name = tp .path .segments diff --git a/prebindgen/src/api/core/mod.rs b/prebindgen/src/api/core/mod.rs index cd9ecba8..be4a9298 100644 --- a/prebindgen/src/api/core/mod.rs +++ b/prebindgen/src/api/core/mod.rs @@ -21,8 +21,8 @@ pub mod domain; pub mod expand; +pub mod flat; pub mod gravestone; -pub mod language; pub mod niches; pub mod prebindgen; pub mod registry; @@ -34,8 +34,8 @@ pub(crate) mod write; pub use self::{ domain::{DomainScalar, RepresentationDomain, ScalarValue}, + flat::{Element, Flat}, gravestone::{Gravestone, Transmute}, - language::{Element, Language}, niches::{NicheSlot, Niches}, prebindgen::{const_path_alias, ConverterImpl, Prebindgen, Stage}, registry::{Direction, Generation, Registry, ScanError, TypeEntry, TypeKey, WriteRustError}, diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs index 8b346647..ad5f09c2 100644 --- a/prebindgen/src/api/core/registry.rs +++ b/prebindgen/src/api/core/registry.rs @@ -1107,6 +1107,11 @@ impl Registry { self.consts.insert(c.ident.clone(), (c, loc)); Ok(()) } + // `#[prebindgen] pub type X = ..` DECLARES an opaque type: it states + // something about the flat API's surface, and is not code to copy + // into the binding. Its target is routinely crate-private, so + // re-emitting it would not even compile. + syn::Item::Type(_) => Ok(()), other => { self.passthrough.push((other, loc)); Ok(()) @@ -1485,7 +1490,7 @@ pub fn immediate_subtype_positions(ty: &syn::Type) -> Vec { /// The callback grammar, which the source language owns — re-exported here for /// the existing call sites until they consume elements (stages L2–L4 of #229). -pub use crate::api::core::language::extract_fn_trait_args; +pub use crate::api::core::flat::extract_fn_trait_args; /// A **resolved** binding generation: the [`Registry`] after /// [`Registry::resolve`] ran the adapter's scan, plans, and type diff --git a/prebindgen/src/lib.rs b/prebindgen/src/lib.rs index 47c8e44a..7d38460c 100644 --- a/prebindgen/src/lib.rs +++ b/prebindgen/src/lib.rs @@ -286,18 +286,17 @@ macro_rules! ident { /// [`lang::JniGen`]. The C / cbindgen proof of concept is available separately /// with the `unstable-cbindgen` feature. pub mod core { - /// The prebindgen **source language**: the parser from captured - /// `#[prebindgen]` records to [`language::Element`]s, and the element model - /// itself. Not to be confused with [`crate::lang`], the *destination* - /// adapters. - pub use crate::api::core::language; - /// [`Language`] and [`Element`] sit here too, next to [`Registry`]: they are - /// what a build script names, and the rest of the element model stays in - /// [`mod@language`] where an adapter reaches for it. + /// The **flat API**: the parser from captured `#[prebindgen]` records to the + /// [`flat::Element`]s that make up one flat namespace, and the model itself. + /// Not to be confused with [`crate::lang`], the *destination* adapters. + pub use crate::api::core::flat; + /// [`Flat`] and [`Element`] sit here too, next to [`Registry`]: they are what + /// a build script names, and the rest of the model stays in [`mod@flat`] + /// where an adapter reaches for it. pub use crate::api::core::{ - ConverterImpl, Direction, DomainScalar, Element, Generation, Gravestone, Language, - NicheSlot, Niches, Prebindgen, Registry, RepresentationDomain, ScalarValue, ScanError, - Stage, Transmute, TypeEntry, TypeKey, WriteRustError, + ConverterImpl, Direction, DomainScalar, Element, Flat, Generation, Gravestone, NicheSlot, + Niches, Prebindgen, Registry, RepresentationDomain, ScalarValue, ScanError, Stage, + Transmute, TypeEntry, TypeKey, WriteRustError, }; } From 610c1c279a8d5e5364ba82601ba17b448be26dc4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 30 Jul 2026 15:11:16 +0200 Subject: [PATCH 04/52] A prelude, Extern instead of Opaque, and no args (#235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * A prelude, Extern instead of Opaque, and no args Three things the same question kept surfacing: what does the language know without being told, and what must it be told? **Path reduction had one rule and a std special case; now it has one rule.** `reduce_flat_path` already reduced `crate`/`self` and any source-module prefix — a path into the flat namespace collapses to its bare name. Bolted on was a five-entry whitelist of std paths. Naming what that whitelist is removes it: those are **aliases the language pre-declares**, a prelude in exactly Rust's sense. A crate need not write `use std::vec::Vec`, and need not write `#[prebindgen] pub type Vec = std::vec::Vec` either, for the same reason. So the mechanism is an alias map from path to name, seeded from `PRELUDE` and extended with every alias the ingested crates declared — because a prelude entry and a hand-written alias say the same kind of thing. That generalises past std: given `#[prebindgen] pub type Session = zenoh::Session;`, a signature may now spell `&zenoh::Session` and reach the declaration. `foreign::Option` is still not `Option`, because the key is the whole path, never a final segment. `Normalization` holds what to reduce against, replacing the module-gathering loop `FlatBuilder::build` and `Registry::from_items` each wrote separately — they cannot normalize differently now. Two traps found on the way. A marked alias must be excluded from the normalization it defines, or `pub type Duration = std::time::Duration` becomes `pub type Duration = Duration`. And the prelude's entries are *generic*, so an early "reduce only without type arguments" guard broke `std::vec::Vec`; the guard was also unnecessary, since a full-path key cannot collide. `mem::MaybeUninit` joining the prelude is a bug fix. It was a grammar builtin that was **not** reducible, so it worked only because perftest-flat happens to `use` it; written `&mut std::mem::MaybeUninit` it became an unresolvable nominal type and silently refused the item — and `maybe_uninit_inner`'s comment claimed normalization had already reduced it. One test row per prelude entry now pins both spellings to the same kind, which is how that class of drift gets caught. **`Opaque` becomes `Extern`, and carries what it points at.** It was never only handles: `pub type Duration = std::time::Duration` crosses by value through a `convert!`, erased to an integer. What the frontend knows is narrower and truer — this name is in the flat API and its contents are not modelled — and the adapter decides the rest. `target` is now a modelled fact, so an adapter can recognise `std::time::Duration` without taking syntax apart, and reduction uses it. Deliberately not classified as std-vs-foreign: `pub type Error = zenoh::Error` IS `Box`, so std-ness is a property of the spelling, not the type. A rule keyed on the path root would answer differently for one type depending on who aliased it. **`args` is gone from `Named`.** A reference is a name. Nothing could read retained arguments: a surviving reference resolves to a declared type, and no declaration takes type parameters, so `Foo` against a declared `Foo` would not compile in the source crate. They are still lowered, so a bad type inside one is diagnosed — the dropped test row asserted a shape real source cannot produce. The boundary ledger gains one site in `types_util` for reading an alias's target; L2 reclaims it when the frontend owns normalization outright. Generation is byte-identical and the JVM covertest passes all 48 sections. Co-Authored-By: Claude Opus 5 * Box the array extent, the size outlier among the kinds `clippy::large_enum_variant` under `-D warnings`: an `ArrayExtent` carries an `Origin` over its length expression, so `Array` towered over the second-largest variant once `Named` lost `args`. The lint compares those two, which is why shrinking one variant surfaced another's size. Boxed rather than allowed — an array is the rare kind, the same trade-off `Unsupported::error` already makes for the same reason. My local clippy runs missed it because they omitted `-- -D warnings`, so it was a warning my filter did not match. CI passes that flag in all three configurations. Co-Authored-By: Claude Opus 5 * Address review: an alias key is a whole type, and never shadows the grammar `path_key` dropped **all** generic arguments, so `type Bytes = std::vec::Vec` keyed on `std::vec::Vec` — overwriting the prelude entry, since the alias pass runs after the seeding. Reduction then swapped the ident and kept the use site's arguments, so `Vec` became `Bytes`, `Named` discarded the argument, and an unrelated parameter stopped being a `Sequence`. Any concrete alias could do this to any prelude entry. The root cause is a constraint I had not stated: normalization decides which spellings denote **one type** (issue #95, "the canonical flat-namespace spelling"), so it may choose a canonical spelling but must never change what a type *means*. `zenoh::Session` → `Session` preserves the kind. `Vec` → `Bytes` turns a sequence into an extern — retyping, not canonicalizing. Naming what the two kinds of alias are makes the fix structural rather than a patch. They **partition** the targets, because a target either has a grammar meaning or it does not: * the prelude, over targets the grammar models. Each names a **constructor**, so arguments are ignored when matching and preserved when rewriting — `std::vec::Vec` is `Vec`. * a crate's aliases, over targets it does not. Each names one **complete type**, so the key keeps type arguments (lifetimes still dropped, since a lifetime is spelling) and a match replaces the whole type — an alias name carries no arguments of its own. So an alias to something the grammar already models is not a reduction rule: the prelude owns that path. `type Bytes = Vec` stays a perfectly good name for an `Extern` — a bare path is never reduced, so `Bytes` resolves — while `Vec` keeps meaning a sequence and `Vec` is untouched. Duplicate targets now resolve deterministically: first declaration wins, rather than last-in-stream. Two regressions, both verified to fail against the old behaviour before being kept: the reported case verbatim, and two concrete aliases over one foreign constructor staying distinct. The partition is documented where each half lives — the equivalence rule list and `Extern`'s own doc, including the asymmetry that an alias is an `Extern` always but a reduction rule only sometimes (`type Error = Box` has no rule at all). Generation byte-identical, ledger unmoved, JVM covertest 48 sections. Co-Authored-By: Claude Opus 5 * An alias is a one-way road, not an equivalence The review found that `alias_key` kept only `GenericArgument::Type`, so `type Small = zenoh::Wrap<4>` and `type Big = zenoh::Wrap<8>` still collided on their const arguments. Retaining every non-lifetime argument would fix that instance, but the key shape was never the real problem. Normalization decides which spellings denote **one type**. An alias does not create such a spelling — it brings a foreign type *into* the flat API under a new name. That is a one-way road: the name is thereafter the only way to spell the type here, and `zenoh::Session` in a signature stays refused even when `type Session = zenoh::Session` is declared. The diagnosis already said exactly that — "Give the type a name here with `#[prebindgen] pub type = ..;` and refer to that" — so alias reduction was weakening a rule the language already had. Treating it as an equivalence is a category error, and the two reported bugs are symptoms of it: `Vec` ≡ `Bytes` turns a sequence into an extern, and once one path can stand for two types, key shape decides which — arguments, const arguments, associated bindings, each a new way to collide. Removing the equivalence makes that class unreachable rather than patched. So a crate's `pub type` is a declaration only, and the prelude alone reduces: `std::vec::Vec` is `Vec`, because those *are* one type. The prelude and a crate's aliases stop being "two kinds of alias" needing a partition — different mechanisms with different jobs, which is the simpler answer to how they relate. Net −133 lines: `alias_key`, `type_args`, the alias map, the alias-collection pass, the first-declaration-wins tie-break, and the circularity guard that stopped an alias rewriting its own target all go. The boundary ledger returns to 205 — the site the previous commit added was reading an alias target, and nothing does that now. Nothing real depended on it: no marked signature in zenoh-flat or the examples spells a qualified alias target. Verified byte-identical generation and 48 JVM sections. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- prebindgen/src/api/core/flat/element.rs | 45 +++- prebindgen/src/api/core/flat/mod.rs | 51 ++-- .../src/api/core/flat/tests/acceptance.rs | 248 ++++++++++++++++-- prebindgen/src/api/core/flat/tests/mod.rs | 8 +- .../src/api/core/flat/tests/roundtrip.rs | 4 +- prebindgen/src/api/core/flat/ty.rs | 37 +-- prebindgen/src/api/core/registry.rs | 34 +-- prebindgen/src/api/core/types_util.rs | 196 +++++++++++--- 8 files changed, 487 insertions(+), 136 deletions(-) diff --git a/prebindgen/src/api/core/flat/element.rs b/prebindgen/src/api/core/flat/element.rs index c928cd6d..87c71dfd 100644 --- a/prebindgen/src/api/core/flat/element.rs +++ b/prebindgen/src/api/core/flat/element.rs @@ -89,7 +89,7 @@ pub enum Type { Variant(Variant), /// An enum whose every alternative is fieldless — a named set of integers. Enum(Enum), - Opaque(Opaque), + Extern(Extern), } impl Type { @@ -98,7 +98,7 @@ impl Type { Type::Struct(s) => &s.name, Type::Variant(v) => &v.name, Type::Enum(e) => &e.name, - Type::Opaque(o) => &o.name, + Type::Extern(e) => &e.name, } } @@ -112,7 +112,7 @@ impl Type { Type::Struct(s) => &s.origin.location, Type::Variant(v) => &v.origin.location, Type::Enum(e) => &e.origin.location, - Type::Opaque(o) => &o.origin.location, + Type::Extern(e) => &e.origin.location, } } @@ -122,27 +122,44 @@ impl Type { Type::Struct(s) => syn::Item::Struct(s.origin.syntax.clone()), Type::Variant(v) => syn::Item::Enum(v.origin.syntax.clone()), Type::Enum(e) => syn::Item::Enum(e.origin.syntax.clone()), - Type::Opaque(o) => o.origin.syntax.clone(), + Type::Extern(e) => e.origin.syntax.clone(), } } } -/// A type whose contents do not cross the boundary — a handle. +/// A type the flat API **names** but whose contents it does not model. /// -/// Two spellings declare one thing, because the model records the *fact* rather -/// than the Rust shape that carried it: +/// Two spellings declare one thing, because what the frontend records is the fact +/// rather than the Rust shape that carried it: /// -/// * `#[prebindgen] pub type X = path::To;` — the way to give a foreign or -/// crate-private type a name in the flat API. This is how a handle is declared -/// deliberately. +/// * `#[prebindgen] pub type X = path::To;` — how a foreign or +/// crate-private type gets a name here. A **one-way road**: the name is +/// thereafter the only way to spell that type inside the flat API, and the +/// qualified path stays refused. This declares a name; it is not an equivalence +/// between spellings — see +/// [`normalize_type`](crate::api::core::types_util::normalize_type)'s rule 4 for +/// why treating it as one is a category error. /// * `#[prebindgen] pub struct X(..);` — a tuple struct, whose fields no adapter /// has ever crossed. /// -/// Either way the adapter decides what the handle becomes: an opaque pointer, a -/// `ptr_class`, a `convert!` target. +/// Not necessarily a *handle*: `#[prebindgen] pub type Duration = +/// std::time::Duration;` crosses by value through a `convert!`, erased to a plain +/// integer. What it becomes — an opaque pointer, a `ptr_class`, a conversion — is +/// the adapter's decision, and this says only that the frontend does not model the +/// contents. #[derive(Clone, Debug)] -pub struct Opaque { +pub struct Extern { pub name: syn::Ident, + /// What the declaration points at, for an alias — `std::time::Duration`, + /// `zenoh::Session`, `handles::Storage`. `None` for a tuple struct, which is + /// itself the definition. + /// + /// Informational, and deliberately **not** classified. `Error` is + /// `Box` behind a `zenoh::` alias in one + /// crate and spelled openly in another, so being "a std type" is a property of + /// the spelling, not of the type. An adapter that wants to recognise a target + /// may; the frontend does not decide for it. + pub target: Option, /// The declaring item — a type alias or a tuple struct. pub origin: Origin, } @@ -173,7 +190,7 @@ pub struct Param { /// A `#[prebindgen]` struct: a product of fields that cross the boundary. /// -/// A struct whose contents do *not* cross is an [`Opaque`], not a `Struct` with +/// A struct whose contents do *not* cross is an [`Extern`], not a `Struct` with /// nothing in it — so `fields` is a plain list, and empty means the source wrote /// a struct with no fields. /// diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index 388365bf..d20b9a18 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -56,7 +56,7 @@ //! | `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(..)` | [`Opaque`] | a handle; contents do not cross | +//! | `type X = ..`, `struct X(..)` | [`Extern`] | named here; contents not modelled | //! | `&mut MaybeUninit` | [`RefMode::Out`] | an out-param slot the caller supplies | //! | no `->`, `-> ()` | [`TypeKind::Unit`] | the same function | //! | `*const T` | *rejected* | a source crate is idiomatic Rust; the adapter owns pointers | @@ -114,7 +114,7 @@ //! //! # Declaring a handle //! -//! `#[prebindgen] pub type X = path::To;` declares an [`Opaque`]: it gives +//! `#[prebindgen] pub type X = path::To;` 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 @@ -158,7 +158,7 @@ use self::{array_len::ConstIndex, ty::lower_type}; pub use self::{ array_len::{ArrayExtent, ArrayLenReason, ConstId, ExtentSource, UnsupportedArrayLen}, element::{ - Alternative, Constant, Element, Enum, EnumValue, Field, Function, Opaque, Param, Struct, + Alternative, Constant, Element, Enum, EnumValue, Extern, Field, Function, Param, Struct, Type, Unsupported, Variant, }, origin::Origin, @@ -303,25 +303,18 @@ impl FlatBuilder { // Pass 0: normalize every item's types to the canonical flat spelling // before a single one is classified. `std::option::Option` is an - // `Option`, and `source_a::TypeA` is `TypeA` — decisions this module - // owns, so it must be the one to see the reduced form. Gathering EVERY - // module name first is what makes a cross-source reference in an - // earlier item normalize the same as in a later one. + // `Option`, `source_a::TypeA` is `TypeA`, and `zenoh::Session` is whatever + // an alias named it — decisions this module owns, so it must be the one to + // see the reduced form. Gathering EVERY module and alias first is what + // makes a reference in an earlier item normalize the same as in a later + // one. // // The consequence is deliberate and stated on `Origin`: a slice // is the spelling generation must EMIT, which is the normalized one — // the flat namespace is what the generated crate can actually name. - let mut modules: Vec = Vec::new(); - for (_, loc) in &items { - if let Some(crate_name) = &loc.crate_name { - let module = crate_name.replace('-', "_"); - if !modules.contains(&module) { - modules.push(module); - } - } - } + let normalization = crate::api::core::types_util::Normalization::from_items(&items); for (item, _) in &mut items { - crate::api::core::types_util::normalize_item_types(item, &modules); + crate::api::core::types_util::normalize_item_types(item, &normalization); } // Pass 1: the consts an array length may name. Unnamed `const _` items @@ -575,7 +568,7 @@ fn first_unresolved( ), // An enum names nothing, an opaque hides what it names, and an // unsupported item already has a diagnosis worth keeping. - Element::Type(Type::Enum(_) | Type::Opaque(_)) | Element::Unsupported(_) => {} + Element::Type(Type::Enum(_) | Type::Extern(_)) | Element::Unsupported(_) => {} } refs.into_iter().find_map(|r| r.first_unresolved(declared)) } @@ -828,16 +821,20 @@ fn lower_item(item: syn::Item, loc: SourceLocation, consts: &ConstIndex) -> Elem // anything about its contents. That is the only way a handle enters the // API deliberately, and the reason references can be required to resolve. syn::Item::Type(t) => match reject_generic_params(&t.generics) { - // `Opaque` has no binder and no arity, so a generic alias would be + // `Extern` has no binder and no arity, so a generic alias would be // accepted as one declaration that `Handle` then resolves against // — losing exactly the scoped-parameter distinction every other item // kind refuses. It is also why `MaybeUninit` needed grammar support // rather than an alias. Err(error) => unsupported(t.ident.clone(), syn::Item::Type(t), &at, error), - Ok(()) => Element::Type(Type::Opaque(Opaque { - name: t.ident.clone(), - origin: Origin::new(syn::Item::Type(t), at), - })), + Ok(()) => { + let target = Some(t.ty.to_token_stream().to_string()); + Element::Type(Type::Extern(Extern { + name: t.ident.clone(), + target, + origin: Origin::new(syn::Item::Type(t), at), + })) + } }, // Including the unnamed `const _` each source injects as its feature // guard: it is a const, so it is one here. `Element::name` returns @@ -958,7 +955,7 @@ fn lower_fn( /// Lower a `struct` item to whichever of the two shapes it is. /// -/// A **tuple struct** is an [`Opaque`]: no adapter has ever crossed its fields, +/// A **tuple struct** is an [`Extern`]: no adapter has ever crossed its fields, /// so they are deliberately not lowered and a field type outside the grammar is /// not an error. Anything else is a product of fields that do cross. fn lower_struct( @@ -987,10 +984,12 @@ fn lower_struct( } // Its contents are not a boundary surface, so nothing is lowered. syn::Fields::Unnamed(_) => { - return Ok(Type::Opaque(Opaque { + return Ok(Type::Extern(Extern { name: s.ident.clone(), + // A tuple struct IS the definition; it points at nothing. + target: None, origin: Origin::new(syn::Item::Struct(s.clone()), Rc::clone(at)), - })) + })); } syn::Fields::Unit => Vec::new(), }; diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index 703d6b6c..5c4e0155 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -114,6 +114,192 @@ fn a_box_classifies_as_what_it_wraps() { /// reduces and classifies, while a path-qualified lookalike is a foreign type /// that merely shares the name, and collapsing it would silently retype the /// field. +/// The prelude: every name the language pre-declares reaches the same kind however +/// it is spelled. This is the drift guard — adding a builtin arm to `lower_path` +/// and forgetting its prelude entry fails here, which is exactly how `MaybeUninit` +/// slipped through and worked only when the source happened to `use` it. +#[test] +fn the_prelude_reaches_every_builtin_by_either_spelling() { + use crate::api::core::types_util::Normalization; + + // Each entry, bare against fully qualified. `MaybeUninit` needs a `&mut` to + // mean anything, so it is checked separately below. + for (path, name) in Normalization::PRELUDE { + if *name == "MaybeUninit" { + continue; + } + let bare: proc_macro2::TokenStream = match *name { + "Result" => quote::quote!(Result), + "String" => quote::quote!(String), + _ => { + let n = quote::format_ident!("{name}"); + quote::quote!(#n) + } + }; + let qualified: proc_macro2::TokenStream = { + let p: syn::Path = syn::parse_str(path).expect("a prelude path"); + match *name { + "Result" => quote::quote!(#p), + "String" => quote::quote!(#p), + _ => quote::quote!(#p), + } + }; + assert_eq!( + format!("{:?}", kind(bare)), + format!("{:?}", kind(qualified)), + "`{name}` must classify the same as `{path}`" + ); + } + + // `core` and `alloc` are re-exports of the same items, so either root works. + assert!(matches!( + kind(quote::quote!(core::option::Option)), + TypeKind::Optional(_) + )); + assert!(matches!( + kind(quote::quote!(alloc::string::String)), + TypeKind::Str + )); + + // The bug: qualified `MaybeUninit` used to fall through to an unresolvable + // nominal type, so an out-parameter worked only if the source `use`d it. + let TypeKind::Ref { mode, .. } = kind(quote::quote!(&mut std::mem::MaybeUninit)) else { + panic!("a borrow"); + }; + assert_eq!(mode, RefMode::Out); +} + +/// A `#[prebindgen] pub type` is a **one-way road**: it brings a foreign type into +/// the flat API under a name, and that name is thereafter the only way to spell it. +/// +/// It is a *declaration*, not an equivalence. Treating it as a reduction rule broke +/// the contract normalization actually has — choose among spellings of one type, +/// never change what a type is — because `type Bytes = Vec` would make `Vec` +/// an extern. So a qualified spelling stays refused even when an alias names exactly +/// that type. +#[test] +fn an_alias_is_a_declaration_not_an_equivalence() { + let items: Vec = vec![ + syn::parse_quote!( + pub type Session = zenoh::Session; + ), + syn::parse_quote!( + pub fn by_name(s: &Session) {} + ), + syn::parse_quote!( + pub fn by_path(s: &zenoh::Session) {} + ), + ]; + let flat = Flat::builder() + .items(items.into_iter().map(|i| (i, loc()))) + .build() + .expect("a refusal is deferred, not fatal"); + + // The declared name works. + let f = flat.function("by_name").expect("declared"); + let TypeKind::Ref { inner, .. } = &f.params[0].ty.kind else { + panic!("a borrow"); + }; + let TypeKind::Named { id } = &inner.kind else { + panic!("a nominal type"); + }; + assert_eq!(id.name, "Session"); + + // The path it aliases does NOT, and the diagnosis says to use the name. + assert!(flat.function("by_path").is_none()); + let u = flat.unsupported().next().expect("one refusal"); + assert!(matches!( + &*u.error, + ItemError::UnresolvedType { name } if name == "zenoh::Session" + )); + assert!( + u.error.to_string().contains("refer to that"), + "the diagnosis must point at the declared name: {}", + u.error + ); + + // And the declaration itself still records what it points at. + let Type::Extern(e) = flat.declared_type("Session").expect("declared") else { + panic!("an extern"); + }; + assert_eq!(e.target.as_deref(), Some("zenoh :: Session")); +} + +/// An alias cannot retype anything, whatever its target's arguments — the property +/// that made key-shape bugs possible in the first place, now unreachable because an +/// alias is not an equivalence at all. +/// +/// Covers the reported cases: a concrete generic target (`Vec`, which shadowed +/// the prelude), and a const-generic pair (`Wrap<4>` / `Wrap<8>`, which collided +/// because a key kept only type arguments). +#[test] +fn an_alias_never_retypes_a_spelling() { + let flat = Flat::builder() + .items( + vec![ + syn::parse_quote!( + pub type Bytes = std::vec::Vec; + ), + syn::parse_quote!( + pub type Small = zenoh::Wrap<4>; + ), + syn::parse_quote!( + pub type Big = zenoh::Wrap<8>; + ), + syn::parse_quote!( + pub fn strings(xs: std::vec::Vec) {} + ), + syn::parse_quote!( + pub fn bytes(xs: std::vec::Vec) {} + ), + syn::parse_quote!( + pub fn by_name(b: Bytes) {} + ), + syn::parse_quote!( + pub fn small(w: Small) {} + ), + syn::parse_quote!( + pub fn big(w: Big) {} + ), + ] + .into_iter() + .map(|i: syn::Item| (i, loc())), + ) + .build() + .expect("parses"); + + let param = |name: &str| { + flat.function(name) + .unwrap_or_else(|| panic!("{name} survives")) + .params[0] + .ty + .kind + .clone() + }; + + // A prelude type keeps its grammar meaning at every instantiation, whatever an + // unrelated alias happens to target. + for f in ["strings", "bytes"] { + assert!( + matches!(param(f), TypeKind::Sequence(_)), + "`{f}`: the grammar's spelling stays canonical" + ); + } + + // Each alias is usable by its own name, and they cannot collide: a bare path is + // never reduced, so the name IS the identity. + for (f, expected) in [("by_name", "Bytes"), ("small", "Small"), ("big", "Big")] { + let TypeKind::Named { id } = param(f) else { + panic!("{f}: a nominal type"); + }; + assert_eq!(id.name, expected, "{f}"); + assert!(matches!( + flat.declared_type(expected).expect(expected), + Type::Extern(_) + )); + } +} + #[test] fn a_qualified_builtin_is_a_named_type() { assert!(matches!( @@ -191,17 +377,26 @@ fn a_raw_pointer_is_not_in_the_language() { ); } -/// A lifetime argument is accepted and not modelled — `Foo<'a, T>` classifies -/// as `Foo` with one type argument, and the spelling keeps the rest. +/// Generic arguments are accepted and not modelled — a reference is a *name*, and +/// the spelling keeps the rest. +/// +/// Nothing could read retained arguments: a surviving reference resolves to a +/// declared type, and no declaration takes type parameters. They are still lowered, +/// so a bad type inside one is diagnosed. #[test] -fn a_lifetime_argument_is_spelling_only() { +fn generic_arguments_are_spelling_only() { let ty = lower(quote::quote!(Foo<'a, u8>)).expect("in the language"); - let TypeKind::Named { id, args } = &ty.kind else { + let TypeKind::Named { id } = &ty.kind else { panic!("a named type"); }; assert_eq!(id.name, "Foo"); - assert_eq!(args.len(), 1); assert_eq!(tokens(&ty.origin.syntax), "Foo < 'a , u8 >"); + + // Lowered, so still checked: a tuple inside a generic argument is refused. + assert_eq!( + reason(quote::quote!(Foo<(u8, u8)>)), + UnsupportedTypeReason::UnsupportedTuple + ); } #[test] @@ -438,7 +633,7 @@ fn struct_shapes() { let tuple = parse_one(syn::parse_quote!( pub struct B(SomethingUnexpressible<'_, dyn Trait>); )); - assert_eq!(as_opaque(&tuple).name, "B"); + assert_eq!(as_extern(&tuple).name, "B"); let unit = parse_one(syn::parse_quote!( pub struct C; @@ -600,18 +795,39 @@ fn an_unmodelled_item_kind_is_diagnosed() { /// crate-private type gets a name in the flat API. That is what lets references /// be required to resolve, so it is the keystone of the whole model. #[test] -fn a_marked_alias_declares_an_opaque() { +fn a_marked_alias_declares_an_extern() { let element = parse_one(syn::parse_quote!( pub type Session = zenoh::Session; )); - let o = as_opaque(&element); - assert_eq!(o.name, "Session"); + let e = as_extern(&element); + assert_eq!(e.name, "Session"); + // What it points at is a modelled fact, so an adapter can recognise a target + // without taking the syntax apart. Not classified: a std type may hide behind a + // foreign alias, as `Error = zenoh::Error` does. + assert_eq!(e.target.as_deref(), Some("zenoh :: Session")); // The whole item survives, so a consumer can still read what it aliased. assert_eq!( - tokens(&o.origin.syntax), + tokens(&e.origin.syntax), "pub type Session = zenoh :: Session ;" ); + // A std target is recorded the same way — nothing here decides it is special. + let element = parse_one(syn::parse_quote!( + pub type Duration = std::time::Duration; + )); + assert_eq!( + as_extern(&element).target.as_deref(), + Some("std :: time :: Duration") + ); + + // A tuple struct points at nothing: it IS the definition. + let element = parse_one(syn::parse_quote!( + pub struct Handle(Whatever); + )); + let e = as_extern(&element); + assert_eq!(e.name, "Handle"); + assert_eq!(e.target, None); + // And it satisfies a reference, which is the point. let mut items = fixture_types(); items.push(syn::parse_quote!( @@ -930,7 +1146,7 @@ fn a_reference_resolves_to_its_declaration() { panic!("a nominal type"); }; let target = flat.resolve(id).expect("resolves"); - assert!(matches!(target, Type::Opaque(_))); + assert!(matches!(target, Type::Extern(_))); assert_eq!(target.name(), "Session"); } @@ -1008,12 +1224,14 @@ fn an_undeclared_reference_refuses_the_referencing_item() { quote::quote!(Result), quote::quote!(impl Fn(Session) + Send + Sync + 'static), quote::quote!([Session; 4]), - quote::quote!(Wrapper), + // NOT `Wrapper`: a declared type takes no type parameters, so a + // source writing that would not compile. Generic arguments are lowered and + // discarded — `generic_arguments_are_spelling_only` covers that they are + // still checked. ] { let flat = Flat::builder() .items(vec![ (opaque("Error"), loc()), - (opaque("Wrapper"), loc()), ( syn::parse_quote!( pub fn f(s: #ty) {} @@ -1244,7 +1462,7 @@ fn every_surviving_reference_resolves() { assert!(flat.function("takes_broken").is_none()); } -/// A generic alias is a generic binder like any other item's, and `Opaque` has no +/// A generic alias is a generic binder like any other item's, and `Extern` has no /// binder or arity — so accepting one would let `Handle` resolve against a /// declaration that says nothing about its parameter. It is also why /// `MaybeUninit` needed grammar support rather than an alias. @@ -1283,7 +1501,7 @@ fn a_generic_alias_is_refused() { let element = parse_one(syn::parse_quote!( pub type Borrowed<'a> = hidden::Borrowed<'a>; )); - assert_eq!(as_opaque(&element).name, "Borrowed"); + assert_eq!(as_extern(&element).name, "Borrowed"); } // ── The flat namespace ───────────────────────────────────────────────── diff --git a/prebindgen/src/api/core/flat/tests/mod.rs b/prebindgen/src/api/core/flat/tests/mod.rs index f6b70884..39dfaa0f 100644 --- a/prebindgen/src/api/core/flat/tests/mod.rs +++ b/prebindgen/src/api/core/flat/tests/mod.rs @@ -128,10 +128,10 @@ fn as_variant(e: &Element) -> &Variant { } } -fn as_opaque(e: &Element) -> &Opaque { +fn as_extern(e: &Element) -> &Extern { match as_type(e) { - Type::Opaque(o) => o, - other => panic!("expected an opaque, got {}", describe_type(other)), + Type::Extern(e) => e, + other => panic!("expected an extern, got {}", describe_type(other)), } } @@ -169,7 +169,7 @@ fn describe_type(t: &Type) -> String { Type::Struct(_) => "struct", Type::Variant(_) => "sum", Type::Enum(_) => "enum", - Type::Opaque(_) => "opaque", + Type::Extern(_) => "extern", }; format!("{kind} `{}`", t.name()) } diff --git a/prebindgen/src/api/core/flat/tests/roundtrip.rs b/prebindgen/src/api/core/flat/tests/roundtrip.rs index 3eba87e3..b1ee44ab 100644 --- a/prebindgen/src/api/core/flat/tests/roundtrip.rs +++ b/prebindgen/src/api/core/flat/tests/roundtrip.rs @@ -281,12 +281,12 @@ fn struct_delimiters_survive_and_spell() { (1, "C { x : __f0 }".to_string()) ); - // A tuple struct is a handle, so it is an `Opaque` and has no field list to + // A tuple struct is named-only, so it is an `Extern` and has no field list to // spell from — the delimiters still survive in its retained syntax. let element = parse_one(syn::parse_quote!( pub struct D(Whatever<'_, dyn Trait>); )); - let o = as_opaque(&element); + let o = as_extern(&element); assert_eq!(o.name, "D"); assert!(tokens(&o.origin.syntax).contains("Whatever")); } diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 73b9e23f..26327687 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -71,12 +71,7 @@ impl TypeRef { declared: &std::collections::HashSet, ) -> Option { match &self.kind { - TypeKind::Named { id, args } => { - if !declared.contains(&id.name) { - return Some(id.name.clone()); - } - args.iter().find_map(|a| a.first_unresolved(declared)) - } + TypeKind::Named { id } => (!declared.contains(&id.name)).then(|| id.name.clone()), TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => { t.first_unresolved(declared) } @@ -102,9 +97,8 @@ impl TypeRef { ok.collect_extents(out); err.collect_extents(out); } - TypeKind::Callback { args } | TypeKind::Named { args, .. } => { - args.iter().for_each(|t| t.collect_extents(out)) - } + TypeKind::Callback { args } => args.iter().for_each(|t| t.collect_extents(out)), + TypeKind::Named { .. } => {} TypeKind::Scalar(_) | TypeKind::Str | TypeKind::Unit => {} } } @@ -152,7 +146,7 @@ pub enum TypeKind { /// segment's generic arguments live in `args`, and only the *type* /// arguments: a lifetime argument says nothing a destination language can /// act on. The full spelling is in [`TypeRef::origin`] for whoever re-emits it. - Named { id: TypeId, args: Vec }, + Named { id: TypeId }, /// `[T; N]` — a run of `T` whose length is known at compile time. /// /// Deliberately not a [`Sequence`](TypeKind::Sequence) with an optional @@ -161,7 +155,10 @@ pub enum TypeKind { /// at every site. Array { elem: Box, - extent: ArrayExtent, + /// Boxed: an extent carries an [`Origin`] over the length expression, which + /// makes it the size outlier among the kinds, and an array is the rare one. + /// The same trade-off [`Unsupported::error`](super::Unsupported) makes. + extent: Box, }, /// A borrow — `&T`, `&mut T`, or `&mut MaybeUninit`. The lifetime is /// spelling, so it lives in [`TypeRef::origin`] rather than here. @@ -449,7 +446,7 @@ pub(crate) fn lower_type( .map_err(|e| fail(UnsupportedTypeReason::BadArrayExtent(Box::new(e))))?; TypeKind::Array { elem: Box::new(lower_type(&a.elem, consts, at)?), - extent, + extent: Box::new(extent), } } // The callback shape is decided by `extract_fn_trait_args`, this @@ -579,11 +576,11 @@ fn lower_path( let ok = Box::new(args.remove(0)); return Ok(TypeKind::Fallible { ok, err }); } - _ => return Ok(named(tp, args)), + _ => return Ok(named(tp)), } } } - Ok(named(tp, args)) + Ok(named(tp)) } /// If `ty` is a bare `MaybeUninit`, the `T` it holds storage for. @@ -626,7 +623,16 @@ pub(crate) fn is_unit_type(ty: &syn::Type) -> bool { /// `Named` with the identity read off the path: every segment joined, minus the /// generic arguments, which are already in `args`. -fn named(tp: &syn::TypePath, args: Vec) -> TypeKind { +/// `Named` with the identity read off the path: every segment joined, minus the +/// generic arguments. +/// +/// The arguments are **lowered but not retained** — a bad type inside one is still +/// diagnosed, it just leaves no trace. Nothing could read them: a surviving +/// reference resolves to a declared type, and no declaration takes type parameters, +/// so `Foo` against a declared `Foo` would not compile in the source crate. +/// Accepting *instantiated* generics — `Wrapper` as its own declared type — is +/// what would bring the field back. +fn named(tp: &syn::TypePath) -> TypeKind { let name = tp .path .segments @@ -636,6 +642,5 @@ fn named(tp: &syn::TypePath, args: Vec) -> TypeKind { .join("::"); TypeKind::Named { id: TypeId { name }, - args, } } diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs index 1665d73b..d80f87d2 100644 --- a/prebindgen/src/api/core/registry.rs +++ b/prebindgen/src/api/core/registry.rs @@ -103,7 +103,10 @@ impl TypeKey { /// input is not modified). pub fn from_type(ty: &syn::Type) -> Self { let mut t = ty.clone(); - crate::api::core::types_util::normalize_type(&mut t, &[]); + crate::api::core::types_util::normalize_type( + &mut t, + &crate::api::core::types_util::Normalization::prelude(), + ); Self { canon: t.to_token_stream().to_string().into(), ty: std::rc::Rc::new(t), @@ -730,25 +733,22 @@ impl Registry { let mut registry = Registry::default(); // Pass 1: collect and gather EVERY source module name first, so // cross-source type references (`source_a::TypeA` in a later-chained - // source's signature) normalize order-independently in pass 2. + // source's signature) normalize order-independently in pass 2 — and so do + // alias-named paths, whose declaration may arrive later or in another + // source. let items: Vec<(syn::Item, SourceLocation)> = items.into_iter().collect(); - for (_, loc) in &items { - if let Some(crate_name) = &loc.crate_name { - let module = crate_name.replace('-', "_"); - if !registry.source_modules.contains(&module) { - registry.source_modules.push(module); - } - } - } + let normalization = crate::api::core::types_util::Normalization::from_items(&items); + registry + .source_modules + .clone_from(&normalization.source_modules); // Pass 2: normalize each item's types to the canonical flat spelling - // (`crate::`/source-module paths reduce to the bare indexed name — - // see `normalize_type`'s rule list), then index. Every downstream - // `TypeKey::from_type` over a signature type therefore sees the - // normalized form, so bare adapter declarations match qualified - // captured spellings (issue #95). - let modules = registry.source_modules.clone(); + // (`crate::`/source-module paths reduce to the bare indexed name, and an + // aliased path to the name its alias gives it — see `normalize_type`'s + // rule list), then index. Every downstream `TypeKey::from_type` over a + // signature type therefore sees the normalized form, so bare adapter + // declarations match qualified captured spellings (issue #95). for (mut item, loc) in items { - crate::api::core::types_util::normalize_item_types(&mut item, &modules); + crate::api::core::types_util::normalize_item_types(&mut item, &normalization); let crate_name = loc.crate_name.clone(); let named: Option = match &item { syn::Item::Fn(f) => Some(f.sig.ident.clone()), diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index 5f0bcc9c..4c091f3b 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -3,9 +3,13 @@ //! replaces the per-module copies that used to live in `core::unfold`, //! `core::expand`, and the jnigen adapter. +use std::collections::HashMap; + use proc_macro2::Span; use quote::ToTokens; +use crate::SourceLocation; + /// The single-segment path type for a bare item ident (`Foo` → `Foo`) — /// direct construction, no string round trip, cannot fail. pub fn type_from_ident(ident: &syn::Ident) -> syn::Type { @@ -29,13 +33,23 @@ pub fn type_from_ident(ident: &syn::Ident) -> syn::Type { /// `#[prebindgen]` source crates chained into the registry, /// hyphens-as-underscores) reduces the same way (`myflat::Foo` ≡ `Foo`). /// Pure callers pass `&[]`. -/// 4. The std prelude whitelist reduces to its bare form — exactly -/// `std|core|alloc :: vec::Vec | option::Option | result::Result | -/// string::String | boxed::Box` (with or without a leading `::`). -/// Nothing else: `std::ffi::CString` stays qualified, and unknown crate -/// paths (`zenoh::KeyExpr`) are NEVER touched — the registry has no -/// index of a foreign namespace, so `a::KeyExpr` and `b::KeyExpr` may be -/// genuinely distinct types and their spelling is their identity. +/// 4. A **prelude** path reduces to the bare name the language knows it by — +/// exactly [`Normalization::PRELUDE`], with `core`/`alloc` read as `std`. +/// Each entry names a *constructor*, so arguments are preserved: +/// `std::vec::Vec` ≡ `Vec`. +/// +/// Nothing else. `std::ffi::CString` stays qualified, and so does a +/// foreign path (`zenoh::KeyExpr`) **even when an alias names that +/// type**: a `#[prebindgen] pub type` is a one-way road, bringing a +/// foreign type into the flat API under a name that is thereafter the +/// only way to spell it. It declares +/// an [`Extern`](crate::core::flat::Extern); it is not an equivalence. +/// +/// That keeps the rule meaning-preserving, which is the whole contract +/// here: reduction may choose among spellings of ONE type, never change +/// what a type is. Treating an alias as an equivalence broke that — +/// `Vec` ≡ `Bytes` turns a sequence into an extern — and no +/// key-shape refinement fixes the category error. /// 5. Lifetimes are NOT normalized (`&'a T` ≠ `&T`, `Foo<'static>` ≠ `Foo`) /// — [`match_pattern`] treats lifetimes as fixed structure and /// foreign-type declarations (`ptr_class!(ZKeyExpr<'static>)`) rely on @@ -44,10 +58,118 @@ pub fn type_from_ident(ident: &syn::Ident) -> syn::Type { /// Idempotent; recurses through references, slices, tuples, pointers, /// generic arguments, and `impl Trait` bounds. Paths with a qualified self /// (`::Assoc`) are left untouched. -pub fn normalize_type(ty: &mut syn::Type, source_modules: &[String]) { +/// What a captured path may be reduced against: the ingested source crates' own +/// modules, and every name an alias gives to a foreign path. +/// +/// One value rather than a bare `&[String]`, because reduction has one rule and +/// two sources of aliases feeding it — see [`normalize_type`]'s rule list. +/// [`Self::default`] is the prelude alone, which is what a caller normalizing a +/// lone type (rather than an ingested stream) wants. +#[derive(Clone, Debug)] +pub struct Normalization { + /// Module name per ingested source, first-seen order. The first doubles as the + /// default module for references with no recorded origin. + pub source_modules: Vec, + /// Constructor path → the bare name the language knows it by, from + /// [`Self::PRELUDE`] alone. Matched with the use site's type arguments ignored + /// and preserved, because a prelude entry names a constructor: + /// `std::vec::Vec` is every `Vec`. + /// + /// A crate's `#[prebindgen] pub type` is deliberately **not** here — see + /// [`normalize_type`]'s rule 4. + constructors: HashMap, +} + +impl Normalization { + /// The names the language **pre-declares**, so no source crate has to write + /// them — exactly Rust's own idea of a prelude, a set of `use`s you need not + /// write. A crate need not write `use std::vec::Vec`, and need not write + /// `#[prebindgen] pub type Vec = std::vec::Vec` either, for the same reason. + /// + /// Not identical to Rust's prelude: it adds `MaybeUninit`, which the grammar + /// recognises for out-parameters. Its entries are exactly the bare names + /// [`lower_path`](crate::core::flat) classifies as builtins and that have a + /// std path at all — `str` has none, and neither do the scalars. + /// + /// Written with the `std` root; `core` and `alloc` are re-exports of the same + /// items, so a leading `core`/`alloc` is read as `std` before matching. + pub const PRELUDE: &'static [(&'static str, &'static str)] = &[ + ("std::vec::Vec", "Vec"), + ("std::option::Option", "Option"), + ("std::result::Result", "Result"), + ("std::string::String", "String"), + ("std::boxed::Box", "Box"), + ("std::mem::MaybeUninit", "MaybeUninit"), + ]; + + /// The prelude alone: no ingested sources, no declared aliases. + pub fn prelude() -> Self { + Self { + source_modules: Vec::new(), + constructors: Self::PRELUDE + .iter() + .map(|(path, name)| ((*path).to_string(), (*name).to_string())) + .collect(), + } + } + + /// Collect from a captured stream, before anything is normalized. + /// + /// Both entry points — `FlatBuilder::build` and `Registry::from_items` — build + /// this, so they cannot normalize differently. Gathering every module and alias + /// first is what makes reduction order-independent: a signature may name a type + /// whose alias is declared later, or in another source. + pub fn from_items(items: &[(syn::Item, SourceLocation)]) -> Self { + let mut out = Self::prelude(); + for (_, loc) in items { + if let Some(crate_name) = &loc.crate_name { + let module = crate_name.replace('-', "_"); + if !out.source_modules.contains(&module) { + out.source_modules.push(module); + } + } + } + out + } + + /// The bare name the language knows this constructor by, arguments ignored. + fn constructor_of(&self, path: &syn::Path) -> Option<&str> { + self.constructors + .get(&constructor_key(path)) + .map(String::as_str) + } +} + +impl Default for Normalization { + fn default() -> Self { + Self::prelude() + } +} + +/// A path as a key: segments joined, arguments dropped, and a leading +/// `core`/`alloc` read as `std` since they re-export the same items. +/// +/// Only [`Normalization::constructors`] is keyed this way, and a constructor is +/// exactly a path without arguments — `std::vec::Vec` matches every `Vec`. +fn constructor_key(path: &syn::Path) -> String { + let mut out = String::new(); + for (i, seg) in path.segments.iter().enumerate() { + if i > 0 { + out.push_str("::"); + } + let mut ident = seg.ident.to_string(); + if i == 0 && (ident == "core" || ident == "alloc") { + ident = "std".to_string(); + } + out.push_str(&ident); + } + out +} + +pub fn normalize_type(ty: &mut syn::Type, against: &Normalization) { use syn::visit_mut::VisitMut; struct Normalizer<'a> { - modules: &'a [String], + against: &'a Normalization, } impl VisitMut for Normalizer<'_> { fn visit_type_mut(&mut self, ty: &mut syn::Type) { @@ -61,16 +183,13 @@ pub fn normalize_type(ty: &mut syn::Type, source_modules: &[String]) { } if let syn::Type::Path(tp) = ty { if tp.qself.is_none() { - reduce_flat_path(&mut tp.path, self.modules); + reduce_flat_path(&mut tp.path, self.against); } } syn::visit_mut::visit_type_mut(self, ty); } } - Normalizer { - modules: source_modules, - } - .visit_type_mut(ty); + Normalizer { against }.visit_type_mut(ty); } /// Apply [`normalize_type`] to every type position inside an item — fn @@ -78,29 +197,41 @@ pub fn normalize_type(ty: &mut syn::Type, source_modules: &[String]) { /// pass ([`crate::api::core::registry::Registry::from_items`]) that makes /// captured spellings canonical before any key is formed, so every /// downstream `TypeKey::from_type` sees the flat spelling. -pub fn normalize_item_types(item: &mut syn::Item, source_modules: &[String]) { +pub fn normalize_item_types(item: &mut syn::Item, against: &Normalization) { use syn::visit_mut::VisitMut; + struct ItemNormalizer<'a> { - modules: &'a [String], + against: &'a Normalization, } impl VisitMut for ItemNormalizer<'_> { fn visit_type_mut(&mut self, ty: &mut syn::Type) { // Normalizes the whole subtree; no further descent needed. - normalize_type(ty, self.modules); + normalize_type(ty, self.against); } } - ItemNormalizer { - modules: source_modules, - } - .visit_item_mut(item); + ItemNormalizer { against }.visit_item_mut(item); } /// The path-reduction step of [`normalize_type`]: collapse a reducible /// multi-segment path to its final segment. See the rule list there. -fn reduce_flat_path(path: &mut syn::Path, source_modules: &[String]) { +fn reduce_flat_path(path: &mut syn::Path, against: &Normalization) { if path.segments.len() < 2 { return; } + + // A prelude entry names a CONSTRUCTOR, so arguments are ignored when matching + // and preserved when rewriting: `std::vec::Vec` is `Vec`. A crate's + // own alias is NOT consulted — see rule 4. + if let Some(name) = against.constructor_of(path) { + let mut last = path.segments.last().expect("len checked").clone(); + last.ident = syn::Ident::new(name, last.ident.span()); + path.leading_colon = None; + path.segments = std::iter::once(last).collect(); + return; + } + + // Otherwise only a prefix into the flat namespace reduces, to the final + // segment: this crate's own path, or an ingested source's module. let head = path .segments .first() @@ -109,26 +240,7 @@ fn reduce_flat_path(path: &mut syn::Path, source_modules: &[String]) { .to_string(); let reduce = match head.as_str() { "crate" | "self" => true, - "std" | "core" | "alloc" => { - let tail: Vec = path - .segments - .iter() - .skip(1) - .map(|s| s.ident.to_string()) - .collect(); - matches!( - tail.iter() - .map(String::as_str) - .collect::>() - .as_slice(), - ["vec", "Vec"] - | ["option", "Option"] - | ["result", "Result"] - | ["string", "String"] - | ["boxed", "Box"] - ) - } - other => source_modules.iter().any(|m| m == other), + other => against.source_modules.iter().any(|m| m == other), }; if reduce { let last = path.segments.last().expect("len checked").clone(); From 3d8f2921a07ec339f953588d6ee92b64b300fa9c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 30 Jul 2026 15:51:36 +0200 Subject: [PATCH 05/52] Cow<'_, T> is transparent, like Box (#236) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `zenoh-flat`'s `zbytes_to_bytes(z: &ZBytes) -> Cow<'_, [u8]>` was refused by the closed flat API, so it would vanish when that crate migrates. The cause was an assumption in `lower_path`'s guard — "a builtin generic takes types only; a lifetime argument on one is not a shape this language has" — which skips the whole builtin match when any lifetime argument is present. `Cow` is the counterexample it did not anticipate: a builtin generic whose own signature includes a lifetime. So `Cow<'_, [u8]>` fell through to an undeclared nominal `Cow` and the item was refused. **A `Cow` carries nothing a destination language can see, and both adapters already say so in code.** cbindgen: "`Cow<'_, [T]>` → `T_wire* + size_t`. The C side receives an owned malloc'd copy, just like `Vec` outputs", and `type_contains_vec` groups the two. jnigen: `env.byte_array_from_slice(&v)` — `&Cow<[u8]>` derefs to `&[u8]`, so there is no Cow-specific conversion at all — yielding Kotlin `ByteArray`, exactly what `Vec` yields. So `Cow<'_, T>` classifies as `T`'s own kind, the `Box` treatment, and no `TypeKind` variant is added: the semantic surface says nothing about a fact no destination acts on. What codegen genuinely needs is the *spelling* — jnigen rewrites its generated fn's param type to `::std::borrow::Cow<'_, [u8]>` because "the param type must be resolvable without imports" — and spelling already travels in `origin`. Classify off `kind`, spell off `origin`, with both adapters' existing behaviour now predicted by the classification instead of special-cased. Transparent for any target, as `Box` is. Whether a `Cow` can actually cross stays the adapter's call, and both already restrict — cbindgen to scalar slices, jnigen to `[u8]` — refusing the rest with their own diagnostics. `std::borrow::Cow` joins the prelude, for the reason every entry is there: a name no source has to import. It also stops the frontend being *stricter* than the adapters, which tail-match the last path segment and so accept a qualified spelling — the cbindgen fixture `cow_u8_returns_scalar_array` writes exactly that, which is the proof the qualified form occurs. Verified the three new rows fail against the old guard before keeping them. Generation byte-identical, ledger unmoved, 48 JVM sections. zenoh-flat is a separate repo, so `zbytes_to_bytes` is covered by an acceptance row rather than by a build. Co-authored-by: Claude Opus 5 --- .../src/api/core/flat/tests/acceptance.rs | 80 +++++++++++++++++++ prebindgen/src/api/core/flat/ty.rs | 24 +++++- prebindgen/src/api/core/types_util.rs | 4 +- 3 files changed, 104 insertions(+), 4 deletions(-) diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index 5c4e0155..b40772be 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -131,6 +131,8 @@ fn the_prelude_reaches_every_builtin_by_either_spelling() { let bare: proc_macro2::TokenStream = match *name { "Result" => quote::quote!(Result), "String" => quote::quote!(String), + // `Cow` needs a lifetime and an unsized target to be valid Rust. + "Cow" => quote::quote!(Cow<'_, [u8]>), _ => { let n = quote::format_ident!("{name}"); quote::quote!(#n) @@ -141,6 +143,7 @@ fn the_prelude_reaches_every_builtin_by_either_spelling() { match *name { "Result" => quote::quote!(#p), "String" => quote::quote!(#p), + "Cow" => quote::quote!(#p<'_, [u8]>), _ => quote::quote!(#p), } }; @@ -361,6 +364,83 @@ fn a_sequence_is_a_sequence_borrowed_or_owned() { assert!(matches!(inner.kind, TypeKind::Sequence(_))); } +/// `Cow<'_, T>` **is** `T`, the same treatment `Box` gets: borrowed or owned, and +/// no destination language can tell. +/// +/// Both adapters already behave that way — cbindgen lowers `Cow<'_, [T]>` "just like +/// `Vec` outputs", and jnigen's converter is `byte_array_from_slice(&v)`, which +/// works by deref and is identical to the `Vec` one — so this classification +/// predicts their behaviour rather than leaving it a special case. +#[test] +fn a_cow_is_what_it_borrows() { + // The property the whole treatment rests on: indistinguishable from the owned + // spelling of the same thing. + assert_eq!( + format!("{:?}", kind(quote::quote!(Cow<'_, [u8]>))), + format!("{:?}", kind(quote::quote!(Vec))), + "a byte Cow classifies exactly as a byte Vec" + ); + assert!(matches!(kind(quote::quote!(Cow<'_, str>)), TypeKind::Str)); + + // The `Cow` survives where codegen reads it: a generated signature must spell + // `Cow<'_, [u8]>`, which is not interchangeable with `Vec` in Rust. + let ty = lower(quote::quote!(Cow<'_, [u8]>)).expect("in the language"); + assert_eq!(tokens(&ty.origin.syntax), "Cow < '_ , [u8] >"); + + // Transparent for any target, as `Box` is: whether it can actually cross is the + // adapter's call, and both already restrict which elements they accept. + let TypeKind::Sequence(elem) = kind(quote::quote!(Cow<'_, [Sample]>)) else { + panic!("a sequence"); + }; + assert!(matches!(elem.kind, TypeKind::Named { .. })); + + // A lifetime argument is expected on `Cow` alone. On any other builtin it is + // still not a shape the language has, so the exception is exactly one name wide: + // `Vec<'a, u8>` is a nominal `Vec` nobody declared, and the item is refused. + let element = { + let mut items = fixture_types(); + let n = items.len(); + items.push(syn::parse_quote!( + pub struct S { + pub f: Vec<'a, u8>, + } + )); + parse(items).remove(n) + }; + assert!(matches!( + as_unsupported(&element), + ItemError::UnresolvedType { name } if name == "Vec" + )); +} + +/// The signature that motivated this: zenoh-flat's `zbytes_to_bytes`. It was refused +/// under the closed API because a lifetime argument sent `Cow` to an undeclared +/// nominal type. +#[test] +fn a_cow_returning_accessor_resolves() { + let flat = Flat::builder() + .items( + vec![ + syn::parse_quote!( + pub type ZBytes = zenoh::bytes::ZBytes; + ), + syn::parse_quote!( + pub fn zbytes_to_bytes(z: &ZBytes) -> Cow<'_, [u8]> {} + ), + ] + .into_iter() + .map(|i: syn::Item| (i, loc())), + ) + .build() + .expect("parses"); + + assert_eq!(flat.unsupported().count(), 0, "no longer refused"); + let f = flat.function("zbytes_to_bytes").expect("survives"); + assert!(matches!(f.ret.kind, TypeKind::Sequence(_))); + // And the return still spells its `Cow`, so an adapter can emit the signature. + assert_eq!(tokens(&f.ret.origin.syntax), "Cow < '_ , [u8] >"); +} + /// A raw pointer is not in the language. A `#[prebindgen]` crate is idiomatic /// Rust and the adapter owns the lowering to pointers — no adapter has a /// selection arm for one, so accepting it would only defer the failure to a diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 26327687..00b27725 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -534,9 +534,11 @@ fn lower_path( return Ok(TypeKind::Str); } } - // A builtin generic takes types only; a lifetime argument on one is not - // a shape this language has. - if !has_lifetime_arg { + // A builtin generic takes TYPE arguments only — a lifetime on one is not a + // shape this language has — with one exception: `Cow`'s own signature HAS a + // lifetime, so it is the one builtin where a lifetime argument is expected + // rather than refused. + if !has_lifetime_arg || name == "Cow" { let mut args = args; let arity = |n: usize| { if args.len() == n { @@ -570,6 +572,22 @@ fn lower_path( arity(1)?; return Ok(args.remove(0).kind); } + // `Cow<'_, T>` **is** `T`, for the same reason `Box` is: borrowed + // or owned, and no destination language can tell. Both adapters + // already say exactly that — cbindgen lowers it "just like `Vec` + // outputs", and jnigen's converter body is + // `byte_array_from_slice(&v)`, which works by deref and is identical + // to the `Vec` one. So this classification *predicts* their + // behaviour instead of leaving it a special case. + // + // The `Cow` survives in `TypeRef::origin`, which is where an adapter + // reads the param type its generated fn must spell — and it must, + // since `Cow<'_, [u8]>` is not interchangeable with `Vec` in a + // Rust signature. + "Cow" => { + arity(1)?; + return Ok(args.remove(0).kind); + } "Result" => { arity(2)?; let err = Box::new(args.remove(1)); diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index 4c091f3b..6a28eaf2 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -87,7 +87,8 @@ impl Normalization { /// `#[prebindgen] pub type Vec = std::vec::Vec` either, for the same reason. /// /// Not identical to Rust's prelude: it adds `MaybeUninit`, which the grammar - /// recognises for out-parameters. Its entries are exactly the bare names + /// recognises for out-parameters, and `Cow`, which it treats as transparent. + /// Its entries are exactly the bare names /// [`lower_path`](crate::core::flat) classifies as builtins and that have a /// std path at all — `str` has none, and neither do the scalars. /// @@ -100,6 +101,7 @@ impl Normalization { ("std::string::String", "String"), ("std::boxed::Box", "Box"), ("std::mem::MaybeUninit", "MaybeUninit"), + ("std::borrow::Cow", "Cow"), ]; /// The prelude alone: no ingested sources, no declared aliases. From 4a10ea028e595b1939249f438d18d3797d873cc4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 30 Jul 2026 22:56:48 +0200 Subject: [PATCH 06/52] L1: Registry consumes Flat (#238) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make every test fixture self-sufficient Preparation for L1, where `Registry` consumes `Flat` and an item naming a type the flat API does not declare stops being ingested. 167 of 524 tests held such an item; this makes them all declare what they name, verified against a temporary `#[cfg(test)]` check inside `from_items` that the next commit deletes. **`declare_referenced`** appends a marked alias for every nominal type a stream names but never declares, to a fixed point. Most fixtures are *about* a plan shape or a converter, and a handle declaration is noise in them — `reg_with(&["fn get(s: &Storage) -> Payload"])` is testing an unfold plan, not what `Storage` is. Declaring those as `Extern`s is what a real source crate does for a foreign handle, and it is inert either way: a type alias lands in no registry map. `reg_with` now parses `syn::Item`, so a fixture *can* declare its own types when that is the subject. Four things the helper cannot cover, each a real correction: **`std::time::Duration`** was spelled path-qualified in 15 places. A qualified name can never be a flat-API name, so those fixtures now declare `Duration` and spell it bare — the shape a real source crate uses. That moves the `TypeKey`, so the matching `convert!` and two generated-name assertions move with it. **Two array-length "qualification" tests** asserted that `Holder::N` and `array_len()` lengths get qualified. The subgrammar was narrowed to "an integer literal or the bare name of a marked const" in #212, so neither can reach an adapter any more; they survived only because `from_items` never validated lengths. Reduced to the form that can. (jnigen's qualifier still handles the dead shapes — removing that is L4's business.) **Three array-length rejection tests** move to `flat/tests/acceptance.rs`, where the subgrammar lives. They cover the dangerous family — `const {}`, `match`, `if let`, all of which bind a local that could shadow a marked item — and belong with the classification, not with jnigen. **Two registry tests are removed**, not edited: both assert that ingestion does *not* validate signatures, which is precisely what the next commit reverses. Their replacement lands there. Co-Authored-By: Claude Opus 5 * Registry consumes Flat L1 of #229. `Registry::from_items` indexed the raw item stream itself, so the registry and `Flat` were two readings of one source that could disagree. The registry is now a **projection** of the model: `from_items` is `Flat::builder().items(..).build()` + `from_flat`, and the maps are arranged from elements the frontend already classified. The `Flat` is **held**, not discarded — `registry.flat()`. That is what makes the projection framing real rather than a slogan, and it is how L2–L4 reach the model: an adapter already has the registry. The maps stay owned rather than becoming live queries, because they are a projection *plus* synthesis: `resolve()` injects adapter-declared binding-local fns straight into `functions`. Projection rules worth stating, because two are asymmetries: * an unnamed `const _` — each source's injected `konst` guard — is the whole of `passthrough` now. The proc-macro refuses to mark a `use`/`mod`/`macro_rules!`, so nothing else ever reached it. * an `Extern` lands in **no** map. A type alias was already a no-op here, and keeping it that way is what holds generation byte-identical. It is reachable through `flat()` for the stages that will want it. **Ingestion now checks that the flat API is expressible.** A `self` receiver, an `async fn`, a generic binder, a type form outside the grammar, or a reference to a type the flat API does not declare fails the build — reporting **all** offenders at once, so a source crate that needs migrating sees one list rather than one rebuild per item. An opt-out for deliberately-unsupported elements is filed separately. That makes three registry guards unreachable, so they and their `ScanError` variants are deleted: `UnsupportedReceiver`, `UnsupportedParamPattern`, `DisallowedImplTrait`. The frontend's diagnosis is strictly richer — it names the parameter the bad type sits on. `index_item`, `check_no_duplicate` and `first_seen_loc` go with them: `Flat` owns both indexing and duplicate detection. `ParseError::DuplicateName` gains the two crate names, so the one authority produces the message that names both colliding sources. covertest-kotlin's hand-rolled closure assertion is removed — it was a stopgap for exactly this stage, and the registry now raises the same thing. Generation is byte-identical, the JVM covertest passes all 48 sections, and the boundary ledger drops to 204 (the deleted `impl Trait` classifier). Co-Authored-By: Claude Opus 5 * Record L1 in the stage map Ticks L1 and records the decision that supersedes its original wording: an item the language cannot express fails ingestion rather than staying inert until declared. Also notes the measured fixture cost and what is still open — zenoh-flat's 26 unmarked aliases, which block its two consumers until marked. Co-Authored-By: Claude Opus 5 * Point the L1 note at the filed opt-out issue Co-Authored-By: Claude Opus 5 * Address review: validate the one input that bypasses Flat **The diagnostics regression (review 2.1).** `resolve()` inserts `adapter.local_functions()` straight into `self.functions`, so a `sig!(..)` written by hand in a build script never touches `Flat`. Deleting `scan_fn_signature`'s receiver and pattern guards therefore did not merely move those checks — it removed them for that input. `sig!((self, x: u32) -> Ret)` would `continue` here, `continue` again in `fn_plan`, and drop the parameter silently; the user would meet it as an arity mismatch out of rustc on generated code. Fixed where the reviewer suggested, at synthesis rather than back in `scan_fn_signature`: `Flat::check_signature` runs the frontend's own `lower_fn` over a local fn, so the grammar stays decided in one place and the check sits on the one input that bypasses it. Grammar only — whether a local fn's types are *declared* is a whole-model question, and a binding-local fn may legitimately name types the source crate never did. Both halves are tested. The two "cannot reach here" comments now say why, naming both paths. **The report loses the crate (review 1.2).** A captured path is crate-relative, so two offenders read `src/lib.rs:0:0` and the location alone cannot say which crate to fix — exactly why this PR added crate names to duplicate-name diagnostics. `NotExpressible` now renders `in crate `x`` using the same `in_crate` phrasing, with a two-source test whose offenders share a file path. The trailing newline is gone with it. `DeclaredNotFound` and `QualifiedDeclaredTypes` have the same trailing-newline shape and are left alone as pre-existing. **`Registry::default()` (review 2.2).** A registry built that way projects nothing, so `flat()` would hand a later stage an empty model claiming to be its source. The `Default` impl becomes `pub(crate) fn empty()`: outside the crate the entry points are `from_items`, `from_flat` and `builder`, each with a model behind it. Nothing required the bound; in-tree fixtures were the only callers. **Flat's docs promised the opposite (review 1.1).** They said an `Unsupported` element stays inert until an adapter declares it, which this PR supersedes. Rewritten around the actual split — **parsing diagnoses, ingestion raises** — which is what lets one model serve both a consumer inspecting what a crate marked and a binding that must be built against a model read in full. Four sites, including `Element::Unsupported` and `Flat::unsupported`. **Untested behaviour changes (reviews 1.3, 2.3).** `from_flat` had no direct test; everything reached it through `from_items`, which cannot tell "the projection is right" from "parser and projection are wrong in matching ways". Added one asserting every element kind's destination, that the model is kept, and — the change the reviewer caught — that an `Extern` now records an origin where the old `syn::Item::Type` no-op recorded none, so a helper-crate alias qualifies against the helper crate instead of the default module. Generation byte-identical, 524 + 452 tests, covertest 48 sections. The local-fn guard was checked against its own removal and fails as it should. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- docs/language-integration.md | 45 ++- examples/covertest-kotlin/build.rs | 30 -- prebindgen/src/api/core/flat/boundary.ledger | 4 +- prebindgen/src/api/core/flat/element.rs | 8 +- prebindgen/src/api/core/flat/mod.rs | 93 ++++- .../src/api/core/flat/tests/acceptance.rs | 31 ++ prebindgen/src/api/core/registry.rs | 360 ++++++++++-------- prebindgen/src/api/core/registry/tests.rs | 284 ++++++++++++-- prebindgen/src/api/core/resolve/tests.rs | 4 +- prebindgen/src/api/core/write/tests.rs | 4 +- .../src/api/lang/cbindgen/tests/aliasing.rs | 2 +- .../cbindgen/tests/boundary_invariants.rs | 6 +- .../src/api/lang/cbindgen/tests/builder.rs | 20 +- .../src/api/lang/cbindgen/tests/callbacks.rs | 16 +- .../src/api/lang/cbindgen/tests/errors.rs | 19 +- .../src/api/lang/cbindgen/tests/inputs.rs | 29 +- .../src/api/lang/cbindgen/tests/lowering.rs | 41 +- prebindgen/src/api/lang/cbindgen/tests/mod.rs | 1 + .../src/api/lang/cbindgen/tests/returns.rs | 45 ++- .../src/api/lang/cbindgen/tests/structs.rs | 40 +- .../api/lang/cbindgen/tests/tagged_unions.rs | 48 +-- .../src/api/lang/jnigen/jni/overloads.rs | 11 +- .../src/api/lang/jnigen/jni/tests/aliasing.rs | 2 +- .../api/lang/jnigen/jni/tests/callbacks.rs | 15 +- .../src/api/lang/jnigen/jni/tests/config.rs | 41 +- .../src/api/lang/jnigen/jni/tests/consts.rs | 31 +- .../lang/jnigen/jni/tests/cross_artifact.rs | 3 +- .../src/api/lang/jnigen/jni/tests/flatten.rs | 82 ++-- .../src/api/lang/jnigen/jni/tests/mod.rs | 1 + .../src/api/lang/jnigen/jni/tests/niches.rs | 12 +- .../src/api/lang/jnigen/jni/tests/sealed.rs | 76 ++-- .../api/lang/jnigen/jni/tests/snapshots.rs | 18 +- .../src/api/lang/jnigen/jni/tests/symbols.rs | 13 +- .../api/lang/jnigen/jni/tests/value_form.rs | 81 ++-- .../src/api/lang/jnigen/jni/tests/values.rs | 268 ++++--------- prebindgen/src/api/test_util.rs | 72 +++- 36 files changed, 1126 insertions(+), 730 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 6a6adc67..719387e1 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -96,7 +96,7 @@ moves it. |---|---|---| | L0 | `Language` + `Element` + the ledger | **done** — [#227](https://github.com/milyin/prebindgen/pull/227) | | L0.5 | `Flat`: the model, indexed and resolved | **done** — this branch | -| L1 | `Registry` consumes elements | not started | +| L1 | `Registry` consumes elements | **done** — this branch | | L2 | `api/core` stops classifying source syntax | not started | | L3 | `Cbindgen` consumes elements | not started | | L4 | `JniGen` consumes elements *(the long pole — 105 sites)* | not started | @@ -155,22 +155,37 @@ same treatment before they parse. `Cow<'_, [u8]>` has no alias spelling — gene and lifetime-bearing — so `zbytes_to_bytes` needs either the `MaybeUninit` treatment or a signature change. -### L1 — `Registry` consumes elements +### L1 — `Registry` consumes elements — **done** + +The seam that makes the direction real. Adapters were not touched. + +- [x] `Registry::from_flat(Flat)`; `from_items` is `Flat::builder` + `from_flat`, + so both entry points share one parser +- [x] The registry **holds** the model (`registry.flat()`), which is how L2–L4 + reach it: an adapter already has the registry +- [x] The maps are a projection of the elements — plus synthesis, since `resolve` + injects adapter-declared binding-local fns into `functions` +- [x] `scan_fn_signature`'s receiver / parameter-pattern / `impl Trait` guards + deleted with their `ScanError` variants, along with `index_item`, + `check_no_duplicate` and `first_seen_loc`: `Flat` owns indexing and + duplicate detection +- [x] `ParseError::DuplicateName` carries both crate names, so one authority + produces the message +- [x] **Did not move**: every generated artifact byte-identical -The seam that makes the direction real. Adapters must not need touching. +**Correctness is checked by default**, superseding L0's "inert until declared": +ingestion fails on anything the language cannot express, listing every offender at +once so a source crate needing migration sees one list. An opt-out for +deliberately-unsupported elements is #237. -- [ ] `Registry::from_flat(&Flat)`; `from_items` becomes `Flat::builder` + - `from_flat`, so both entry points share one parser -- [ ] The `functions` / `structs` / `enums` / `consts` / `passthrough` maps are - rebuilt from each element's retained `syntax` — a projection, not a second - source of truth -- [ ] `scan_fn_signature`'s receiver / parameter-pattern / `impl Trait` guards - are deleted: the diagnosis is already on `Element::Unsupported`, and - declaring such an item is what raises it -- [ ] `ScanError`'s per-item variants map onto `ItemError`, so one authority - produces the message -- [ ] **Must not move**: every generated artifact byte-identical - (`examples/regen-check.sh`) +The cost landed in test fixtures: 167 of 524 tests held an item naming a type they +never declared. `test_util::declare_referenced` supplies a marked alias for those +where the handle is incidental; the rest were real corrections — a path-qualified +`std::time::Duration` that no declaration can name, and two array-length tests +asserting shapes the subgrammar dropped in #212. + +**Still open**: `zenoh-flat`'s 26 unmarked aliases. Until they are marked, +`zenoh-flat-c` and `zenoh-flat-jni` do not generate. ### L2 — `api/core` stops classifying source syntax diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index 3e9ebba6..facf6762 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -118,36 +118,6 @@ fn strip_flat_class_prefix(class: &str, name: &str) -> String { } fn main() { - // The flat API must be CLOSED: every type a marked signature names has to be - // declared here too, as a struct/enum or as `#[prebindgen] pub type X = ..` - // for a handle. `Flat` proves that across BOTH sources at once, which is the - // only way the helper crate's references to `perftest-flat`'s types can - // resolve — it cannot mark them itself. - // - // Asserted rather than merely computed: without this, an unmarked type would - // go unnoticed until the adapters consume elements (L1+), and then surface as - // a late unresolved-converter error instead of naming the missing marker. - // - // `source_named` for the helpers, for the reason spelled out at the registry - // below: the dep is renamed in Cargo.toml. - let flat = prebindgen::core::Flat::builder() - .source(perftest_flat::PREBINDGEN_OUT_DIR) - .source_named(cov_helpers::PREBINDGEN_OUT_DIR, "cov_helpers") - .build() - .expect("the flat API parses"); - let unresolved: Vec = flat - .unsupported() - .map(|u| match &u.name { - Some(name) => format!(" {name}: {}", u.error), - None => format!(" {}", u.error), - }) - .collect(); - assert!( - unresolved.is_empty(), - "the flat API is not closed:\n{}", - unresolved.join("\n") - ); - let jni = JniGen::new() .set_package_prefix("io.prebindgen.covertest") .set_jni_native_init("io.prebindgen.covertest.NativeLibrary.ensureLoaded()") diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 22de7c0d..9a90f23d 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -42,7 +42,7 @@ # gaps are listed here rather than implied away. 4 api/core/expand.rs -12 api/core/registry.rs +11 api/core/registry.rs 40 api/core/types_util.rs 18 api/core/unfold.rs 8 api/lang/cbindgen/builder.rs @@ -70,4 +70,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 205 +# total: 204 diff --git a/prebindgen/src/api/core/flat/element.rs b/prebindgen/src/api/core/flat/element.rs index 87c71dfd..f8193a5c 100644 --- a/prebindgen/src/api/core/flat/element.rs +++ b/prebindgen/src/api/core/flat/element.rs @@ -30,9 +30,11 @@ pub enum Element { /// grammar, a `self` receiver, a reference to a type the flat API never /// declares, or a whole item kind it does not model such as a `union`. /// - /// Inert: it is indexed under its name so nothing else can claim it, and - /// the diagnosis rides along, to be raised by whatever declares it. See the - /// [module docs](super) on where acceptance is enforced. + /// Indexed under its name so nothing else can claim it, with the diagnosis + /// riding along. Parsing carries it; building a + /// [`Registry`](crate::core::Registry) from a model holding one fails, reporting + /// every offender at once. See the [module docs](super) on where acceptance + /// is enforced. Unsupported(Unsupported), } diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index d20b9a18..abd9b191 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -96,14 +96,23 @@ //! [`TypeKind`] is a form the language does not accept, so there is no second //! acceptance list to drift from it. //! -//! But an item the language cannot express is not automatically a build failure. -//! A source crate may mark items no binding uses, and those have never been -//! required to be expressible — the pipeline scans a signature only once an -//! adapter *declares* it. So parsing diagnoses per item and defers the raising: -//! such an item becomes [`Element::Unsupported`], carrying the diagnosis, inert -//! until something declares it. Only whole-stream rules — a duplicate name in -//! the flat namespace — are [`ParseError`]s, because no declaration can make -//! two items with one name unambiguous. +//! **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 [`ParseError`]s, 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`](crate::core::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. //! //! There is **no verbatim passthrough**, because a `#[prebindgen]` crate marks //! the items that cross the boundary and leaves the supporting code to the @@ -133,7 +142,8 @@ //! | `fn f(a: u8, ...)` | a function without the tail | the variadic arguments vanish | //! | `struct S`, `fn f()`, `struct S` | `T` as a nominal reference | a parameter is indistinguishable from an item named `T` | //! -//! All three are [`ItemError`]s, inert until declared, like any other refusal. A +//! All three are [`ItemError`]s, 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, @@ -340,6 +350,8 @@ impl FlatBuilder { name: first_name.clone(), first: first.clone(), second: element.location().clone(), + first_crate: first.crate_name.clone(), + second_crate: element.location().crate_name.clone(), }))); } seen.push((name.clone(), element.location().clone())); @@ -376,13 +388,13 @@ impl FlatBuilder { /// Every [`TypeKind::Named`] in a surviving element denotes a [`Type`] this model /// holds, and [`Self::resolve`] hands it over. An item that named something the /// flat API does not declare is [`Element::Unsupported`] with -/// [`ItemError::UnresolvedType`] — inert until an adapter declares it, exactly -/// like every other refusal, so an item no binding uses stays harmless. +/// [`ItemError::UnresolvedType`], exactly like every other refusal — carried +/// here, raised by [`Registry`](crate::core::Registry) ingestion. /// /// Resolving here rather than in the adapters is the point of #211: a dangling /// name used to surface much later as an unresolved-converter error, from /// whichever adapter happened to look first. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Flat { /// Source order, so iteration reports items as the sources were fed. elements: Vec, @@ -463,8 +475,10 @@ impl Flat { /// Every item the language could not express, with its diagnosis. /// - /// An adapter raises one of these when it declares the item; until then they - /// are inert. See the [module docs](self) on where acceptance is enforced. + /// Present in the model so a consumer can inspect what a source crate marked + /// — building a [`Registry`](crate::core::Registry) from a model holding any of + /// these fails, and reports all of them. See the [module docs](self) on where + /// acceptance is enforced. pub fn unsupported(&self) -> impl Iterator { self.elements.iter().filter_map(|e| match e { Element::Unsupported(u) => Some(u), @@ -472,6 +486,32 @@ impl Flat { }) } + /// Check a function signature against the source language's grammar. + /// + /// For the **one input that does not come through this module**: a binding's + /// `local_functions`, whose signatures are written by hand in a build script + /// and inserted straight into the registry. Everything else was already + /// lowered here, so this exists to keep the grammar decided in one place + /// rather than re-checked at the far end. + /// + /// Grammar only. Whether the types it names are *declared* is a whole-model + /// question ([`resolve_references`]), and a binding-local fn may legitimately + /// name types the source crate never did. + pub fn check_signature(&self, f: &syn::ItemFn) -> Result<(), ItemError> { + // Rebuilt from the model rather than kept: this runs once per local fn, + // and a stored index would be a second copy of what `constants()` says. + let consts = ConstIndex::new(self.constants().map(|c| { + ( + c.name.to_string(), + (*c.origin.syntax.expr).clone(), + c.origin.crate_name().map(str::to_owned), + ) + })); + // A synthesized fn has no captured location; the caller names it. + let at = Rc::new(SourceLocation::default()); + lower_fn(f, &at, &consts).map(|_| ()) + } + /// The declaration a reference denotes. /// /// Infallible in practice for any reference reached from a surviving element: @@ -642,17 +682,30 @@ pub struct DuplicateName { pub name: syn::Ident, pub first: SourceLocation, pub second: SourceLocation, + /// The crate each was marked in. A captured file path is crate-relative + /// (both are `src/lib.rs`), so these are the only unambiguous coordinates + /// when two sources collide. + pub first_crate: Option, + pub second_crate: Option, } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - ParseError::DuplicateName(d) => write!( - f, - "duplicate `#[prebindgen]` name `{}`: first at {}, again at {} — marked items \ - share one flat namespace across all source crates", - d.name, d.first, d.second - ), + ParseError::DuplicateName(d) => { + let at = |loc: &SourceLocation, krate: &Option| match krate { + Some(k) => format!("{loc} (crate `{k}`)"), + None => loc.to_string(), + }; + write!( + f, + "duplicate `#[prebindgen]` name `{}`: first at {}, again at {} — marked items \ + share one flat namespace across all source crates", + d.name, + at(&d.first, &d.first_crate), + at(&d.second, &d.second_crate) + ) + } } } } diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index b40772be..bac95bbf 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -571,6 +571,37 @@ fn extents_outside_the_subgrammar() { extent_reason(quote::quote!([u8; UNMARKED])), ArrayLenReason::NotAMarkedConst ); + // Expression forms that BIND a local, which is the dangerous family: a length + // is qualified against its source module, so a local shadowing a marked item + // would be rewritten into it. Scope tracking is the general answer; none of + // these has a place in a boundary type, so the whole family is refused. + // (Moved here from the jnigen suite: the subgrammar is the frontend's.) + assert_eq!( + extent_reason(quote::quote!( + [u8; const { + let n = 3; + n + }] + )), + ArrayLenReason::NotLiteralOrName + ); + assert_eq!( + extent_reason(quote::quote!( + [u8; match 3 { + n => n, + }] + )), + ArrayLenReason::NotLiteralOrName + ); + assert_eq!( + extent_reason(quote::quote!([u8; if let n = 3 { n } else { 0 }])), + ArrayLenReason::NotLiteralOrName + ); + // A CALL is not a name either, however const the callee. + assert_eq!( + extent_reason(quote::quote!([u8; array_len()])), + ArrayLenReason::NotLiteralOrName + ); assert_eq!( extent_reason(quote::quote!([u8; 'c'])), ArrayLenReason::NotAnIntegerLiteral diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs index d80f87d2..712e1ee0 100644 --- a/prebindgen/src/api/core/registry.rs +++ b/prebindgen/src/api/core/registry.rs @@ -233,6 +233,10 @@ impl Direction { /// [`crate::api::core::prebindgen::ConverterImpl`] that produced it. /// Adapters that don't carry extras leave `M = ()`. pub struct Registry { + /// The parsed model these maps project. Held rather than discarded, so a + /// later stage can ask it what a name means through the registry it already + /// has — see [`Self::flat`]. + flat: crate::api::core::flat::Flat, pub functions: HashMap, pub structs: HashMap, pub enums: HashMap, @@ -312,9 +316,17 @@ pub struct Registry { HashMap, } -impl Default for Registry { - fn default() -> Self { +impl Registry { + /// An empty registry: no model, no items, no types. + /// + /// **Not public.** A `Registry` is a projection of a [`Flat`], and one built + /// this way projects nothing — [`Self::flat`] would hand a later stage an + /// empty model that claims to be this registry's source. Outside this crate + /// the entry points are [`Self::from_items`], [`Self::from_flat`] and + /// [`Self::builder`], each of which has a model behind it. + pub(crate) fn empty() -> Self { Self { + flat: crate::api::core::flat::Flat::default(), functions: HashMap::new(), structs: HashMap::new(), enums: HashMap::new(), @@ -336,6 +348,33 @@ impl Default for Registry { } } +impl From for ScanError { + fn from(e: crate::api::core::flat::ParseError) -> Self { + match e { + crate::api::core::flat::ParseError::DuplicateName(d) => { + ScanError::DuplicateName(Box::new(DuplicateNameError { + name: d.name, + first: d.first, + second: d.second, + first_crate: d.first_crate, + second_crate: d.second_crate, + })) + } + } + } +} + +/// One item of a [`ScanError::NotExpressible`] report. +#[derive(Debug)] +pub struct NotExpressibleEntry { + /// The item's name, or `None` for an item kind that has none. + pub name: Option, + /// Rendered [`ItemError`](crate::core::flat::ItemError) — the frontend's own + /// message, so one authority produces it. + pub reason: String, + pub location: SourceLocation, +} + /// Payload of [`ScanError::DuplicateName`], boxed to keep the error enum /// small (`clippy::result_large_err`). #[derive(Debug)] @@ -361,15 +400,15 @@ pub enum ScanError { ConflictingTypeIntent { key: TypeKey, }, - DisallowedImplTrait { - ty: String, - loc: SourceLocation, - }, - UnsupportedReceiver { - loc: SourceLocation, - }, - UnsupportedParamPattern { - loc: SourceLocation, + /// Items the flat language cannot express, all of them at once. + /// + /// The message for each comes from + /// [`ItemError`](crate::core::flat::ItemError), so one authority produces it. + /// This replaces the per-item guards the registry used to duplicate — a `self` + /// receiver, a non-ident parameter pattern, a disallowed `impl Trait` — which + /// the frontend now catches with a richer diagnosis (it names the parameter). + NotExpressible { + entries: Vec, }, /// An adapter-invariant check failed — see [`Prebindgen::validate`]. /// The message is adapter-authored and printed verbatim. @@ -416,26 +455,35 @@ impl fmt::Display for ScanError { e.second ) } - ScanError::ConflictingFunctionIntent { name } => write!( - f, - "function `{}` cannot be both declared and ignored", - name - ), - ScanError::ConflictingTypeIntent { key } => write!( - f, - "type `{}` cannot be both declared and ignored", - key - ), - ScanError::DisallowedImplTrait { ty, loc } => write!( - f, - "`impl Trait` is not allowed at {}: `{}` (only `impl Fn(...) + Send + Sync + 'static` is supported)", - loc, ty - ), - ScanError::UnsupportedReceiver { loc } => { - write!(f, "method receiver (`self`) parameters are not supported at {}", loc) + ScanError::ConflictingFunctionIntent { name } => { + write!(f, "function `{}` cannot be both declared and ignored", name) } - ScanError::UnsupportedParamPattern { loc } => { - write!(f, "non-ident parameter pattern is not supported at {}", loc) + ScanError::ConflictingTypeIntent { key } => { + write!(f, "type `{}` cannot be both declared and ignored", key) + } + ScanError::NotExpressible { entries } => { + write!( + f, + "{} `#[prebindgen]` item(s) the flat language cannot express:", + entries.len() + )?; + for e in entries { + // The crate, because a captured path is crate-relative: with + // several sources, two offenders both read `src/lib.rs:..` + // and the location alone says nothing about which one to fix. + // Same reason the duplicate-name diagnostic carries it. + let in_crate = match &e.location.crate_name { + Some(c) => format!(" in crate `{c}`"), + None => String::new(), + }; + match &e.name { + Some(name) => { + write!(f, "\n {}{in_crate}: {name} {}", e.location, e.reason)? + } + None => write!(f, "\n {}{in_crate}: {}", e.location, e.reason)?, + } + } + Ok(()) } ScanError::AdapterInvariant { message } => write!(f, "{}", message), ScanError::DeclaredNotFound { entries } => { @@ -730,52 +778,118 @@ impl Registry { where I: IntoIterator, { - let mut registry = Registry::default(); - // Pass 1: collect and gather EVERY source module name first, so - // cross-source type references (`source_a::TypeA` in a later-chained - // source's signature) normalize order-independently in pass 2 — and so do - // alias-named paths, whose declaration may arrive later or in another - // source. - let items: Vec<(syn::Item, SourceLocation)> = items.into_iter().collect(); - let normalization = crate::api::core::types_util::Normalization::from_items(&items); - registry - .source_modules - .clone_from(&normalization.source_modules); - // Pass 2: normalize each item's types to the canonical flat spelling - // (`crate::`/source-module paths reduce to the bare indexed name, and an - // aliased path to the name its alias gives it — see `normalize_type`'s - // rule list), then index. Every downstream `TypeKey::from_type` over a - // signature type therefore sees the normalized form, so bare adapter - // declarations match qualified captured spellings (issue #95). - for (mut item, loc) in items { - crate::api::core::types_util::normalize_item_types(&mut item, &normalization); - let crate_name = loc.crate_name.clone(); - let named: Option = match &item { - syn::Item::Fn(f) => Some(f.sig.ident.clone()), - syn::Item::Struct(s) => Some(s.ident.clone()), - syn::Item::Enum(e) => Some(e.ident.clone()), - syn::Item::Const(c) if c.ident != "_" => Some(c.ident.clone()), - _ => None, - }; - match registry.index_item(item, loc) { - Ok(()) => { - // Only after successful indexing — a collision must keep - // the FIRST item's origin for the error below. - if let (Some(ident), Some(crate_name)) = (named, crate_name) { - registry.item_origins.insert(ident, crate_name); - } + let flat = crate::api::core::flat::Flat::builder() + .items(items) + .build()?; + Self::from_flat(flat) + } + + /// Index a parsed [`Flat`] model. + /// + /// The registry is a **projection** of the model, not a second reading of the + /// source: `Flat` decided what every item means, and this arranges those + /// decisions into the maps adapters read. The model itself is kept + /// ([`Self::flat`]) so later stages can ask it questions rather than + /// re-deriving them. + /// + /// **Fails on anything the language cannot express** — a `self` receiver, an + /// `async fn`, a generic binder, a type form outside the grammar, or a + /// reference to a type the flat API does not declare. All of them at once, so + /// a source crate that needs migrating sees one list instead of one rebuild + /// per item. + pub fn from_flat(flat: crate::api::core::flat::Flat) -> Result { + use crate::api::core::flat::{Element, Type}; + + let entries: Vec = flat + .unsupported() + .map(|u| NotExpressibleEntry { + name: u.name.clone(), + reason: u.error.to_string(), + location: (*u.origin.location).clone(), + }) + .collect(); + if !entries.is_empty() { + return Err(ScanError::NotExpressible { entries }); + } + + let mut registry = Registry::empty(); + // First-seen order, which is what makes the first entry the default + // module. Derived from the elements rather than stored twice. + for element in flat.elements() { + if let Some(crate_name) = element.location().crate_name.as_ref() { + let module = crate_name.replace('-', "_"); + if !registry.source_modules.contains(&module) { + registry.source_modules.push(module); + } + } + } + + for element in flat.elements() { + let crate_name = element.location().crate_name.clone(); + let named = element.name().cloned(); + match element { + Element::Function(f) => { + registry.functions.insert( + f.name.clone(), + (f.origin.syntax.clone(), element.location().clone()), + ); } - Err(ScanError::DuplicateName(mut e)) => { - e.first_crate = registry.item_origins.get(&e.name).cloned(); - e.second_crate = crate_name; - return Err(ScanError::DuplicateName(e)); + Element::Type(Type::Struct(t)) => { + registry.structs.insert( + t.name.clone(), + (t.origin.syntax.clone(), element.location().clone()), + ); } - Err(e) => return Err(e), + Element::Type(Type::Variant(t)) => { + registry.enums.insert( + t.name.clone(), + (t.origin.syntax.clone(), element.location().clone()), + ); + } + Element::Type(Type::Enum(t)) => { + registry.enums.insert( + t.name.clone(), + (t.origin.syntax.clone(), element.location().clone()), + ); + } + // An unnamed `const _` is each source's injected `konst` feature + // guard: not addressable, re-emitted verbatim. That is the whole + // of `passthrough` now — the proc-macro refuses to mark a `use`, + // `mod` or `macro_rules!`, so nothing else ever reached it. + Element::Constant(c) if named.is_none() => { + registry.passthrough.push(( + syn::Item::Const(c.origin.syntax.clone()), + element.location().clone(), + )); + } + Element::Constant(c) => { + registry.consts.insert( + c.name.clone(), + (c.origin.syntax.clone(), element.location().clone()), + ); + } + // An `Extern` states that a name exists and its contents do not + // cross. There is no map for that, and adapters have never seen + // one — a type alias was already a no-op here. Reachable through + // [`Self::flat`] for the stages that will want it. + Element::Type(Type::Extern(_)) => {} + // Refused above. + Element::Unsupported(_) => unreachable!("checked before indexing"), + } + if let (Some(ident), Some(crate_name)) = (named, crate_name) { + registry.item_origins.insert(ident, crate_name); } } + + registry.flat = flat; Ok(registry) } + /// The parsed model this registry projects. + pub fn flat(&self) -> &crate::api::core::flat::Flat { + &self.flat + } + /// The origin crate's **module path** for an item ingested via /// the item's [`SourceLocation`] stamp, or `None` when unknown — /// callers then fall @@ -1173,78 +1287,6 @@ impl Registry { self.required_inputs_scan.remove(&TypeKey::from_type(ty)); } - fn index_item(&mut self, item: syn::Item, loc: SourceLocation) -> Result<(), ScanError> { - match item { - syn::Item::Fn(f) => { - self.check_no_duplicate(&f.sig.ident, &loc)?; - self.functions.insert(f.sig.ident.clone(), (f, loc)); - Ok(()) - } - syn::Item::Struct(s) => { - self.check_no_duplicate(&s.ident, &loc)?; - self.structs.insert(s.ident.clone(), (s, loc)); - Ok(()) - } - syn::Item::Enum(e) => { - self.check_no_duplicate(&e.ident, &loc)?; - self.enums.insert(e.ident.clone(), (e, loc)); - Ok(()) - } - syn::Item::Const(c) => { - // Unnamed `const _` items (each source's injected `konst` - // feature guard) live outside the flat namespace: several - // sources may each carry one, all passed through verbatim. - if c.ident == "_" { - self.passthrough.push((syn::Item::Const(c), loc)); - return Ok(()); - } - self.check_no_duplicate(&c.ident, &loc)?; - self.consts.insert(c.ident.clone(), (c, loc)); - Ok(()) - } - // `#[prebindgen] pub type X = ..` DECLARES an opaque type: it states - // something about the flat API's surface, and is not code to copy - // into the binding. Its target is routinely crate-private, so - // re-emitting it would not even compile. - syn::Item::Type(_) => Ok(()), - other => { - self.passthrough.push((other, loc)); - Ok(()) - } - } - } - - fn check_no_duplicate(&self, name: &syn::Ident, loc: &SourceLocation) -> Result<(), ScanError> { - if let Some(first) = self.first_seen_loc(name) { - // Origin crates are unknown at this level; `from_items` enriches - // the error with them (the locations alone are crate-relative). - return Err(ScanError::DuplicateName(Box::new(DuplicateNameError { - name: name.clone(), - first, - second: loc.clone(), - first_crate: None, - second_crate: None, - }))); - } - Ok(()) - } - - fn first_seen_loc(&self, name: &syn::Ident) -> Option { - if let Some((_, loc)) = self.functions.get(name) { - return Some(loc.clone()); - } - if let Some((_, loc)) = self.structs.get(name) { - return Some(loc.clone()); - } - if let Some((_, loc)) = self.enums.get(name) { - return Some(loc.clone()); - } - if let Some((_, loc)) = self.consts.get(name) { - return Some(loc.clone()); - } - None - } - fn scan_fn_signature( &mut self, f: &syn::ItemFn, @@ -1256,15 +1298,14 @@ impl Registry { // wrappers; propagation through `subs` then marks transitive deps // (e.g. &Foo's `&_` converter returns subs=[Foo], so Foo becomes // required). + // No receiver or non-ident pattern can reach here: a captured item was + // refused by the frontend and `from_flat` failed before indexing it, and + // a binding-local fn was checked against the same grammar + // (`Flat::check_signature`) when `resolve` synthesized it. for input in &f.sig.inputs { match input { - syn::FnArg::Receiver(_) => { - return Err(ScanError::UnsupportedReceiver { loc: loc.clone() }); - } + syn::FnArg::Receiver(_) => continue, syn::FnArg::Typed(pt) => { - if !matches!(&*pt.pat, syn::Pat::Ident(_)) { - return Err(ScanError::UnsupportedParamPattern { loc: loc.clone() }); - } self.register_type_recursive(Direction::Input, &pt.ty, true, loc)?; } } @@ -1328,15 +1369,10 @@ impl Registry { loc: &SourceLocation, visited: &mut HashSet, ) -> Result<(), ScanError> { - // Reject `impl Trait` except `impl Fn(...) + Send + Sync + 'static`. - if let syn::Type::ImplTrait(it) = ty { - if extract_fn_trait_args(ty).is_none() { - return Err(ScanError::DisallowedImplTrait { - ty: it.to_token_stream().to_string(), - loc: loc.clone(), - }); - } - } + // A disallowed `impl Trait` cannot reach here: every fn whose signature + // reaches this point passed the frontend's grammar — captured items at + // ingestion, binding-local ones at synthesis — and it names the + // parameter the bad type sits on. let key = TypeKey::from_type(ty); if !visited.insert(key.clone()) { @@ -1437,6 +1473,18 @@ impl Registry { // stage treats them exactly like `#[prebindgen]` fns. for (item_fn, origin) in adapter.local_functions() { let ident = item_fn.sig.ident.clone(); + // The one input that does not come through `Flat`: a `sig!(..)` is + // written by hand in a build script and inserted straight into the + // maps, so the grammar has to be checked here or nowhere. Silently + // dropping a `self` receiver or a pattern parameter would surface as + // an arity mismatch out of rustc on generated code, which is the + // wrong end of the pipeline to learn about a build.rs typo. + if let Err(error) = self.flat.check_signature(&item_fn) { + return Err(ScanError::AdapterInvariant { + message: format!("binding-local fn `{ident}`: {error}"), + } + .into()); + } if self.functions.contains_key(&ident) { return Err(ScanError::AdapterInvariant { message: format!( diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 8ab8f142..1110b79d 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -20,6 +20,7 @@ struct StubExt { consts: Option>, types: HashSet, ignored_types: HashSet, + local_fns: Vec<(syn::ItemFn, String)>, } impl Prebindgen for StubExt { @@ -46,6 +47,9 @@ impl Prebindgen for StubExt { fn ignored_types(&self) -> HashSet { self.ignored_types.clone() } + fn local_functions(&self) -> Vec<(syn::ItemFn, String)> { + self.local_fns.clone() + } fn on_function(&self, _f: &syn::ItemFn, _registry: &Registry<()>) -> TokenStream { TokenStream::new() @@ -83,22 +87,6 @@ fn fn_item(src: &str) -> (syn::Item, SourceLocation) { (syn::Item::Fn(item), SourceLocation::default()) } -#[test] -fn from_items_does_not_scan_signatures() { - // A `#[prebindgen]`-marked fn whose return is a bare `impl Foo` - // would have failed `from_items` under the old code path - // (ScanError::DisallowedImplTrait). Now `from_items` is index- - // only and accepts it without complaint. - let items = vec![fn_item("fn bogus(x: u64) -> impl std::fmt::Debug { 0u64 }")]; - let reg: Registry<()> = Registry::from_items(items).expect("from_items must succeed"); - assert!(reg.required_inputs_scan.is_empty()); - assert!(reg.required_outputs_scan.is_empty()); - // The fn is indexed but no types are pre-required. - assert!(reg - .functions - .contains_key(&syn::parse_str("bogus").unwrap())); -} - #[test] fn scan_declared_empty_ext_marks_nothing_required() { let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; @@ -133,24 +121,6 @@ fn scan_declared_marks_types_required_only_for_declared_fns() { .contains(&TypeKey::parse("u32").expect("test type"))); } -#[test] -fn scan_declared_fails_disallowed_impl_trait_only_when_fn_declared() { - let items = vec![fn_item("fn bogus(x: u64) -> impl std::fmt::Debug { 0u64 }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); - - // Empty ext: the bogus fn is not scanned, so no error. - let empty = StubExt::default(); - assert!(reg.scan_declared(&empty).is_ok()); - - // Declare the fn: scan now fires the disallowed-impl-Trait error. - let mut ext = StubExt::default(); - ext.functions.insert(syn::parse_str("bogus").unwrap()); - match reg.scan_declared(&ext) { - Err(ScanError::DisallowedImplTrait { .. }) => (), - other => panic!("expected DisallowedImplTrait, got {:?}", other), - } -} - #[test] fn scan_declared_rejects_function_declared_and_ignored_overlap() { let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; @@ -343,6 +313,33 @@ fn type_entry_helpers_expose_converter_chain_contract() { /// `SourceLocation` file paths are crate-relative (both may read /// `src/lib.rs`), so the crates (stamped into each stream item's location /// by `Source`) are the only unambiguous coordinates. +/// Ingestion checks that the flat API is expressible, and reports **every** +/// offender at once — a source crate that needs migrating should see one list, +/// not one rebuild per item. +/// +/// This replaces two tests that asserted the opposite (that `from_items` was +/// index-only and diagnosed at declaration time). The frontend owns that +/// judgement now, and its diagnosis is richer: it names the parameter. +#[test] +fn from_items_rejects_what_the_language_cannot_express() { + let err = match Registry::<()>::from_items(vec![ + fn_item("fn bogus(x: u64) -> impl std::fmt::Debug { 0u64 }"), + fn_item("fn worse(self) -> u64 { 0 }"), + ]) { + Ok(_) => panic!("neither item is expressible"), + Err(e) => e, + }; + + let ScanError::NotExpressible { entries } = &err else { + panic!("expected a NotExpressible report, got {err}"); + }; + assert_eq!(entries.len(), 2, "all offenders at once"); + + let msg = err.to_string(); + assert!(msg.contains("bogus") && msg.contains("impl Trait"), "{msg}"); + assert!(msg.contains("worse") && msg.contains("self"), "{msg}"); +} + #[test] fn duplicate_name_across_sources_names_both_crates() { use crate::{ @@ -767,3 +764,222 @@ fn builder_and_from_items_agree() { ); assert_eq!(built.passthrough.len(), streamed.passthrough.len()); } + +// ── The projection itself ────────────────────────────────────────────── + +/// Every element kind lands where the projection says, the model is **kept**, +/// and a name's origin crate survives the trip. +/// +/// The seam's only direct test: everything else reaches `from_flat` through +/// `from_items`, which cannot distinguish "the projection is right" from "the +/// parser and the projection are wrong in matching ways". +#[test] +fn from_flat_projects_each_element_kind() { + let at = |krate: &str| SourceLocation { + file: "src/lib.rs".into(), + crate_name: Some(krate.to_string()), + ..SourceLocation::default() + }; + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::parse_quote!( + pub fn f(v: u64) -> u64 { + v + } + ), + at("myflat"), + ), + ( + syn::parse_quote!( + pub struct S { + pub a: u64, + } + ), + at("myflat"), + ), + // A sum and a C-style enum are different elements, one map. + ( + syn::parse_quote!( + pub enum Sum { + A(u64), + B, + } + ), + at("myflat"), + ), + ( + syn::parse_quote!( + pub enum Flags { + X = 1, + Y = 2, + } + ), + at("myflat"), + ), + ( + syn::parse_quote!( + pub const K: u64 = 7; + ), + at("myflat"), + ), + // Each source's injected feature guard: no address, so several coexist. + ( + syn::parse_quote!( + const _: () = (); + ), + at("myflat"), + ), + ( + syn::parse_quote!( + const _: () = (); + ), + at("helpers"), + ), + // An alias declared by a SECONDARY source. It lands in no map — an + // `Extern` states a name exists, which the registry has never had a + // place for — but it DOES record an origin, so a reference to it + // qualifies against the crate that declared it rather than falling back + // to the default module. + ( + syn::parse_quote!( + pub type Handle = helpers::Inner; + ), + at("helpers"), + ), + ]; + let flat = crate::api::core::flat::Flat::builder() + .items(items) + .build() + .expect("parse"); + let reg: Registry<()> = Registry::from_flat(flat).expect("project"); + + let id = |n: &str| syn::parse_str::(n).unwrap(); + assert!(reg.functions.contains_key(&id("f"))); + assert!(reg.structs.contains_key(&id("S"))); + assert!(reg.enums.contains_key(&id("Sum")), "a sum is an enum here"); + assert!(reg.enums.contains_key(&id("Flags"))); + assert!(reg.consts.contains_key(&id("K"))); + assert_eq!(reg.passthrough.len(), 2, "one guard per source"); + assert!( + !reg.structs.contains_key(&id("Handle")) && !reg.enums.contains_key(&id("Handle")), + "an Extern names a type; it declares no body to index" + ); + + // The model is held, not discarded — this is what makes the registry a + // projection rather than a second reading. + assert!(reg.flat().element("f").is_some()); + assert!( + reg.flat().declared_type("Handle").is_some(), + "the alias is reachable through the model even though no map holds it" + ); + + // Origins, including the alias's — a behaviour change from the old + // `syn::Item::Type` no-op, which recorded none. + assert_eq!(reg.origin_module(&id("f")), Some(syn::parse_quote!(myflat))); + assert_eq!( + reg.origin_module(&id("Handle")), + Some(syn::parse_quote!(helpers)), + "an alias declared by a helper crate qualifies against that crate" + ); + // First-seen source order, which is what makes the first entry the default. + assert_eq!(reg.default_module(), Some(syn::parse_quote!(myflat))); +} + +/// The inexpressible report names the **crate**, not just the location. +/// +/// A captured path is crate-relative, so two offenders from different sources +/// both read `src/lib.rs:0:0` and the location alone cannot say which crate to +/// fix. Same reason the duplicate-name diagnostic carries it. +#[test] +fn not_expressible_report_names_the_crate_of_each_offender() { + let at = |krate: &str| SourceLocation { + file: "src/lib.rs".into(), + crate_name: Some(krate.to_string()), + ..SourceLocation::default() + }; + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::parse_quote!( + pub async fn a() {} + ), + at("myflat"), + ), + ( + syn::parse_quote!( + pub fn b(v: T) -> T { + v + } + ), + at("helpers"), + ), + ]; + let Err(err) = Registry::<()>::from_items(items) else { + panic!("both items are inexpressible") + }; + let msg = err.to_string(); + + assert!(msg.contains("2 `#[prebindgen]` item(s)"), "{msg}"); + assert!(msg.contains("in crate `myflat`"), "{msg}"); + assert!(msg.contains("in crate `helpers`"), "{msg}"); + // Both share a file path, so the crate is the only thing telling them apart. + assert_eq!(msg.matches("src/lib.rs").count(), 2, "{msg}"); + // No trailing newline: the message is embedded in `expect`/`panic` output. + assert!(!msg.ends_with('\n'), "{msg:?}"); +} + +/// A binding-local fn is checked against the **same grammar** as a captured one. +/// +/// `sig!(..)` is written by hand in a build script and inserted straight into the +/// registry, so it is the one input that never passes through `Flat`. Without a +/// check here a `self` receiver or a pattern parameter is silently dropped and +/// the user meets it as an arity mismatch out of rustc on generated code — the +/// wrong end of the pipeline to learn about a build.rs typo. +#[test] +fn a_binding_local_fn_is_checked_against_the_grammar() { + for (src, expected) in [ + ("fn takes_self(&self, x: u32) -> u32 { x }", "receiver"), + ( + "fn takes_pattern((a, b): (u32, u32)) -> u32 { a }", + "pattern", + ), + ("fn takes_impl(x: impl std::fmt::Debug) {}", "impl Trait"), + ("async fn is_async() {}", "async"), + ] { + let reg: Registry<()> = + Registry::from_items(vec![fn_item("fn good(x: u64) -> u64 { x }")]).unwrap(); + let ext = StubExt { + local_fns: vec![(syn::parse_str(src).expect("parse local fn"), "b".into())], + ..Default::default() + }; + let err = reg + .resolve(ext) + .expect_err(&format!("`{src}` must be refused")); + let msg = err.to_string(); + assert!( + msg.contains("binding-local fn"), + "must say which input is at fault: {msg}" + ); + assert!( + msg.to_lowercase().contains(&expected.to_lowercase()), + "`{src}` should be diagnosed as {expected}, got: {msg}" + ); + } +} + +/// The same check accepts what the grammar allows, so it is a grammar check and +/// not a blanket refusal — and it does **not** demand that a local fn's types be +/// declared, which a binding-local fn legitimately may not be. +#[test] +fn a_well_formed_binding_local_fn_passes() { + let reg: Registry<()> = + Registry::from_items(vec![fn_item("fn good(x: u64) -> u64 { x }")]).unwrap(); + let ext = StubExt { + local_fns: vec![( + syn::parse_str("fn helper(s: &Undeclared) -> u64 { 0 }").expect("parse"), + "b".into(), + )], + ..Default::default() + }; + reg.resolve(ext) + .expect("a grammatical local fn passes, undeclared types and all"); +} diff --git a/prebindgen/src/api/core/resolve/tests.rs b/prebindgen/src/api/core/resolve/tests.rs index a6483d8a..38efd77f 100644 --- a/prebindgen/src/api/core/resolve/tests.rs +++ b/prebindgen/src/api/core/resolve/tests.rs @@ -9,7 +9,7 @@ use super::*; fn final_invariant_reports_unresolved_field_of_unresolved_struct() { use crate::api::core::registry::{Registry, TypeKey}; - let mut reg: Registry<()> = Registry::default(); + let mut reg: Registry<()> = Registry::empty(); // Index a struct `Outer { inner: ZKeyExpr }` so the BFS can walk // into its field. `ZKeyExpr` itself stays *unindexed* (the user's @@ -60,7 +60,7 @@ fn final_invariant_stops_at_resolved_nodes() { SourceLocation as Loc, }; - let mut reg: Registry<()> = Registry::default(); + let mut reg: Registry<()> = Registry::empty(); let outer_struct: syn::ItemStruct = syn::parse_str("struct Outer { inner: Inner }").unwrap(); let inner_struct: syn::ItemStruct = diff --git a/prebindgen/src/api/core/write/tests.rs b/prebindgen/src/api/core/write/tests.rs index 9c63e39f..b1353085 100644 --- a/prebindgen/src/api/core/write/tests.rs +++ b/prebindgen/src/api/core/write/tests.rs @@ -58,7 +58,7 @@ impl Prebindgen for IdentityExt { #[test] fn dedup_and_sort() { - let mut reg: Registry<()> = Registry::default(); + let mut reg: Registry<()> = Registry::empty(); let key_a = TypeKey::parse("u64").expect("test type"); let key_b = TypeKey::parse("Sample").expect("test type"); let wire: syn::Type = syn::parse_quote!(i64); @@ -107,7 +107,7 @@ fn dedup_and_sort() { #[test] fn write_rust_sorts_declared_items_by_ident() { - let mut reg: Registry<()> = Registry::default(); + let mut reg: Registry<()> = Registry::empty(); let loc = SourceLocation::default(); reg.functions.insert( diff --git a/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs b/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs index 853a2b13..f11ff2ea 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs @@ -38,7 +38,7 @@ fn build(fns: &[&str]) -> String { idents.push(f.sig.ident.clone()); items.push((syn::Item::Fn(f), loc.clone())); } - let registry = Registry::<()>::from_items(items).expect("index items"); + let registry = Registry::<()>::from_items(declare_referenced(items)).expect("index items"); let mut cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(myflat)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs b/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs index b21974b6..95aa3557 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs @@ -141,8 +141,10 @@ fn every_input_category() -> String { ), ]; - let registry = Registry::<()>::from_items(items.into_iter().map(|i| (i, loc.clone()))) - .expect("index items"); + let registry = Registry::<()>::from_items(declare_referenced( + items.into_iter().map(|i| (i, loc.clone())), + )) + .expect("index items"); let mut cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(example_flat)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/builder.rs b/prebindgen/src/api/lang/cbindgen/tests/builder.rs index 119407be..37e37764 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/builder.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/builder.rs @@ -10,8 +10,8 @@ fn function_name_renames_symbol() { unimplemented!() } ); - let reg = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + let reg = Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cb = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(rust_init)) @@ -81,10 +81,10 @@ fn free_memory_function_required() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); // String output (and an Error with a String field) but no free fn declared. @@ -122,10 +122,10 @@ fn manglers_generate_all_names() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -211,8 +211,8 @@ fn qualified_signature_spelling_matches_bare_opaque_ptr() { unimplemented!() } ); - let reg = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + let reg = Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cb = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_ptr(syn::parse_quote!(ZKeyExpr)) @@ -244,10 +244,10 @@ fn enum_mirror_preserves_the_source_discriminant_domain() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(f), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() diff --git a/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs b/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs index a6d7a3e5..d35b8b42 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs @@ -17,10 +17,10 @@ fn takeable_callback_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(func), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -77,10 +77,10 @@ fn callback_subscriber_emits_closure_structs() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -173,10 +173,10 @@ fn callback_scalar_arg_not_module_qualified() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -218,10 +218,10 @@ fn callback_struct_name_defaults_generically() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() diff --git a/prebindgen/src/api/lang/cbindgen/tests/errors.rs b/prebindgen/src/api/lang/cbindgen/tests/errors.rs index 36c025ed..3df6f4cc 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/errors.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/errors.rs @@ -10,10 +10,10 @@ fn result_error_not_declared_is_build_error() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); // Error declared as data_struct but NOT marked `.error()`. @@ -49,8 +49,11 @@ fn fallible_input_without_result_needs_panic() { ); // No `.panic()` → build error. - let reg1 = Registry::<()>::from_items([(syn::Item::Fn(func.clone()), loc.clone())]) - .expect("index items"); + let reg1 = Registry::<()>::from_items(declare_referenced([( + syn::Item::Fn(func.clone()), + loc.clone(), + )])) + .expect("index items"); let cb1 = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(z_log)); @@ -62,8 +65,8 @@ fn fallible_input_without_result_needs_panic() { assert!(err.is_err(), "expected a build error without .panic()"); // With `.panic()` → wrapper aborts on decode failure. - let reg2 = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + let reg2 = Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cb2 = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(z_log)) @@ -95,11 +98,11 @@ fn error_out_param_is_null_guarded() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(ptr_fn), loc.clone()), (syn::Item::Fn(unit_fn), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() diff --git a/prebindgen/src/api/lang/cbindgen/tests/inputs.rs b/prebindgen/src/api/lang/cbindgen/tests/inputs.rs index f75fca84..02c6a6bb 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/inputs.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/inputs.rs @@ -11,7 +11,8 @@ fn slice_u8_input_two_params() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -46,10 +47,10 @@ fn option_opaque_input_reuses_pointer() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -91,7 +92,8 @@ fn option_scalar_input_boxed_pointer() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -122,7 +124,8 @@ fn str_borrow_input_lowering() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -161,10 +164,10 @@ fn relation_to_lowering() { } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Enum(enum_item), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -221,11 +224,11 @@ fn enum_input_validates_the_discriminant() { } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Enum(enum_item), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -290,10 +293,10 @@ fn enum_input_without_error_channel_requires_panic() { ); let build = |allow_panic: bool| { let loc = SourceLocation::default(); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func.clone()), loc.clone()), (syn::Item::Enum(enum_item.clone()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -331,10 +334,10 @@ fn mutable_opaque_borrow_input_lowering() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() diff --git a/prebindgen/src/api/lang/cbindgen/tests/lowering.rs b/prebindgen/src/api/lang/cbindgen/tests/lowering.rs index 566b09f9..a4dd7fa9 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/lowering.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/lowering.rs @@ -4,22 +4,24 @@ use super::*; fn bounded_duration_option_is_one_scalar_with_named_niche() { let loc = SourceLocation::default(); let items: Vec<(syn::Item, SourceLocation)> = [ - "pub fn duration_from_millis(v: u64) -> std::time::Duration { unimplemented!() }", - "pub fn duration_to_millis(v: &std::time::Duration) -> u64 { unimplemented!() }", - "pub fn duration_echo(v: Option) -> Option { unimplemented!() }", - "pub fn duration_nested_echo(v: Option>) -> Option> { unimplemented!() }", + "#[prebindgen] pub type Duration = std::time::Duration;", + "pub fn duration_from_millis(v: u64) -> Duration { unimplemented!() }", + "pub fn duration_to_millis(v: &Duration) -> u64 { unimplemented!() }", + "pub fn duration_echo(v: Option) -> Option { unimplemented!() }", + "pub fn duration_nested_echo(v: Option>) -> Option> { unimplemented!() }", ] .into_iter() .map(|source| { - let function: syn::ItemFn = syn::parse_str(source).unwrap(); - (syn::Item::Fn(function), loc.clone()) + // `syn::Item`, not `ItemFn`: a fixture declares the types it names. + let item: syn::Item = syn::parse_str(source).unwrap(); + (item, loc.clone()) }) .collect(); - let registry = Registry::<()>::from_items(items).unwrap(); + let registry = Registry::<()>::from_items(declare_referenced(items)).unwrap(); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(myflat)) .convert( - crate::convert!(std::time::Duration) + crate::convert!(Duration) .input(crate::fun!(duration_from_millis)) .output(crate::fun!(duration_to_millis)) .valid_range(0u64..=1_000_000u64), @@ -68,11 +70,12 @@ fn bounded_float_option_uses_a_finite_bit_exact_niche() { ] .into_iter() .map(|source| { - let function: syn::ItemFn = syn::parse_str(source).unwrap(); - (syn::Item::Fn(function), loc.clone()) + // `syn::Item`, not `ItemFn`: a fixture declares the types it names. + let item: syn::Item = syn::parse_str(source).unwrap(); + (item, loc.clone()) }) .collect(); - let registry = Registry::<()>::from_items(items).unwrap(); + let registry = Registry::<()>::from_items(declare_referenced(items)).unwrap(); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(myflat)) .convert( @@ -113,11 +116,12 @@ fn custom_conversion_without_domain_stays_infallible() { ] .into_iter() .map(|source| { - let function: syn::ItemFn = syn::parse_str(source).unwrap(); - (syn::Item::Fn(function), loc.clone()) + // `syn::Item`, not `ItemFn`: a fixture declares the types it names. + let item: syn::Item = syn::parse_str(source).unwrap(); + (item, loc.clone()) }) .collect(); - let registry = Registry::<()>::from_items(items).unwrap(); + let registry = Registry::<()>::from_items(declare_referenced(items)).unwrap(); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(myflat)) .convert( @@ -148,7 +152,7 @@ fn custom_conversion_without_domain_stays_infallible() { #[test] fn empty_adapter_writes_empty_file() { let cbindgen = Cbindgen::new(); - let registry: Registry<()> = Registry::default(); + let registry: Registry<()> = Registry::empty(); let src = write(cbindgen, registry, "empty"); assert!(src.trim().is_empty(), "expected empty output, got:\n{src}"); } @@ -165,10 +169,10 @@ fn keyexpr_try_from_lowering() { } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -238,7 +242,8 @@ fn opaque_error_lowering() { ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/mod.rs b/prebindgen/src/api/lang/cbindgen/tests/mod.rs index 08490880..ba9a675e 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/mod.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/mod.rs @@ -1,4 +1,5 @@ use super::*; +pub(crate) use crate::api::test_util::declare_referenced; use crate::{api::test_util::unique_test_dir, SourceLocation}; mod aliasing; diff --git a/prebindgen/src/api/lang/cbindgen/tests/returns.rs b/prebindgen/src/api/lang/cbindgen/tests/returns.rs index bc07535a..1245b856 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/returns.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/returns.rs @@ -10,10 +10,10 @@ fn result_unit_omits_out_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -47,10 +47,10 @@ fn result_string_uses_owned_string_wire() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -94,7 +94,8 @@ fn option_string_returns_pointer_null_for_none() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -136,10 +137,10 @@ fn result_option_uses_out_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -184,7 +185,8 @@ fn vec_string_returns_ptr_and_len() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -232,7 +234,8 @@ fn vec_u8_returns_scalar_array() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -260,7 +263,8 @@ fn cow_u8_returns_scalar_array() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -292,10 +296,10 @@ fn result_vec_uses_out_params() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -334,7 +338,8 @@ fn option_vec_uses_present_and_out() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -371,10 +376,10 @@ fn result_option_vec_full() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -412,10 +417,10 @@ fn result_pointer_returns_null_on_error() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -451,7 +456,8 @@ fn borrowed_ref_output_is_const_non_owning() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) @@ -489,7 +495,8 @@ fn borrowed_option_ref_output_nullable() { } ); let registry = - Registry::<()>::from_items([(syn::Item::Fn(func), loc.clone())]).expect("index items"); + Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) + .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(zenoh_flat)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/structs.rs b/prebindgen/src/api/lang/cbindgen/tests/structs.rs index 35df9bae..715b4fb2 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/structs.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/structs.rs @@ -22,11 +22,11 @@ fn opaque_owned_transmute_by_value() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(out_fn), loc.clone()), (syn::Item::Fn(in_fn), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -117,11 +117,11 @@ fn opaque_data_no_gravestone_writeback() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(out_fn), loc.clone()), (syn::Item::Fn(in_fn), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -196,13 +196,13 @@ fn repr_c_struct_visible_mirror_and_zero_copy_borrow() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(make_fn), loc.clone()), (syn::Item::Fn(put_fn), loc.clone()), (syn::Item::Fn(cb_fn), loc.clone()), (syn::Item::Fn(string_fn), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -284,11 +284,11 @@ fn repr_c_struct_owned_inferred_field_nulls_without_default() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(put_fn), loc.clone()), (syn::Item::Fn(string_fn), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -337,10 +337,10 @@ fn repr_c_struct_plain_data_has_no_writeback() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(take_fn), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -385,11 +385,11 @@ fn repr_c_struct_bare_box_field_keeps_full_gravestone() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(put_fn), loc.clone()), (syn::Item::Fn(string_fn), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -450,12 +450,12 @@ fn repr_c_struct_mut_ref_and_maybe_uninit_out_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(upd_fn), loc.clone()), (syn::Item::Fn(into_fn), loc.clone()), (syn::Item::Fn(string_fn), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -517,7 +517,7 @@ fn repr_c_struct_restricted_validity_field_is_rejected() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), ( syn::Item::Enum(syn::parse_quote!( @@ -529,7 +529,7 @@ fn repr_c_struct_restricted_validity_field_is_rejected() { loc.clone(), ), (syn::Item::Fn(take), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -569,10 +569,10 @@ fn repr_c_struct_restricted_validity_field_accepted_when_acknowledged() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(take), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -612,10 +612,10 @@ fn repr_c_struct_restricted_validity_field_audited_even_when_output_only() { } ); let registry = || { - Registry::<()>::from_items([ + Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st.clone()), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), - ]) + ])) .expect("index items") }; diff --git a/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs b/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs index 9367ccdc..798578d9 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs @@ -39,12 +39,12 @@ fn tagged_union_mirror_and_converters() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(make), loc.clone()), (syn::Item::Fn(take), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -133,11 +133,11 @@ fn owning_payload_gets_typed_drop() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(make), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -189,10 +189,10 @@ fn plain_data_union_has_no_drop() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(make), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -223,12 +223,12 @@ fn tagged_union_as_data_struct_field() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(f), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -292,14 +292,14 @@ fn a_union_nested_in_a_struct_payload_is_freed() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Struct(drawing), loc.clone()), (syn::Item::Enum(note), loc.clone()), (syn::Item::Fn(make), loc.clone()), (syn::Item::Fn(shape_new), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -345,10 +345,10 @@ fn plain_data_struct_decode_stays_infallible() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(f), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -381,11 +381,11 @@ fn declarators_do_not_accept_each_others_shape() { // Payload enum handed to `.enum_type()`. let payload_as_enum = || { - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(example_flat)) @@ -404,10 +404,10 @@ fn declarators_do_not_accept_each_others_shape() { } ); let unit_as_union = || { - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(unit_fn.clone()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(example_flat)) @@ -442,10 +442,10 @@ fn each_payload_rejection_names_its_own_reason() { Many(Vec), } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(example_flat)) @@ -492,10 +492,10 @@ fn unsupported_payload_is_a_generation_error() { } ); let boom = || { - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(e.clone()), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(example_flat)) @@ -533,11 +533,11 @@ fn null_opaque_payload_is_reported_not_materialised() { unimplemented!() } ); - let registry = Registry::<()>::from_items([ + let registry = Registry::<()>::from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(make), loc.clone()), (syn::Item::Fn(take), loc.clone()), - ]) + ])) .expect("index items"); let cbindgen = Cbindgen::new() @@ -633,7 +633,7 @@ fn payload_wires_come_from_the_converter_destination() { loc.clone(), ), ]; - let registry = Registry::<()>::from_items(items).expect("index items"); + let registry = Registry::<()>::from_items(declare_referenced(items)).expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(example_flat)) @@ -726,7 +726,7 @@ fn bool_payload_is_normalised_not_materialised() { loc.clone(), ), ]; - let registry = Registry::<()>::from_items(items).expect("index items"); + let registry = Registry::<()>::from_items(declare_referenced(items)).expect("index items"); let cbindgen = Cbindgen::new() .source_module(syn::parse_quote!(example_flat)) diff --git a/prebindgen/src/api/lang/jnigen/jni/overloads.rs b/prebindgen/src/api/lang/jnigen/jni/overloads.rs index 5b021eac..86128e22 100644 --- a/prebindgen/src/api/lang/jnigen/jni/overloads.rs +++ b/prebindgen/src/api/lang/jnigen/jni/overloads.rs @@ -599,11 +599,12 @@ mod tests { unimplemented!() } }; - let registry = Registry::::from_items(vec![( - syn::Item::Fn(ctor), - SourceLocation::default(), - )]) - .expect("index constructor"); + let registry = + Registry::::from_items(crate::api::test_util::declare_referenced(vec![( + syn::Item::Fn(ctor), + SourceLocation::default(), + )])) + .expect("index constructor"); let variant = crate::api::core::expand::FoldVariant { ctor: Some(syn::parse_quote!(z_summary_optional)), fallible: false, diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs b/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs index 2be16f7e..3d2b7aaa 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs @@ -41,7 +41,7 @@ fn build(fns: &[&str], tag: &str) -> String { decls = decls.fun(crate::lang::FunctionDecl::new(id)); items.push((syn::Item::Fn(f), loc.clone())); } - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(decls); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs index dbba8910..f175c6bb 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs @@ -32,7 +32,8 @@ fn callback_snapshot_pipeline() -> (String, std::collections::BTreeMap::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -217,7 +218,8 @@ fn callback_root_identity_moved_after_nested_borrow() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -312,7 +314,8 @@ fn callback_double_option_unwrap_pipeline() { )), loc.clone(), )); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -480,7 +483,8 @@ fn iface_spec_memo_shares_one_derivation() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -552,7 +556,8 @@ fn fn_plan_memo_shares_one_derivation() { )), loc.clone(), )]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_do_thing))); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/config.rs b/prebindgen/src/api/lang/jnigen/jni/tests/config.rs index 665138ed..c391b0e7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/config.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/config.rs @@ -24,7 +24,7 @@ fn ptr_class_implements_adds_interface_supertypes() { .iter() .map(|src| (syn::Item::Fn(syn::parse_str(src).unwrap()), loc.clone())) .collect(); - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") .class( @@ -74,7 +74,7 @@ fn ptr_class_interface_emits_generated_api() { .iter() .map(|src| (syn::Item::Fn(syn::parse_str(src).unwrap()), loc.clone())) .collect(); - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") .class( @@ -133,7 +133,8 @@ fn interface_name_mangle_identity_rejected() { let f: syn::ItemFn = syn::parse_str("pub fn z_thing_new() -> ZThing { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .set_interface_name_mangle(|package, n| { @@ -179,7 +180,7 @@ fn interface_name_override_and_hook() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .set_interface_name_mangle(|package, n| { @@ -241,7 +242,7 @@ fn data_class_interface_emits_generated_api() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("t").class( crate::data_class!(ZStamp) @@ -311,7 +312,8 @@ fn per_class_name_and_base_package_fun() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -387,7 +389,8 @@ fn setters_after_declarations_apply() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); // Declarations first, settings last. let jni = JniGen::new() @@ -436,7 +439,8 @@ fn generation_writes_are_order_free() { let f: syn::ItemFn = syn::parse_str("pub fn z_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_ping))); @@ -492,7 +496,8 @@ fn method_hook_can_strip_flat_class_prefix() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .set_method_name_mangle(|package, class, name| { @@ -570,7 +575,8 @@ fn method_name_mangle_hook_applies_order_independently() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -620,7 +626,8 @@ fn harness_hook_receives_derived_default() { let f: syn::ItemFn = syn::parse_str("pub fn z_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .set_harness_name_mangle(|n| { @@ -656,7 +663,8 @@ fn function_and_native_method_hooks_receive_placement() { let f: syn::ItemFn = syn::parse_str("pub fn z_session_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .set_fun_name_mangle(|package, name| { @@ -700,7 +708,8 @@ fn write_kotlin_owns_and_resets_the_root() { let f: syn::ItemFn = syn::parse_str("pub fn z_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_ping))); @@ -748,7 +757,8 @@ fn report_explains_the_resolved_surface() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -864,7 +874,8 @@ fn docs_become_kdoc_with_shape_notes() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs b/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs index 8bb6f45d..760ea660 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs @@ -29,7 +29,8 @@ fn const_items() -> Vec<(syn::Item, crate::SourceLocation)> { /// private helpers, and `JNINative` declares the matching `external fun`s. #[test] fn declared_consts_emit_getter_and_val() { - let registry = Registry::::from_items(const_items()).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(const_items())).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("cfg") @@ -108,12 +109,12 @@ fn declared_consts_emit_getter_and_val() { /// return: public `ULong`, private/native `Long`, with a bit-preserving wrap. #[test] fn unsigned_const_uses_ulong_surface() { - let registry = Registry::::from_items(vec![( + let registry = Registry::::from_items(declare_referenced(vec![( syn::Item::Const(syn::parse_quote!( pub const MAX_UNSIGNED: u64 = u64::MAX; )), myflat_loc(), - )]) + )])) .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -143,7 +144,8 @@ fn unsigned_const_uses_ulong_surface() { /// acknowledges it without emitting. #[test] fn undeclared_const_not_emitted() { - let registry = Registry::::from_items(const_items()).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(const_items())).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -184,7 +186,8 @@ fn constant_fun_source_emits_val_over_ordinary_wrapper() { )), loc.clone(), )]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -236,7 +239,8 @@ fn constant_fun_source_non_nullary_rejected() { )), loc.clone(), )]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("cfg").constant(crate::constant!(SCALED).fun(crate::fun!(scaled))), ); @@ -272,7 +276,8 @@ fn constant_fun_source_handle_return_rejected() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("things") .class(crate::ptr_class!(ZThing)) @@ -303,7 +308,8 @@ fn constant_expr_emits_getter_and_val() { )), loc.clone(), )]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("cfg").fun(crate::fun!(tag_of)).constant( @@ -374,7 +380,8 @@ fn constant_expr_handle_type_rejected() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("things") .class(crate::ptr_class!(ZThing)) @@ -421,7 +428,8 @@ fn handle_const_rejected() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("things") @@ -453,7 +461,8 @@ fn constant_with_source_calls_path_verbatim() { )), loc.clone(), )]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("cfg").fun(crate::fun!(unrelated)).constant( crate::constant!(COVER_VERSION) diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs b/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs index dab65986..7d3602f7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs @@ -261,7 +261,8 @@ fn run_pipeline( items: Vec<(syn::Item, crate::SourceLocation)>, jni: JniGen, ) -> (String, BTreeMap) { - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs b/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs index 0d73864c..5fbb3914 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs @@ -19,7 +19,8 @@ fn inline_output_gets_own_builder() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -116,7 +117,8 @@ fn error_unwrap_universal_records() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -247,7 +249,8 @@ fn method_constructor_and_inline_field_self() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") @@ -317,7 +320,8 @@ fn rust_side_only_error_type() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -385,7 +389,8 @@ fn rust_side_only_input_type() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -429,7 +434,8 @@ fn rust_side_only_variant_self_rejected() { let f: syn::ItemFn = syn::parse_str("pub fn z_run(opts: ZOpts) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .package(crate::package!("ops").fun(crate::fun!(z_run))) .expand(crate::expand_param!(ZOpts).variant_self()); @@ -449,7 +455,8 @@ fn rust_side_only_field_self_rejected() { let loc = myflat_loc(); let f: syn::ItemFn = syn::parse_str("pub fn z_make() -> ZThing { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .package(crate::package!("ops").fun(crate::fun!(z_make))) .expand(crate::expand_return!(ZThing).field_self()); @@ -478,7 +485,8 @@ fn fn_expand_param_type_mismatch_rejected() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().package( crate::package!("ops") .class(crate::ptr_class!(ZThing).constructor(crate::fun!(z_thing_make))) @@ -513,7 +521,8 @@ fn fn_expand_return_type_mismatch_rejected() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().package( crate::package!("ops") .class(crate::ptr_class!(ZThing).method(crate::fun!(z_thing_name).name("name"))) @@ -545,7 +554,8 @@ fn fn_expand_param_unknown_param_rejected() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().package( crate::package!("ops") .class(crate::ptr_class!(ZThing).constructor(crate::fun!(z_thing_make))) @@ -582,7 +592,8 @@ fn typo_in_expand_decl_is_hard_error() { let f: syn::ItemFn = syn::parse_str("pub fn z_fallible() -> Result { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_fallible))) @@ -625,7 +636,8 @@ fn ignore_matching_acknowledges_naming_family() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_len))) @@ -722,7 +734,8 @@ fn method_without_receiver_rejected() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("t").class( crate::ptr_class!(ZThing) @@ -753,7 +766,8 @@ fn constructor_with_wrong_return_rejected() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("t").class( crate::ptr_class!(ZThing) @@ -796,7 +810,8 @@ fn binding_local_field_conditional_handle() { loc.clone(), )); } - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -880,7 +895,8 @@ fn binding_local_field_name_collision_rejected() { loc.clone(), )); } - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -941,7 +957,8 @@ fn binding_local_field_splices_through_parent() { loc.clone(), )); } - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1015,7 +1032,8 @@ fn binding_local_functions_all_positions() { loc.clone(), )); } - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1110,7 +1128,8 @@ fn binding_local_fn_names_flow_through_manglers() { ] { items.push((syn::Item::Fn(syn::parse_str(src).unwrap()), loc.clone())); } - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") // Custom hooks: prefix every derived name — proof the hook RAN and @@ -1199,7 +1218,8 @@ fn binding_local_fun_name_collision_rejected() { ), loc.clone(), )); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("t").class(crate::ptr_class!(ZThing)).fun( // shadows the #[prebindgen] fn of the same name @@ -1252,7 +1272,8 @@ fn gc_managed_handle_lifecycle() { loc.clone(), )); } - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("t") .class( @@ -1337,7 +1358,7 @@ fn split_fixture(extra: &[&str]) -> Registry { loc.clone(), )); } - Registry::::from_items(items).expect("index items") + Registry::::from_items(declare_referenced(items)).expect("index items") } pub(super) fn write_all(gen: crate::api::core::Generation, tag: &str) -> String { @@ -1523,7 +1544,7 @@ fn split_on_param_product_ambiguous_rejected() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1566,7 +1587,7 @@ fn split_declaration_colliding_variants_rejected() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1604,7 +1625,7 @@ fn split_declaration_collision_fails_resolve() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1647,7 +1668,7 @@ fn split_no_split_suppresses_check() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1832,7 +1853,8 @@ fn optional_selector_dispatch_end_to_end() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1891,7 +1913,8 @@ fn constructor_member_skips_default_output_expand() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1957,7 +1980,8 @@ fn qualified_signature_spelling_matches_bare_ptr_class() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") .class(crate::ptr_class!(ZThing).method(crate::fun!(z_thing_name).name("name"))) diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs index 13f1b2e6..a26bdc1a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs @@ -1,6 +1,7 @@ use quote::ToTokens; use super::*; +pub(crate) use crate::api::test_util::declare_referenced; use crate::{ api::{ core::{ diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs b/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs index 07dd453a..f5f1a3f3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs @@ -4,7 +4,7 @@ use super::*; /// remainder is empty. No widening to JObject. #[test] fn option_carves_single_niche() { - let mut reg = Registry::default(); + let mut reg = Registry::empty(); install_input( &mut reg, "TestType", @@ -31,7 +31,7 @@ fn option_carves_single_niche() { /// wire. The third layer hits empty niches and falls back to box. #[test] fn option_cascades_through_multi_niche() { - let mut reg = Registry::default(); + let mut reg = Registry::empty(); // TestType: jint with two niches (MIN, MAX). install_input( @@ -107,7 +107,7 @@ fn option_cascades_through_multi_niche() { /// `None` arm of the match, and the remainder is re-exported. #[test] fn option_output_cascades_through_multi_niche() { - let mut reg = Registry::default(); + let mut reg = Registry::empty(); install_output( &mut reg, "TestType", @@ -165,7 +165,7 @@ fn option_output_cascades_through_multi_niche() { /// decoder stays on `JObject` (no boxing). #[test] fn option_over_jobject_uses_default_null_niche() { - let mut reg = Registry::default(); + let mut reg = Registry::empty(); install_input( &mut reg, "MyStruct", @@ -191,7 +191,7 @@ fn option_over_jobject_uses_default_null_niche() { /// JNI primitives. #[test] fn option_fails_when_no_niche_and_non_primitive_wire() { - let mut reg = Registry::default(); + let mut reg = Registry::empty(); install_input( &mut reg, "MyStruct", @@ -211,7 +211,7 @@ fn option_fails_when_no_niche_and_non_primitive_wire() { /// to widen. #[test] fn option_box_fallback_exposes_no_niches() { - let mut reg = Registry::default(); + let mut reg = Registry::empty(); install_input( &mut reg, "i64", diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs index 3b789fe9..b3ef1f72 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs @@ -31,7 +31,8 @@ fn sealed_kotlin(rename_labeled: Option<&str>) -> String { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let mut sealed = crate::sealed_class!(Reading); if let Some(n) = rename_labeled { @@ -155,7 +156,8 @@ fn declarators_do_not_accept_each_others_shape() { let emit = |item: syn::Item, decl: crate::lang::ClassDecl, tag: &str| { let registry = - Registry::::from_items(vec![(item, loc.clone())]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(item, loc.clone())])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(decl)); @@ -187,7 +189,7 @@ fn declarators_do_not_accept_each_others_shape() { fn unknown_variant_is_an_error() { let loc = myflat_loc(); let boom = || { - let registry = Registry::::from_items(vec![( + let registry = Registry::::from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -195,7 +197,7 @@ fn unknown_variant_is_an_error() { } )), loc.clone(), - )]) + )])) .expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() @@ -216,7 +218,7 @@ fn unknown_variant_is_an_error() { #[test] fn reopened_sealed_class_merges_variant_names() { let loc = myflat_loc(); - let registry = Registry::::from_items(vec![( + let registry = Registry::::from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -225,7 +227,7 @@ fn reopened_sealed_class_merges_variant_names() { } )), loc.clone(), - )]) + )])) .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -283,7 +285,8 @@ fn reopened_ptr_class_keeps_gc_managed() { )] }; let gc_managed_of = |first: crate::lang::PtrClassDecl, second: crate::lang::PtrClassDecl| { - let registry = Registry::::from_items(items()).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items())).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(first).class(second)); @@ -339,7 +342,8 @@ fn a_type_gets_one_class_declarator() { ] }; let declare = |first: crate::lang::ClassDecl, second: crate::lang::ClassDecl| { - let registry = Registry::::from_items(items()).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items())).expect("index items"); let _ = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(first).class(second)); @@ -440,9 +444,11 @@ fn variant_cannot_take_a_name_the_interface_body_already_uses() { // Resolve is where `validate_symbols` runs, so the error surfaces before // any artifact writer touches disk. let resolve_err = |decl: crate::lang::SealedClassDecl, item: syn::ItemEnum| -> String { - let registry = - Registry::::from_items(vec![(syn::Item::Enum(item), loc.clone())]) - .expect("index items"); + let registry = Registry::::from_items(declare_referenced(vec![( + syn::Item::Enum(item), + loc.clone(), + )])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(decl)); @@ -507,9 +513,11 @@ fn variant_cannot_take_a_name_the_interface_body_already_uses() { fn variant_named_companion_moves_the_companion_not_the_variant() { let loc = myflat_loc(); let emit = |decl: crate::lang::SealedClassDecl, item: syn::ItemEnum, tag: &str| -> String { - let registry = - Registry::::from_items(vec![(syn::Item::Enum(item), loc.clone())]) - .expect("index items"); + let registry = Registry::::from_items(declare_referenced(vec![( + syn::Item::Enum(item), + loc.clone(), + )])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(decl)); @@ -579,7 +587,7 @@ fn variant_named_companion_moves_the_companion_not_the_variant() { fn payload_without_output_converter_is_an_error() { let loc = myflat_loc(); let boom = || { - let registry = Registry::::from_items(vec![( + let registry = Registry::::from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -589,7 +597,7 @@ fn payload_without_output_converter_is_an_error() { } )), loc.clone(), - )]) + )])) .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -617,7 +625,7 @@ fn payload_without_output_converter_is_an_error() { #[test] fn sum_is_its_own_type_kind() { let loc = myflat_loc(); - let registry = Registry::::from_items(vec![( + let registry = Registry::::from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -625,7 +633,7 @@ fn sum_is_its_own_type_kind() { } )), loc.clone(), - )]) + )])) .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -658,7 +666,7 @@ fn vec_of_sum_is_rejected_as_a_struct_field() { unimplemented!() } ); - let registry = Registry::::from_items(vec![ + let registry = Registry::::from_items(declare_referenced(vec![ ( syn::Item::Enum(syn::parse_quote!( pub enum Reading { @@ -670,7 +678,7 @@ fn vec_of_sum_is_rejected_as_a_struct_field() { ), (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(f), loc.clone()), - ]) + ])) .expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() @@ -726,11 +734,11 @@ fn recursive_sum_shapes_fail_deterministically() { unimplemented!() } ); - let registry = Registry::::from_items(vec![ + let registry = Registry::::from_items(declare_referenced(vec![ (syn::Item::Enum(e), loc.clone()), (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(f), loc.clone()), - ]) + ])) .expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() @@ -871,7 +879,8 @@ fn sum_returns(tag: &str) -> (String, String) { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::enum_class!(Priority)) @@ -1187,7 +1196,8 @@ fn a_data_class_field_may_be_a_sum_carrying_a_handle() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::ptr_class!(Probe)) @@ -1279,7 +1289,8 @@ fn two_sum_callback_args_keep_their_own_selectors() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::sealed_class!(Reading)) @@ -1358,7 +1369,8 @@ fn sum_in_result_ok_position_is_rejected_with_its_reason() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::sealed_class!(Reading)) @@ -1408,7 +1420,8 @@ fn undeclared_sum_in_result_error_position_is_rejected() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::sealed_class!(Reading)) @@ -1460,7 +1473,8 @@ fn the_diagnostic_names_the_whole_error_type_where_it_must() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::sealed_class!(Reading)) @@ -1516,7 +1530,8 @@ fn declared_sum_in_result_error_position_resolves() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .expand(crate::expand_return!(Reading).field(crate::fun!(reading_code))) @@ -1560,7 +1575,8 @@ fn slice_of_sum_callback_arg_is_rejected_with_its_reason() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::sealed_class!(Reading)) diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs b/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs index 365f6b26..23274500 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs @@ -43,7 +43,8 @@ fn snapshot_pipeline() -> (String, std::collections::BTreeMap) { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -242,7 +243,8 @@ fn handler_interfaces_carry_split_contract_kdoc() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_fallible))) @@ -302,7 +304,8 @@ fn box_string_field_maps_to_nullable_kotlin_string() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("payload") @@ -379,7 +382,8 @@ fn slice_input_builds_vec_handle() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("foo") @@ -500,7 +504,8 @@ fn native_symbols_are_jni_escaped() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.example.my_pkg") @@ -568,7 +573,8 @@ fn jni_native_init_emits_init_block() { )), loc.clone(), )]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs b/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs index fabb9967..19817a46 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs @@ -25,7 +25,8 @@ fn resolve_result(tag: &str, registry: Registry, jni: JniGen) -> Res fn one_fn(src: &str) -> Registry { let f: syn::ItemFn = syn::parse_str(src).unwrap(); - Registry::::from_items(vec![(syn::Item::Fn(f), myflat_loc())]).expect("index") + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), myflat_loc())])) + .expect("index") } /// A `.name()` override that isn't a legal Kotlin identifier is a hard error @@ -82,7 +83,7 @@ fn duplicate_native_symbol_is_error() { myflat_loc(), ), ]; - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); // The JNINative extern method name (which the `Java_…` symbol derives // from) goes through the method hook; collapsing it onto one name for // every function forces two distinct fns to share a native symbol. @@ -119,7 +120,7 @@ fn keyword_struct_field_is_sanitized_not_error() { myflat_loc(), ), ]; - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") .class(crate::data_class!(Payload)) @@ -186,7 +187,7 @@ fn same_name_same_signature_functions_collide() { myflat_loc(), ), ]; - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); // Both forced to Kotlin name `combine`; both take one `Long` → same sig. let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") @@ -212,7 +213,7 @@ fn same_name_distinct_signature_functions_allowed() { myflat_loc(), ), ]; - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); // Same name `combine`, but one takes Long and the other Boolean. let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") @@ -236,7 +237,7 @@ fn method_and_factory_same_name_do_not_collide() { myflat_loc(), ), ]; - let registry = Registry::::from_items(items).expect("index"); + let registry = Registry::::from_items(declare_referenced(items)).expect("index"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing").class( crate::ptr_class!(Thing) diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index dd15da43..eb0ae712 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -74,7 +74,8 @@ fn value_form_items() -> Vec<(syn::Item, crate::SourceLocation)> { /// Build the fixture through `JniGen`, letting the caller adjust the /// `ZSample` boundary decl. Returns the generated Rust + the joined Kotlin. fn value_form_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> (String, String) { - let registry = Registry::::from_items(value_form_items()).expect("index items"); + let registry = Registry::::from_items(declare_referenced(value_form_items())) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -217,7 +218,8 @@ fn deriving_matches_the_equivalent_hand_written_list() { -> Vec<(String, String)> { let mut all = items.clone(); all.extend(extra); - let registry = Registry::::from_items(all).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(all)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -393,7 +395,8 @@ fn sum_field_gen(tag: &str) -> (String, String) { )), loc, )); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -490,7 +493,8 @@ fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -532,7 +536,8 @@ fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { #[test] fn an_adjustment_naming_an_unknown_field_is_an_error() { let build = |decl: crate::lang::FieldsDecl| { - let registry = Registry::::from_items(value_form_items()).expect("index"); + let registry = Registry::::from_items(declare_referenced(value_form_items())) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -639,7 +644,8 @@ fn a_single_leaf_value_form_delivers_an_owned_field() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -698,7 +704,8 @@ fn a_single_leaf_consuming_value_form_moves_its_field() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -763,7 +770,8 @@ fn a_handle_field_of_a_consuming_value_form_moves() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -833,7 +841,8 @@ fn a_sole_handle_field_of_a_consuming_value_form_moves() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -904,7 +913,8 @@ fn an_optional_handle_field_of_a_consuming_value_form_moves() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -974,7 +984,8 @@ fn a_sole_optional_handle_field_takes_callback_delivery() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1040,7 +1051,8 @@ fn an_owned_root_identity_moves_without_any_value_form() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1072,7 +1084,8 @@ fn an_owned_root_identity_moves_without_any_value_form() { #[test] fn a_per_field_override_must_name_the_field_s_own_type() { let build = || { - let registry = Registry::::from_items(value_form_items()).expect("index"); + let registry = Registry::::from_items(declare_referenced(value_form_items())) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1167,7 +1180,8 @@ fn a_nested_value_form_is_hoisted_too() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1283,7 +1297,8 @@ fn a_nested_consuming_value_form_moves_the_parent_s_field() { ), ] { let registry = - Registry::::from_items(items(outer_by_value)).expect("index items"); + Registry::::from_items(declare_referenced(items(outer_by_value))) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1392,7 +1407,8 @@ fn nested_review_jni(outer: crate::lang::ExpandReturnDecl) -> JniGen { /// `Option` it cannot unwrap. #[test] fn an_optional_nested_value_form_is_rejected_before_emission() { - let registry = Registry::::from_items(nested_review_items()).expect("index items"); + let registry = Registry::::from_items(declare_referenced(nested_review_items())) + .expect("index items"); let jni = nested_review_jni( crate::expand_return!(ZReviewOuter).fields(crate::fields!(z_review_outer_to_struct)), ); @@ -1435,7 +1451,8 @@ fn a_value_form_under_an_optional_accessor_is_hoisted_conditionally() { loc, ), ]); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1511,7 +1528,8 @@ fn conditional_owned_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> Stri loc, ), ]); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1565,7 +1583,8 @@ fn an_owned_optional_payload_is_borrowed_for_the_steps_after_it() { loc, ), ]); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1636,7 +1655,8 @@ fn a_rebased_hoist_projects_its_leading_fields_past_a_sibling_move() { loc, ), ]); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1698,7 +1718,8 @@ fn a_consuming_value_form_keeps_its_by_value_boundary_behind_accessors() { loc, ), ]); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1765,7 +1786,8 @@ fn an_owned_intermediate_result_is_borrowed_for_the_next_step() { loc, ), ]); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1878,7 +1900,8 @@ fn a_sum_field_of_a_conditional_value_form_stays_inside_the_arm() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -1931,7 +1954,8 @@ fn a_sum_field_of_a_conditional_value_form_stays_inside_the_arm() { fn a_vec_field_override_must_name_the_whole_vec_type() { let build = || { let registry = - Registry::::from_items(nested_review_items()).expect("index items"); + Registry::::from_items(declare_referenced(nested_review_items())) + .expect("index items"); let jni = nested_review_jni( crate::expand_return!(ZReviewOuter).fields( crate::fields!(z_review_outer_to_struct) @@ -2000,7 +2024,8 @@ fn consuming_items() -> Vec<(syn::Item, crate::SourceLocation)> { } fn consuming_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> String { - let registry = Registry::::from_items(consuming_items()).expect("index items"); + let registry = Registry::::from_items(declare_referenced(consuming_items())) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -2076,7 +2101,8 @@ fn a_borrowed_plan_clones_before_consuming() { )), loc, )); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( @@ -2141,7 +2167,8 @@ fn a_consuming_value_form_rejects_a_plain_field_sibling() { #[test] fn the_declarator_and_the_accessor_s_receiver_must_agree() { let build = |decl: crate::lang::ExpandReturnDecl| -> String { - let registry = Registry::::from_items(consuming_items()).expect("index"); + let registry = Registry::::from_items(declare_referenced(consuming_items())) + .expect("index"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package( diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs index 7fb8f4e8..99758e22 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs @@ -4,21 +4,23 @@ use super::*; fn bounded_duration_option_uses_u64_niche_without_boxing() { let loc = myflat_loc(); let items: Vec<(syn::Item, SourceLocation)> = [ - "pub fn duration_from_millis(v: u64) -> std::time::Duration { unimplemented!() }", - "pub fn duration_to_millis(v: &std::time::Duration) -> u64 { unimplemented!() }", - "pub fn duration_echo(v: Option) -> Option { unimplemented!() }", + "#[prebindgen] pub type Duration = std::time::Duration;", + "pub fn duration_from_millis(v: u64) -> Duration { unimplemented!() }", + "pub fn duration_to_millis(v: &Duration) -> u64 { unimplemented!() }", + "pub fn duration_echo(v: Option) -> Option { unimplemented!() }", ] .into_iter() .map(|source| { - let function: syn::ItemFn = syn::parse_str(source).unwrap(); - (syn::Item::Fn(function), loc.clone()) + // `syn::Item`, not `ItemFn`: a fixture declares the types it names. + let item: syn::Item = syn::parse_str(source).unwrap(); + (item, loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).unwrap(); + let registry = Registry::::from_items(declare_referenced(items)).unwrap(); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert( - crate::convert!(std::time::Duration) + crate::convert!(Duration) .input(crate::fun!(duration_from_millis)) .output(crate::fun!(duration_to_millis)) .valid_range(0u64..=1_000_000u64), @@ -68,14 +70,14 @@ fn flattened_field_composes_bounded_conversion_stages() { ( syn::Item::Struct(syn::parse_quote!( pub struct Timed { - pub delay: Option, + pub delay: Option, } )), loc.clone(), ), ( syn::Item::Fn(syn::parse_quote!( - pub fn duration_from_millis(v: u64) -> std::time::Duration { + pub fn duration_from_millis(v: u64) -> Duration { unimplemented!() } )), @@ -83,7 +85,7 @@ fn flattened_field_composes_bounded_conversion_stages() { ), ( syn::Item::Fn(syn::parse_quote!( - pub fn duration_to_millis(v: &std::time::Duration) -> u64 { + pub fn duration_to_millis(v: &Duration) -> u64 { unimplemented!() } )), @@ -106,11 +108,12 @@ fn flattened_field_composes_bounded_conversion_stages() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert( - crate::convert!(std::time::Duration) + crate::convert!(Duration) .input(crate::fun!(duration_from_millis)) .output(crate::fun!(duration_to_millis)) .valid_range(0u64..=1_000_000u64), @@ -153,11 +156,11 @@ fn flattened_field_composes_bounded_conversion_stages() { assert!(rc.contains("jlong_to_u64"), "{rust}"); assert!(rc.contains("u64_to_Duration"), "{rust}"); assert!( - rc.contains("jlong_to_Option_std_time_Duration") && rc.contains("env,&__delay_raw)?"), + rc.contains("jlong_to_Option_Duration") && rc.contains("env,&__delay_raw)?"), "whole-JObject input must invoke the complete optional Duration converter:\n{rust}" ); assert!( - rc.contains("let___delay:jni::sys::jlong=Option_std_time_Duration_to_jlong") + rc.contains("let___delay:jni::sys::jlong=Option_Duration_to_jlong") && rc.contains("\"(J)Lio/test/jni/Timed;\""), "whole-struct output must pass the niche as primitive jlong:\n{rust}" ); @@ -170,12 +173,16 @@ fn flattened_field_composes_bounded_conversion_stages() { #[test] fn duration_requires_an_explicit_conversion() { - let function: syn::ItemFn = syn::parse_str( - "pub fn duration_echo(v: std::time::Duration) -> std::time::Duration { unimplemented!() }", - ) - .unwrap(); - let registry = Registry::::from_items([(syn::Item::Fn(function), myflat_loc())]) - .expect("index items"); + let alias: syn::Item = + syn::parse_str("#[prebindgen] pub type Duration = std::time::Duration;").unwrap(); + let function: syn::ItemFn = + syn::parse_str("pub fn duration_echo(v: Duration) -> Duration { unimplemented!() }") + .unwrap(); + let registry = Registry::::from_items(declare_referenced([ + (alias, myflat_loc()), + (syn::Item::Fn(function), myflat_loc()), + ])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .package(crate::package!("time").fun(crate::fun!(duration_echo))); @@ -192,19 +199,20 @@ fn duration_requires_an_explicit_conversion() { fn conversion_domain_must_match_the_representation() { let loc = myflat_loc(); let items: Vec<(syn::Item, SourceLocation)> = [ - "pub fn duration_from_millis(v: u64) -> std::time::Duration { unimplemented!() }", - "pub fn duration_use(v: std::time::Duration) { unimplemented!() }", + "pub fn duration_from_millis(v: u64) -> Duration { unimplemented!() }", + "pub fn duration_use(v: Duration) { unimplemented!() }", ] .into_iter() .map(|source| { - let function: syn::ItemFn = syn::parse_str(source).unwrap(); - (syn::Item::Fn(function), loc.clone()) + // `syn::Item`, not `ItemFn`: a fixture declares the types it names. + let item: syn::Item = syn::parse_str(source).unwrap(); + (item, loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).unwrap(); + let registry = Registry::::from_items(declare_referenced(items)).unwrap(); let jni = JniGen::new() .convert( - crate::convert!(std::time::Duration) + crate::convert!(Duration) .input(crate::fun!(duration_from_millis)) .valid_range(0i64..=1_000i64), ) @@ -242,7 +250,8 @@ fn option_scalar_param_crosses_as_present_value_pair() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -339,7 +348,8 @@ fn vec_of_handle_output_folds_kotlin_side() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("thing") @@ -422,7 +432,8 @@ fn option_scalar_struct_field_flattens() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() @@ -537,7 +548,8 @@ fn recursive_data_class_input_flattens_nested_and_optional_fields() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") @@ -653,7 +665,8 @@ fn jobject_input_is_an_explicit_hybrid_leaf_escape_hatch() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::data_class!(FlatChild)) @@ -726,7 +739,8 @@ fn recursive_flattened_owned_handles_join_lock_and_consume_scaffold() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::ptr_class!(Token)) @@ -782,10 +796,10 @@ fn recursive_flattening_rejects_jvm_parameter_slot_overflow() { } ); let loc = myflat_loc(); - let registry = Registry::::from_items([ + let registry = Registry::::from_items(declare_referenced([ (syn::Item::Struct(wide.clone()), loc.clone()), (syn::Item::Fn(use_wide.clone()), loc.clone()), - ]) + ])) .expect("index items"); let jni = JniGen::new().package( crate::package!() @@ -802,10 +816,10 @@ fn recursive_flattening_rejects_jvm_parameter_slot_overflow() { // The explicit object boundary keeps the same public Kotlin data class, // but the native method receives it in one slot and performs the legacy // whole-object field decode instead of producing an illegal signature. - let registry = Registry::::from_items([ + let registry = Registry::::from_items(declare_referenced([ (syn::Item::Struct(wide), loc.clone()), (syn::Item::Fn(use_wide), loc), - ]) + ])) .expect("index marked items"); let jni = JniGen::new().package( crate::package!() @@ -837,7 +851,8 @@ fn output_only_convert_resolves_without_input_twin() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert(crate::convert!(Len).output(crate::fun!(len_value))) @@ -882,7 +897,8 @@ fn convert_fn_qualifies_with_origin_crate() { "my-helpers", )]; let registry = - Registry::::from_items(flat.into_iter().chain(helpers)).expect("index items"); + Registry::::from_items(declare_referenced(flat.into_iter().chain(helpers))) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert(crate::convert!(Len).output(crate::fun!(len_value))) @@ -917,7 +933,8 @@ fn convert_input_target_mismatch_rejected() { (syn::Item::Fn(f), loc.clone()) }) .collect(); - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new() .convert(crate::convert!(Len).input(crate::fun!(from_long))) .package(crate::package!("len").fun(crate::fun!(use_len))); @@ -938,7 +955,8 @@ fn convert_via_trait_impls() { let f: syn::ItemFn = syn::parse_str("pub fn temp_double(c: Celsius) -> Celsius { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert( @@ -973,7 +991,8 @@ fn convert_via_try_from_is_fallible() { let f: syn::ItemFn = syn::parse_str("pub fn pct_use(p: Percent) -> i32 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert(crate::convert!(Percent).input(crate::try_from!(i32))) @@ -1007,7 +1026,8 @@ fn option_composition_normalizes_fallible_stage_errors() { ) .unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert( @@ -1044,7 +1064,8 @@ fn convert_via_local_fns() { let f: syn::ItemFn = syn::parse_str("pub fn label_id(l: Label) -> Label { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert( @@ -1112,7 +1133,8 @@ fn convert_via_local_try_fn_is_fallible() { let f: syn::ItemFn = syn::parse_str("pub fn label_id(l: Label) -> Label { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(vec![(syn::Item::Fn(f), loc)]).expect("index items"); + Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + .expect("index items"); let jni = JniGen::new() .set_package_prefix("io.test.jni") .convert( @@ -1169,7 +1191,8 @@ fn data_class_members_reenter_as_field_leaves() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!().class( crate::data_class!(Point) @@ -1268,7 +1291,8 @@ fn unsigned_scalars_use_lossless_kotlin_surface_and_raw_jni_wires() { loc, ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::data_class!(Unsigned)) @@ -1396,7 +1420,8 @@ fn data_class_properties_match_their_from_parts_params() { loc.clone(), ), ]; - let registry = Registry::::from_items(items).expect("index items"); + let registry = + Registry::::from_items(declare_referenced(items)).expect("index items"); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!() .class(crate::ptr_class!(Handle)) @@ -1476,35 +1501,10 @@ fn check_array_length_qualification(loc: SourceLocation, module: &str) { )), loc.clone(), )); - // A type owning an ASSOCIATED const, used as the other length below. It is - // deliberately NEVER declared to JniGen: it is only the Rust namespace for - // a compile-time length, not a boundary type, so qualification must not - // require a Kotlin class to exist for it. - items.push(( - syn::Item::Struct(syn::parse_quote!( - pub struct Holder { - pub marker: u8, - } - )), - loc.clone(), - )); - // A `const fn` whose CALL is a length. Also never declared: its result - // determines an array size, which is no reason to put it in the Kotlin - // surface. - items.push(( - syn::Item::Fn(syn::parse_quote!( - pub const fn array_len() -> usize { - 4 - } - )), - loc.clone(), - )); items.push(( syn::Item::Struct(syn::parse_quote!( pub struct Blob { pub bytes: [u8; env], - pub assoc: [u8; Holder::N], - pub called: [u8; array_len()], } )), loc.clone(), @@ -1517,7 +1517,7 @@ fn check_array_length_qualification(loc: SourceLocation, module: &str) { )), loc.clone(), )); - let registry = Registry::::from_items(items).unwrap(); + let registry = Registry::::from_items(declare_referenced(items)).unwrap(); let jni = JniGen::new().set_package_prefix("io.test.jni").package( crate::package!("blob") .class(crate::data_class!(Blob)) @@ -1544,126 +1544,4 @@ fn check_array_length_qualification(loc: SourceLocation, module: &str) { ); assert!(!rc.contains(&format!("{module}::env,")), "{rust}"); assert!(!rc.contains(&format!("&mut{module}::env")), "{rust}"); - - // An ASSOCIATED const qualifies its leading TYPE segment and leaves the - // rest of the path relative to it — `myflat::Holder::N`, never - // `myflat::Holder::myflat::N`. `Holder` is UNDECLARED, so this also pins - // that qualification reads the registry rather than the declared surface. - // Asserted at the two CODE positions (return type and param type); the bare - // spelling legitimately survives inside the decode's diagnostic string, - // which names the type as the source wrote it. - assert!( - rc.contains(&format!("Result<[u8;{module}::Holder::N]")), - "{rust}" - ); - assert!( - rc.contains(&format!("v:[u8;{module}::Holder::N]")), - "{rust}" - ); - assert!(!rc.contains("Result<[u8;Holder::N]"), "{rust}"); - assert!(!rc.contains("v:[u8;Holder::N]"), "{rust}"); - // The leading segment is rewritten ONCE — the associated item stays - // relative to the type it belongs to. - assert!( - !rc.contains(&format!("{module}::Holder::{module}")), - "{rust}" - ); - - // A `const fn` CALL is a third shape a length can take, and its callee is - // an indexed item like the other two. Also undeclared. - assert!( - rc.contains(&format!("Result<[u8;{module}::array_len()]")), - "{rust}" - ); - assert!( - rc.contains(&format!("v:[u8;{module}::array_len()]")), - "{rust}" - ); - assert!(!rc.contains("Result<[u8;array_len()]"), "{rust}"); - assert!(!rc.contains("v:[u8;array_len()]"), "{rust}"); -} - -/// An array length whose expression form is not on the supported whitelist is -/// REJECTED, not qualified. -/// -/// An inline `const { … }` block may bind locals, and this generator qualifies -/// a length's bare paths against their source module — so a local shadowing a -/// source item would be rewritten into it (`array_len` the local becoming -/// `myflat::array_len` the fn). Scope tracking is the general answer; the shape -/// has no place in an FFI boundary type, so the whole family is refused with a -/// message naming the type and the fix. Silently mis-qualifying is the -/// alternative this exists to prevent. -#[test] -#[should_panic(expected = "an unsupported expression form")] -fn array_length_inline_const_block_is_rejected() { - // A local bound by an inline const block, shadowing the indexed fn. - check_array_length_rejected(syn::parse_quote!( - [u8; const { - let array_len = 3; - array_len - }] - )); -} - -/// `match` arms bind their patterns directly, with no `Expr::Block` node in -/// between — which is how this form slipped past the first, blacklist-shaped -/// attempt. The whitelist refuses it because `match` is simply not on the list. -#[test] -#[should_panic(expected = "an unsupported expression form")] -fn array_length_match_arm_binding_is_rejected() { - check_array_length_rejected(syn::parse_quote!( - [u8; match 3 { - array_len => array_len, - }] - )); -} - -/// `if let` likewise binds without an intervening block node. -#[test] -#[should_panic(expected = "an unsupported expression form")] -fn array_length_if_let_binding_is_rejected() { - check_array_length_rejected(syn::parse_quote!( - [u8; if let array_len = 3 { array_len } else { 0 }] - )); -} - -fn check_array_length_rejected(field_ty: syn::Type) { - let loc = myflat_loc(); - let mut items: Vec<(syn::Item, SourceLocation)> = Vec::new(); - items.push(( - syn::Item::Fn(syn::parse_quote!( - pub const fn array_len() -> usize { - 4 - } - )), - loc.clone(), - )); - // The offending length; in each case its binding shadows `array_len`. - items.push(( - syn::Item::Struct(syn::parse_quote!( - pub struct Blob { - pub local: #field_ty, - } - )), - loc.clone(), - )); - items.push(( - syn::Item::Fn(syn::parse_quote!( - pub fn blob_echo(b: Blob) -> Blob { - unimplemented!() - } - )), - loc.clone(), - )); - let registry = Registry::::from_items(items).unwrap(); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("blob") - .class(crate::data_class!(Blob)) - .fun(crate::fun!(blob_echo)), - ); - let dir = unique_test_dir("jnigen_array_len_scope"); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).unwrap(); - generation.write_rust(dir.join("gen.rs")).unwrap(); } diff --git a/prebindgen/src/api/test_util.rs b/prebindgen/src/api/test_util.rs index b13ada8d..833e64a3 100644 --- a/prebindgen/src/api/test_util.rs +++ b/prebindgen/src/api/test_util.rs @@ -8,16 +8,76 @@ use std::{ use crate::api::core::registry::Registry; -/// Index a `Registry` from a list of Rust function sources. -pub(crate) fn reg_with(fns: &[&str]) -> Registry<()> { - let items = fns +/// Index a `Registry` from a list of Rust item sources. +/// +/// Accepts any item, not just a fn, so a fixture can declare the types it names. +/// Whatever it does *not* declare is supplied by [`declare_referenced`], because +/// these fixtures exist to exercise plan shapes and a handle declaration is noise +/// in them. +pub(crate) fn reg_with(sources: &[&str]) -> Registry<()> { + let items = sources .iter() .map(|src| { - let f: syn::ItemFn = syn::parse_str(src).expect("parse fn"); - (syn::Item::Fn(f), crate::SourceLocation::default()) + let item: syn::Item = syn::parse_str(src).expect("parse item"); + (item, crate::SourceLocation::default()) }) .collect::>(); - Registry::from_items(items).expect("index") + Registry::from_items(declare_referenced(items)).expect("index") +} + +/// Append a marked type alias for every nominal type the stream names but never +/// declares, so a fixture satisfies the flat API's self-sufficiency rule. +/// +/// A fixture that is *about* a handle's treatment already declares it; this covers +/// the ones where the handle is incidental — `reg_with(&["fn get(s: &Storage) -> Payload"])` +/// is testing an unfold plan, not what `Storage` is. Declaring them as +/// [`Extern`](crate::core::flat::Extern)s is exactly what a real source crate does +/// for a foreign handle, and it is inert for the registry either way: a type alias +/// lands in no registry map. +/// +/// Runs to a fixed point, since a declaration can only ever resolve more references. +/// It cannot help a **path-qualified** name (`std::time::Duration`), which no +/// declaration can name — such a fixture has to spell the type bare and declare it. +pub(crate) fn declare_referenced(items: I) -> Vec<(syn::Item, crate::SourceLocation)> +where + I: IntoIterator, +{ + use crate::api::core::flat::{Flat, ItemError}; + + let mut items: Vec<(syn::Item, crate::SourceLocation)> = items.into_iter().collect(); + + loop { + let flat = Flat::builder() + .items(items.iter().cloned()) + .build() + .expect("fixture parses"); + // A set: the same name is reported once per referencing item. + let missing: std::collections::BTreeSet = flat + .unsupported() + .filter_map(|u| match &*u.error { + // Skip a name the stream already holds. Refusal is transitive, so a + // declared struct whose own field is undeclared reports as + // unresolved too — declaring an alias for it would collide. Adding + // the root name resolves it on the next round. + ItemError::UnresolvedType { name } + if !name.contains("::") && flat.element(name).is_none() => + { + Some(name.clone()) + } + _ => None, + }) + .collect(); + if missing.is_empty() { + return items; + } + for name in missing { + let ident = quote::format_ident!("{name}"); + let alias: syn::Item = syn::parse_quote!( + pub type #ident = __fixture::#ident; + ); + items.push((alias, crate::SourceLocation::default())); + } + } } /// A process-unique temp directory for a test that writes files. Keyed by From c9bd1169072bed35aefcc00e7a89d055b0553f23 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Thu, 30 Jul 2026 23:16:57 +0200 Subject: [PATCH 07/52] The type table carries Flat's reading of each type (#239) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * The type table carries Flat's reading of each type L1 made `Registry` a projection of `Flat` for the item maps. The **type table** — what generation actually runs on — still threw the frontend's work away, keying cells by a normalized `syn::Type` with the classification deleted. That deletion is why `types_util` exports `is_option_type` / `option_inner_type` / `result_parts` / `bare_path_ident`: 144 uses outside that one file, all recomputing what `Flat` decided. `TypeEntry` itself had nothing to reuse and is unchanged in spirit: `destination`, `function`, `pre_stages`, `niches`, `metadata` are the adapter's answer, not the source's meaning. The reuse is one level up, in the cell the entry hangs off. input_types: HashMap> TypeCell { subject: TypeSubject, root: bool, entry: Option> } TypeSubject::Source(TypeRef) | TypeSubject::Adapter(syn::Type) An enum rather than an `Option` beside a location, because a type the flat API contains **is** a `TypeRef` — classification and origin together — and a type only the binding authored has no reading and no source location. That is a fact about it, not information that went missing. So `type_locations` is deleted: `TypeRef.origin.location` is it. The old map was worse than duplicated, it was circular — the declared-type path read a key's location back out of the map it was about to write, falling back to `SourceLocation::default()`. With one origin per cell the whole `loc` parameter threads out of `ensure_entry`, `scan_fn_signature`, `scan_struct`, `scan_enum`, `register_type_*`, `require_input` and `require_output`. **The readings come from the model, not from lowering twice.** `Flat::type_refs` walks every type the API mentions — the new accessor, distinct from `types()`, which is every type it *declares* — and `from_flat` indexes it before anything is scanned. `ensure_entry` then looks a key up. Keying by type rather than threading positionally is what makes it right for generics: `TypeId` carries no arguments, so `MyBox` has no `Foo` child in its `kind`, yet the registry's walk emits a `Foo` sub-key — which finds its reading from wherever else `Foo` appears. `first_unresolved`'s per-element slot enumeration became `element_type_refs`, so the slots are listed once and `type_refs` cannot drift from the resolver. **`required` stops being stored.** It was one name over three storages, and two facts: *is a root* (a scan fact) and *is reachable from a root through the adapter's `subs`* (a derivation). The old code wrote the derived answer back into `TypeEntry::required` **and** `required_*_scan`, which already held the root fact. Now the cell keeps `root` and `resolve::required_set` returns the reachable set for `final_invariant_check` to consume. Gone: `required_inputs_scan`, `required_outputs_scan`, `TypeEntry::required`, `propagate_required`, `set_required`, `is_required_resolved`, `mark_and_get_subs`, `is_required_*_at_scan`, `lookup_slot`. `root` stays a field rather than folding into `TypeSubject` because the axes are independent — all four combinations occur. `Source + root: false` is the bulk of the table (every nested position, every field type), and `Adapter + root: true` is what `required_output_types` is for. `immediate_edges` reads a declared type's fields off the element (`flat.declared_type`) instead of `syn::Fields::Named`, which silently skipped positional fields. Same edge set today — a tuple struct is an `Extern` and declares none — without the asymmetry. Its fallout in tests was a fixture that hand-inserted into `reg.structs` while leaving `flat` empty; it now drives the real scan, which is the state the pipeline can actually produce. **Measured**: 3 `Adapter` cells out of 342 across the four examples — `Option` and `Result` (shapes the adapter composes) and `MaybeUninit`. The last is the evidence for a deferral: flat absorbs `MaybeUninit` into `RefMode::Out`, so that bare node exists only in the registry's syntactic walk — which is why `immediate_subtype_positions` is not yet replaced by a `TypeKind`-children walk. The `Option` earns its keep. The ledger does not move. This makes the classification available; taking callers off raw syntax is L2's own work. Generation is byte-identical, 523 + 451 tests pass, covertest-kotlin runs all 48 sections. Co-Authored-By: Claude Opus 5 * `const _` is a Guard, not a Constant The injected feature check was modelled as a `Constant` whose name happens to be `_`, and every consumer that must not treat it as API re-checked that sentinel. Five sites did, across four files. Two facts make the sentinel wrong rather than untidy: **The guard is not a captured item.** `Source`'s cfg filter *synthesizes* it (`api/batching/cfg_filter.rs:143`) — one per ingested crate, asserting that crate's `FEATURES` match what the build script asked for. Nothing in the source crate marked it, so it was never part of the flat API, which is the set of things a `#[prebindgen]` crate declares. **Four of the five checks were already dead.** Once L1 routed unnamed consts away from `consts`, `write.rs`'s const gate, the skipped-const warning, and both `on_const` implementations guarded a state the pipeline could no longer produce. That is the failure mode a sentinel invites, and it had already happened. So: Element::Guard(Guard { origin: Origin }) Named for what it **is** — a compile-time assertion protecting the generated file — not for what a consumer does with it. `Element` classifies; `Passthrough` would name an emission strategy, and that variant was deliberately deleted earlier in this program. Recognised by **shape**, not provenance: a constant with no name has no address, so nothing can declare it, reference it, or emit it as an alias. That is the property that makes it infrastructure and it holds whoever wrote it — so no new ingestion channel is needed and today's behaviour is preserved exactly. It carries **no `TypeRef`**. The item is emitted verbatim, so what its types mean is the consumer crate's business. Today the guard's `()` is lowered and does participate in `first_unresolved`, so a guard naming an undeclared type would turn the whole element `Unsupported` and — post-L1 — fail the build. `()` is `Unit`, so that never bit; dropping the slot removes the coupling. `Element::name` loses its `.filter(|id| *id != "_")`, which existed for this alone. `Registry::passthrough` becomes `guards: Vec` — the bucket's one occupant now names it. Emission is unmoved: last in `write_rust`, in stream order. One `"_"` comparison stays, in `flat/mod.rs`'s Pass 1: the `ConstIndex` an array extent resolves against is built before Pass 2 classifies anything, so it has only raw items to filter. It is the one site that cannot read a classification, and now says so. The module doc's "no verbatim passthrough" claim is **amended, not reversed**: no *marked* item passes through, and the one item that does was never marked. Generation byte-identical (the two aarch64 goldens drift identically to the base branch — the known `--features unstable` mismatch), 524 + 452 tests, covertest 48 sections. Both new tests were checked against a reverted classification and fail as they should. Co-Authored-By: Claude Opus 5 * Address review: state the contract the classifier actually enforces **The docs overclaimed (review 1).** `lower_item` classifies *any* anonymous const as a `Guard` — a hand-fed `FlatBuilder` item, a user-written `#[prebindgen] const _: ..` — but the docs said "prebindgen's own injected checks" and "one feature guard per ingested source crate". Both cardinality claims are wrong, and I checked rather than assumed: * `enable_feature_filtering(None)` leaves `features_constant: None`, so `build_cfg_filter` skips the guard entirely — **zero**; * `items_all` / `items_in_groups` / `items_except_groups` each build a *fresh* `CfgFilter` with `prelude_emitted: false`, so composing two iterators from one `Source` yields **two** guards from one crate. Keeping the shape rule, which was the deliberate choice, and making the docs say what it means: a `Guard` is an **anonymous const**, defined by having no address rather than by who produced it; the feature check is documented as today's producer rather than the definition; cardinality is **zero or more**. Six sites, including `Guard`'s own doc, `Flat::guards`, `Registry::guards` and `write.rs`. **Emission was untested (review 2).** `a_guard_never_reaches_the_const_surface` proves the maps are separate but never calls `write_rust`, so nothing caught a change that keeps `Registry::guards` populated and then drops or re-gates it on the way out. `guards_emit_ungated_and_in_stream_order` declares an *empty* `declared_consts()` gate with one named const and two distinguishable guards straddling it, and asserts the named const is gated out while both guards emit in order. Checked against both failure modes — emitting none, and emitting reversed — and it fails on each. **The doc contradiction (review 3).** `from_items` listed `guards` among the maps and then said undeclared items "never emit", which is the opposite of what a guard does. Now says an *API* item behaves that way and names `guards` as the exception that is outside the gate because it has no name to declare. The core module overview's stale "passthrough items" goes with it. Also fixed three unresolved intra-doc links introduced across this stack (one here, two in the type-cell commit) — no CI job gates on them, so they are fixed at the tip rather than by another rebase. Warnings 17 → 16 against the L1 baseline. 529 + 457 tests, clippy clean in three configs, generation byte-identical, covertest 48 sections. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- prebindgen/src/api/core/expand.rs | 4 +- prebindgen/src/api/core/expand/tests.rs | 4 +- prebindgen/src/api/core/flat/element.rs | 54 ++- prebindgen/src/api/core/flat/mod.rs | 93 ++++- .../src/api/core/flat/tests/acceptance.rs | 14 +- prebindgen/src/api/core/flat/tests/mod.rs | 1 + prebindgen/src/api/core/flat/ty.rs | 31 ++ prebindgen/src/api/core/mod.rs | 6 +- prebindgen/src/api/core/prebindgen.rs | 12 +- prebindgen/src/api/core/registry.rs | 338 ++++++++++-------- prebindgen/src/api/core/registry/tests.rs | 171 +++++++-- prebindgen/src/api/core/resolve.rs | 174 ++++----- prebindgen/src/api/core/resolve/tests.rs | 109 ++++-- prebindgen/src/api/core/unfold.rs | 28 +- prebindgen/src/api/core/unfold/tests.rs | 45 ++- prebindgen/src/api/core/write.rs | 27 +- prebindgen/src/api/core/write/tests.rs | 157 ++++++-- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 2 +- .../src/api/lang/jnigen/jni/tests/mod.rs | 9 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 6 - prebindgen/src/api/test_util.rs | 15 +- prebindgen/src/lib.rs | 2 +- 22 files changed, 837 insertions(+), 465 deletions(-) diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index 8335990b..b384134f 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -299,7 +299,7 @@ fn process_expand( exp: &Expansions, ed: &ExpandDecl, ) -> Result<(), ExpandError> { - let (item_fn, loc) = registry + let (item_fn, _) = registry .functions .get(&ed.func) .cloned() @@ -334,7 +334,7 @@ fn process_expand( )?; for leaf in &plan.leaves { - registry.require_input(&leaf.ty, &loc); + registry.require_input(&leaf.ty); } registry .expansion_plans diff --git a/prebindgen/src/api/core/expand/tests.rs b/prebindgen/src/api/core/expand/tests.rs index dac59ab7..fe027c15 100644 --- a/prebindgen/src/api/core/expand/tests.rs +++ b/prebindgen/src/api/core/expand/tests.rs @@ -104,9 +104,7 @@ fn constructor_plan_and_fold() { // Leaf types registered as required inputs (so the resolver builds // their converters). - assert!(reg - .required_inputs_scan - .contains(&TypeKey::from_type(&plan.leaves[1].ty))); + assert!(reg.input_types[&TypeKey::from_type(&plan.leaves[1].ty)].root); let locals = vec![ident("sel"), ident("v0"), ident("vid")]; let folded = emit_fold(plan, &locals, &src_qualify); diff --git a/prebindgen/src/api/core/flat/element.rs b/prebindgen/src/api/core/flat/element.rs index f8193a5c..1bb7ea81 100644 --- a/prebindgen/src/api/core/flat/element.rs +++ b/prebindgen/src/api/core/flat/element.rs @@ -15,8 +15,9 @@ use crate::SourceLocation; /// One member of the flat API. /// /// Three modelled kinds — a function, a type, a constant — plus -/// [`Element::Unsupported`] for anything the language cannot express. There is no -/// verbatim-passthrough variant: a `#[prebindgen]` crate marks the items that +/// [`Element::Guard`] for an anonymous const and [`Element::Unsupported`] for +/// anything the language cannot express. No *marked* +/// item passes through verbatim: a `#[prebindgen]` crate marks the items that /// cross the boundary, and the supporting code around them is the consumer /// crate's job — the proc-macro enforces that already, refusing to mark a `use`, /// `mod`, `impl` or `macro_rules!` at all. @@ -26,6 +27,9 @@ pub enum Element { /// A type declaration: a struct, either enum shape, or an opaque handle. Type(Type), Constant(Constant), + /// An anonymous const — infrastructure re-emitted verbatim. Carries no API + /// surface: nothing can name it, declare it, or cross it. + Guard(Guard), /// An item the language cannot express — a parameter type outside the /// grammar, a `self` receiver, a reference to a type the flat API never /// declares, or a whole item kind it does not model such as a `union`. @@ -42,17 +46,16 @@ impl Element { /// The item's name, which is also its address: `#[prebindgen]` names live /// in one flat namespace across every ingested source crate. /// - /// `None` when the item has no address — an unnamed `const _` (each - /// source's injected feature guard, so several may coexist), or an item - /// kind with no identifier at all. + /// `None` when the item has no address — a [`Guard`], or an item kind with + /// no identifier at all. pub fn name(&self) -> Option<&syn::Ident> { - let named = match self { + match self { Element::Function(f) => Some(&f.name), Element::Type(t) => Some(t.name()), Element::Constant(c) => Some(&c.name), + Element::Guard(_) => None, Element::Unsupported(u) => u.name.as_ref(), - }; - named.filter(|id| *id != "_") + } } /// Where the item was captured, including the crate that marked it. @@ -64,6 +67,7 @@ impl Element { Element::Function(f) => &f.origin.location, Element::Type(t) => t.location(), Element::Constant(c) => &c.origin.location, + Element::Guard(g) => &g.origin.location, Element::Unsupported(u) => &u.origin.location, } } @@ -74,6 +78,7 @@ impl Element { Element::Function(f) => syn::Item::Fn(f.origin.syntax.clone()), Element::Type(t) => t.syntax(), Element::Constant(c) => syn::Item::Const(c.origin.syntax.clone()), + Element::Guard(g) => syn::Item::Const(g.origin.syntax.clone()), Element::Unsupported(u) => u.origin.syntax.clone(), } } @@ -334,10 +339,8 @@ pub struct Field { /// A `#[prebindgen]` constant. /// -/// Also the home of the unnamed `const _` feature guard each source injects: it -/// is a constant, so it is modelled as one, and [`Element::name`] returning -/// `None` for `_` is what keeps several of them from colliding in the flat -/// namespace. +/// Always named: an unnamed `const _` is a [`Guard`], not a constant with no +/// address. #[derive(Clone, Debug)] pub struct Constant { pub name: syn::Ident, @@ -347,6 +350,33 @@ pub struct Constant { pub origin: Origin, } +/// An **anonymous const**: `const _: T = ..`, whatever produced it. +/// +/// The definition is the shape, not the origin. A const with no name has no +/// address, so nothing can declare it, reference it, or emit it as an alias — +/// which is what puts it outside the flat API rather than in it, and that holds +/// however the item arrived: synthesized, hand-fed to [`FlatBuilder`](super::FlatBuilder), or written +/// as `#[prebindgen] const _: () = ..` in a source crate. +/// +/// **Today's producer** is [`Source`](crate::Source)'s cfg filter, which +/// synthesizes `const _: () = { konst::assertc_eq!(..) }` to assert that a source +/// crate's `FEATURES` match what the build script asked for. Nothing *marked* +/// that one — it is prebindgen's own item riding the same stream — but it is not +/// the only thing that can land here, and the model does not claim otherwise. +/// +/// **Cardinality is zero or more.** `enable_feature_filtering(None)` produces +/// none, and each item iterator taken from a `Source` emits its own, so composing +/// two iterators from one crate yields two. +/// +/// It carries no type, unlike a [`Constant`]. The item is emitted verbatim, so +/// what its types mean is the consumer crate's business; modelling them would let +/// a guard that names something undeclared refuse the whole element. +#[derive(Clone, Debug)] +pub struct Guard { + /// Emitted verbatim, so the item is all there is. + pub origin: Origin, +} + /// An item the language cannot express. #[derive(Clone, Debug)] pub struct Unsupported { diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index abd9b191..913a6a96 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -114,13 +114,20 @@ //! adapter declaration is examined. A binding is built against a model the //! frontend could read in full, or it is not built. //! -//! There is **no verbatim passthrough**, because a `#[prebindgen]` crate marks -//! the items that cross the boundary and leaves the supporting code to the +//! 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`](crate::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;` declares an [`Extern`]: it gives @@ -168,8 +175,8 @@ use self::{array_len::ConstIndex, ty::lower_type}; pub use self::{ array_len::{ArrayExtent, ArrayLenReason, ConstId, ExtentSource, UnsupportedArrayLen}, element::{ - Alternative, Constant, Element, Enum, EnumValue, Extern, Field, Function, Param, Struct, - Type, Unsupported, Variant, + Alternative, Constant, Element, Enum, EnumValue, Extern, Field, Function, Guard, Param, + Struct, Type, Unsupported, Variant, }, origin::Origin, ty::{RefMode, ScalarKind, TypeId, TypeKind, TypeRef, UnsupportedType, UnsupportedTypeReason}, @@ -327,9 +334,11 @@ impl FlatBuilder { crate::api::core::types_util::normalize_item_types(item, &normalization); } - // Pass 1: the consts an array length may name. Unnamed `const _` items - // are excluded for the same reason `Element::name` skips them — they - // are not addressable, so no length can name one. + // Pass 1: the consts an array length may name. Unnamed items are + // excluded because no length can name one — the same fact that makes + // them `Guard`s. This is the one place that tests the spelling rather + // than the classification, and it has to: it runs before Pass 2, so no + // classification exists yet. let consts = ConstIndex::new(items.iter().filter_map(|(item, loc)| match item { syn::Item::Const(c) if c.ident != "_" => Some(( c.ident.to_string(), @@ -473,6 +482,35 @@ impl Flat { }) } + /// Every type the API **mentions**, at every nesting depth — as distinct from + /// [`Self::types`], which is every type it **declares**. + /// + /// A parameter, a return, a field, a constant's type, and everything reachable + /// inside those. The same type mentioned in several places yields one + /// [`TypeRef`] per mention, each with its own spelling and origin; a consumer + /// that wants one per type indexes them and picks, and element order makes + /// that pick deterministic. + /// + /// This is how a later stage gets the frontend's reading of a type it holds + /// only as syntax, without lowering it a second time. + pub fn type_refs(&self) -> impl Iterator { + self.elements + .iter() + .flat_map(element_type_refs) + .flat_map(TypeRef::walk) + } + + /// Every anonymous const, in stream order — **zero or more**. + /// + /// Not part of the flat API — see [`Guard`] — but ingested with it, and a + /// consumer that re-emits the source must re-emit these too. + pub fn guards(&self) -> impl Iterator { + self.elements.iter().filter_map(|e| match e { + Element::Guard(g) => Some(g), + _ => None, + }) + } + /// Every item the language could not express, with its diagnosis. /// /// Present in the model so a consumer can inspect what a source crate marked @@ -576,6 +614,7 @@ fn resolve_references(elements: &mut [Element]) { Element::Function(f) => &f.origin.location, Element::Type(t) => t.location_rc(), Element::Constant(c) => &c.origin.location, + Element::Guard(g) => &g.origin.location, Element::Unsupported(u) => &u.origin.location, }), ); @@ -588,11 +627,12 @@ fn resolve_references(elements: &mut [Element]) { } } -/// The first type this element names that the flat API does not declare. -fn first_unresolved( - element: &Element, - declared: &std::collections::HashSet, -) -> Option { +/// Every type slot this element writes, outermost only — a parameter, a return, +/// a field, a constant's type. +/// +/// The one place the slots are enumerated, so a new element shape is taught to +/// every consumer at once instead of drifting between them. +fn element_type_refs(element: &Element) -> Vec<&TypeRef> { let mut refs: Vec<&TypeRef> = Vec::new(); match element { Element::Function(f) => { @@ -606,11 +646,24 @@ fn first_unresolved( .iter() .flat_map(|a| a.fields.iter().map(|f| &f.ty)), ), - // An enum names nothing, an opaque hides what it names, and an + // An enum names nothing, an extern hides what it names, a guard is + // emitted verbatim so its types are the consumer's business, and an // unsupported item already has a diagnosis worth keeping. - Element::Type(Type::Enum(_) | Type::Extern(_)) | Element::Unsupported(_) => {} + Element::Type(Type::Enum(_) | Type::Extern(_)) + | Element::Guard(_) + | Element::Unsupported(_) => {} } - refs.into_iter().find_map(|r| r.first_unresolved(declared)) + refs +} + +/// The first type this element names that the flat API does not declare. +fn first_unresolved( + element: &Element, + declared: &std::collections::HashSet, +) -> Option { + element_type_refs(element) + .into_iter() + .find_map(|r| r.first_unresolved(declared)) } /// If `ty` is `impl Fn(T1, T2, ...) + Send + Sync + 'static`, return the `Fn` @@ -889,10 +942,12 @@ fn lower_item(item: syn::Item, loc: SourceLocation, consts: &ConstIndex) -> Elem })) } }, - // Including the unnamed `const _` each source injects as its feature - // guard: it is a const, so it is one here. `Element::name` returns - // `None` for `_`, which is what keeps several sources' guards from - // colliding in the flat namespace. + // An unnamed const is a `Guard`, not a constant: nothing can name it, so + // it is not part of the API, and several sources' guards coexist because + // none of them has an address to collide on. + syn::Item::Const(c) if c.ident == "_" => Element::Guard(Guard { + origin: Origin::new(c, at), + }), syn::Item::Const(c) => match lower_type(&c.ty, consts, &at) { Ok(ty) => Element::Constant(Constant { name: c.ident.clone(), diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index bac95bbf..c0c59993 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -866,11 +866,12 @@ fn consts_carry_their_type_and_value() { assert_eq!(tokens(&c.origin.syntax.expr), "4"); } -/// An unnamed `const _` — each source's injected feature guard — is a const -/// like any other, and simply has no address, so several sources may each carry -/// one without colliding in the flat namespace. +/// An unnamed const is a `Guard`, not a `Constant`: nothing can name it, so it +/// is not part of the API. Several coexist because none has an address to +/// collide on — which is what lets a binding ingest two source crates, each +/// injecting its own feature check. #[test] -fn an_unnamed_const_is_a_const_without_an_address() { +fn an_unnamed_const_is_a_guard() { let elements = parse(vec![ syn::parse_quote!( const _: () = (); @@ -879,8 +880,11 @@ fn an_unnamed_const_is_a_const_without_an_address() { const _: () = (); ), ]); - assert!(elements.iter().all(|e| matches!(e, Element::Constant(_)))); + assert!(elements.iter().all(|e| matches!(e, Element::Guard(_)))); assert!(elements.iter().all(|e| e.name().is_none())); + // A guard writes no type slot, so a consumer walking the API never reaches + // one — the reason it carries no `TypeRef`. + assert!(!elements.iter().any(|e| matches!(e, Element::Constant(_)))); } /// An item kind the language does not model is diagnosed, not carried: a diff --git a/prebindgen/src/api/core/flat/tests/mod.rs b/prebindgen/src/api/core/flat/tests/mod.rs index 39dfaa0f..ecf9e2a4 100644 --- a/prebindgen/src/api/core/flat/tests/mod.rs +++ b/prebindgen/src/api/core/flat/tests/mod.rs @@ -157,6 +157,7 @@ fn describe(e: &Element) -> String { Element::Function(f) => format!("function `{}`", f.name), Element::Type(t) => describe_type(t), Element::Constant(c) => format!("constant `{}`", c.name), + Element::Guard(_) => "guard".to_string(), Element::Unsupported(u) => match &u.name { Some(name) => format!("unsupported `{name}` ({})", u.error), None => format!("unsupported ({})", u.error), diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 00b27725..102022bf 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -84,6 +84,37 @@ impl TypeRef { } } + /// This type and every type reachable inside it, outermost first. + /// + /// The nested positions are real [`TypeRef`]s carrying their own spelling and + /// origin, so a consumer that indexes types finds `Foo` from `Vec` with + /// the classification already made rather than a sub-path to re-read. + /// + /// A [`Named`](TypeKind::Named)'s generic arguments are **not** among them: + /// [`TypeId`] keeps a name and nothing else, so `MyBox` reaches no `Foo` + /// here. The full spelling is in [`Self::origin`] for whoever needs it. + pub fn walk(&self) -> Vec<&TypeRef> { + let mut out = Vec::new(); + self.collect_refs(&mut out); + out + } + + fn collect_refs<'a>(&'a self, out: &mut Vec<&'a TypeRef>) { + out.push(self); + match &self.kind { + TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => { + t.collect_refs(out) + } + TypeKind::Array { elem, .. } => elem.collect_refs(out), + TypeKind::Fallible { ok, err } => { + ok.collect_refs(out); + err.collect_refs(out); + } + TypeKind::Callback { args } => args.iter().for_each(|t| t.collect_refs(out)), + TypeKind::Named { .. } | TypeKind::Scalar(_) | TypeKind::Str | TypeKind::Unit => {} + } + } + fn collect_extents<'a>(&'a self, out: &mut Vec<&'a ArrayExtent>) { match &self.kind { TypeKind::Array { elem, extent } => { diff --git a/prebindgen/src/api/core/mod.rs b/prebindgen/src/api/core/mod.rs index ec7f8862..9b975338 100644 --- a/prebindgen/src/api/core/mod.rs +++ b/prebindgen/src/api/core/mod.rs @@ -14,7 +14,7 @@ //! until no unresolved type advances, then propagates `ConverterImpl::subs` //! from required roots. //! 5. [`registry::Registry::write_rust`] emits adapter prerequisites, -//! converters, per-item wrapper Rust, and passthrough items. +//! converters, per-item wrapper Rust, and verbatim anonymous consts. //! //! Secondary artifacts such as C headers or Kotlin sources are produced by the //! language adapter after the Rust registry is resolved. @@ -39,7 +39,7 @@ pub use self::{ niches::{NicheSlot, Niches}, prebindgen::{const_path_alias, ConverterImpl, Prebindgen, Stage}, registry::{ - Direction, Generation, Registry, RegistryBuilder, ScanError, TypeEntry, TypeKey, - WriteRustError, + Direction, Generation, Registry, RegistryBuilder, ScanError, TypeCell, TypeEntry, TypeKey, + TypeSubject, WriteRustError, }, }; diff --git a/prebindgen/src/api/core/prebindgen.rs b/prebindgen/src/api/core/prebindgen.rs index 8f78c831..84effb99 100644 --- a/prebindgen/src/api/core/prebindgen.rs +++ b/prebindgen/src/api/core/prebindgen.rs @@ -504,14 +504,16 @@ pub trait Prebindgen { /// (see [`const_path_alias`]) when [`Self::source_module`] is available — /// initializer tokens are never copied, so a const whose initializer /// references source-crate internals stays valid in the generated file. - /// Unnamed `const _` items (self-contained infrastructure guards, e.g. - /// the injected `konst::assertc_eq!` feature check) and adapters without - /// a source module pass through verbatim. + /// An adapter without a source module passes the const through verbatim. + /// + /// A const reaching here is always named: prebindgen's own injected feature + /// checks are [`Guard`](crate::api::core::flat::Guard)s, not consts, so this + /// never has to recognise one. fn on_const(&self, c: &syn::ItemConst, _registry: &Registry) -> TokenStream { use quote::ToTokens; match self.source_module() { - Some(m) if c.ident != "_" => const_path_alias(c, m), - _ => c.to_token_stream(), + Some(m) => const_path_alias(c, m), + None => c.to_token_stream(), } } diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs index 712e1ee0..baf063e1 100644 --- a/prebindgen/src/api/core/registry.rs +++ b/prebindgen/src/api/core/registry.rs @@ -4,8 +4,8 @@ //! * Item maps (`functions`, `structs`, `enums`, `consts`) indexed by ident. //! Duplicate names across kinds OR within a kind are an error — prebindgen //! items live in one flat namespace. -//! * `passthrough` — items that aren't function/struct/enum/const (use, mod, -//! type alias, macro_rules) emitted verbatim. +//! * `guards` — anonymous consts, emitted verbatim. Not API: having no name, +//! they cannot be declared, so they are neither gated nor addressable. //! * `input_types` / `output_types` — direction-specific type tables. Each //! scanned type maps to either a resolved [`TypeEntry`] or an unresolved cell //! that the fixed-point resolver can retry. @@ -137,6 +137,66 @@ impl fmt::Display for TypeKey { } } +/// What a type-table key names. +/// +/// Two populations, and saying which is which is what keeps one origin per cell: +/// a type the flat API contains **is** a [`TypeRef`](crate::api::core::flat::TypeRef), reused whole, so its +/// classification and its source location are already there. +#[derive(Clone, Debug)] +pub enum TypeSubject { + /// A type the flat API contains — the frontend's own reading, unmodified. + Source(crate::api::core::flat::TypeRef), + /// A type only the binding authored: a declared wire type with no + /// `#[prebindgen]` item behind it, an [`unfold`](crate::api::core::unfold) + /// leaf. It has no reading and no source location — a fact about it, rather + /// than information that went missing. + Adapter(syn::Type), +} + +impl TypeSubject { + /// Where the source wrote this type, or `None` when no source did. + pub fn location(&self) -> Option<&SourceLocation> { + match self { + TypeSubject::Source(t) => Some(&t.origin.location), + TypeSubject::Adapter(_) => None, + } + } + + /// The frontend's classification, or `None` for an adapter-authored type. + pub fn kind(&self) -> Option<&crate::api::core::flat::TypeKind> { + match self { + TypeSubject::Source(t) => Some(&t.kind), + TypeSubject::Adapter(_) => None, + } + } + + /// The type as Rust must spell it, either way. + pub fn syntax(&self) -> &syn::Type { + match self { + TypeSubject::Source(t) => &t.origin.syntax, + TypeSubject::Adapter(ty) => ty, + } + } +} + +/// One type-table cell: what the key names, and the adapter's answer for it. +pub struct TypeCell { + /// The type itself, as the frontend reads it when it can. + pub subject: TypeSubject, + /// The binding asks for this cell **directly** — a declared fn's signature, a + /// declared type, an `unfold` leaf — as opposed to reaching it through some + /// converter's [`TypeEntry::subs`]. + /// + /// A scan fact. Whether a converter is *needed* here is reachability from + /// these roots, which [`crate::api::core::resolve`] derives rather than + /// stores: the scan deliberately over-approximates the table (every nested + /// position, every struct in both directions), so the roots are what say + /// which of it has to work. + pub root: bool, + /// The adapter's converter, once resolved. + pub entry: Option>, +} + /// Per-cell registry entry. #[derive(Clone)] pub struct TypeEntry { @@ -159,10 +219,6 @@ pub struct TypeEntry { /// terminal converters; populated by wrapper converters. Used by the /// post-resolution propagation pass. pub subs: Vec, - /// Initially true for types that appear directly in a `#[prebindgen]` fn - /// signature; false for sub-positions. Promoted true by the propagation - /// pass for any type reachable via `subs` from another required type. - pub required: bool, /// Wire bit-patterns this converter never produces / always rejects. /// Wrappers (`Option<_>`, sum-typed enums) carve from this set for /// their own discriminants. See [`Niches`] for the cascade model. @@ -241,8 +297,11 @@ pub struct Registry { pub structs: HashMap, pub enums: HashMap, pub consts: HashMap, - /// Anything else (use, mod, type alias, macro_rules) — passed through. - pub passthrough: Vec<(syn::Item, SourceLocation)>, + /// Anonymous consts, in stream order, re-emitted verbatim — **zero or + /// more**. Not API: having no name, they cannot be declared, so they are + /// neither gated nor addressable. See + /// [`Guard`](crate::api::core::flat::Guard) for what produces them. + pub guards: Vec, /// Origin crate name of each named item (fn/struct/enum/const), /// recorded by [`Self::from_items`] from each item's @@ -262,21 +321,20 @@ pub struct Registry { /// back to `crate`. pub(crate) source_modules: Vec, - /// Type tables, one per direction. Each scanned type maps to its resolved - /// [`TypeEntry`] (`Some`) or stays unresolved (`None`) until the structural - /// resolver fills it. - pub input_types: HashMap>>, - pub output_types: HashMap>>, - - /// First-seen source location for each type key. Used in error messages - /// to point the user at where a required-but-unresolved type came from. - pub type_locations: HashMap, + /// Type tables, one per direction. Each scanned type gets a [`TypeCell`] + /// holding what the key names, whether the binding asks for it directly, and + /// the resolved [`TypeEntry`] once the structural resolver fills it. + pub input_types: HashMap>, + pub output_types: HashMap>, - /// Sidecar tracking which keys were registered as top-level fn-signature - /// types, separate from per-entry `required` (which the resolver flips - /// into `TypeEntry::required` once an entry is filled). - pub required_inputs_scan: HashSet, - pub required_outputs_scan: HashSet, + /// The frontend's reading of every type the flat API mentions, keyed the way + /// the tables are, so a cell gets its [`TypeSubject::Source`] by lookup + /// instead of by lowering the type a second time. + /// + /// Built once from [`Self::flat`] before anything is scanned. A type + /// mentioned in several places keeps the first mention in **element order** — + /// a property of the model, not of the order items were fed in. + type_refs: HashMap, /// Resolved constructor-expansion plans, keyed by `(function, parameter)`. /// Filled by [`crate::api::core::expand::apply`] before resolution; read @@ -331,14 +389,12 @@ impl Registry { structs: HashMap::new(), enums: HashMap::new(), consts: HashMap::new(), - passthrough: Vec::new(), + guards: Vec::new(), item_origins: HashMap::new(), source_modules: Vec::new(), input_types: Default::default(), output_types: Default::default(), - type_locations: HashMap::new(), - required_inputs_scan: HashSet::new(), - required_outputs_scan: HashSet::new(), + type_refs: HashMap::new(), expansion_plans: HashMap::new(), unfold_plans: HashMap::new(), error_plans: HashMap::new(), @@ -769,11 +825,15 @@ impl Registry { /// override could only fix one module). /// /// This step only populates the item maps (`functions`, `structs`, - /// `enums`, `consts`, `passthrough`). Signature/body scanning that + /// `enums`, `consts`, `guards`). Signature/body scanning that /// drives type-resolution requirements happens later, in /// [`Self::scan_declared`], and is gated on what the language adapter - /// has explicitly declared. Items that are never declared remain in - /// the registry but never drive type resolution and never emit. + /// has explicitly declared. An **API** item that is never declared remains + /// in the registry but never drives type resolution and never emits. + /// + /// `guards` is the exception, and it is not one of the API maps: an + /// anonymous const has no name to declare, so it is outside the gate + /// entirely and always emits. pub fn from_items(items: I) -> Result where I: IntoIterator, @@ -784,7 +844,7 @@ impl Registry { Self::from_flat(flat) } - /// Index a parsed [`Flat`] model. + /// Index a parsed [`Flat`](crate::api::core::flat::Flat) model. /// /// The registry is a **projection** of the model, not a second reading of the /// source: `Flat` decided what every item means, and this arranges those @@ -813,6 +873,17 @@ impl Registry { } let mut registry = Registry::empty(); + // Every type the model mentions, indexed the way the type tables are. + // Read up front, from the whole model: which mention of a repeated type + // wins is then decided by element order rather than by when a scan + // happened to reach it. + for ty in flat.type_refs() { + registry + .type_refs + .entry(TypeKey::from_type(&ty.origin.syntax)) + .or_insert_with(|| ty.clone()); + } + // First-seen order, which is what makes the first entry the default // module. Derived from the elements rather than stored twice. for element in flat.elements() { @@ -852,16 +923,9 @@ impl Registry { (t.origin.syntax.clone(), element.location().clone()), ); } - // An unnamed `const _` is each source's injected `konst` feature - // guard: not addressable, re-emitted verbatim. That is the whole - // of `passthrough` now — the proc-macro refuses to mark a `use`, - // `mod` or `macro_rules!`, so nothing else ever reached it. - Element::Constant(c) if named.is_none() => { - registry.passthrough.push(( - syn::Item::Const(c.origin.syntax.clone()), - element.location().clone(), - )); - } + // An anonymous const, re-emitted verbatim. No name means nothing + // can declare it, which is why it is in none of the API maps. + Element::Guard(g) => registry.guards.push(g.clone()), Element::Constant(c) => { registry.consts.insert( c.name.clone(), @@ -1030,8 +1094,8 @@ impl Registry { // Scan declared functions. for ident in &declared.functions { - if let Some((item_fn, loc)) = self.functions.get(ident).cloned() { - self.scan_fn_signature(&item_fn, &loc)?; + if let Some((item_fn, _)) = self.functions.get(ident).cloned() { + self.scan_fn_signature(&item_fn)?; } else { missing.push(("function", ident.to_string())); } @@ -1061,8 +1125,8 @@ impl Registry { // so the type is required in the output direction only. if let Some(decl_consts) = &declared.consts { for ident in decl_consts { - if let Some((item_const, loc)) = self.consts.get(ident).cloned() { - self.ensure_entry(Direction::Output, &item_const.ty, true, &loc); + if let Some((item_const, _)) = self.consts.get(ident).cloned() { + self.ensure_entry(Direction::Output, &item_const.ty, true); } else { missing.push(("constant", ident.to_string())); } @@ -1085,7 +1149,7 @@ impl Registry { // Adapter-required extra output types — synthesized values with no // `#[prebindgen]` item behind them (e.g. expression constants). for ty in &declared.required_output_types { - self.ensure_entry(Direction::Output, ty, true, &SourceLocation::default()); + self.ensure_entry(Direction::Output, ty, true); } // Scan declared types. @@ -1093,15 +1157,15 @@ impl Registry { let ty = key.to_type(); let mut matched = false; if let Some(ident) = bare_path_ident(&ty) { - if let Some((s, loc)) = self.structs.get(&ident).cloned() { - self.scan_struct(&s, &loc)?; - self.ensure_entry(Direction::Input, &ty, true, &loc); - self.ensure_entry(Direction::Output, &ty, true, &loc); + if let Some((s, _)) = self.structs.get(&ident).cloned() { + self.scan_struct(&s)?; + self.ensure_entry(Direction::Input, &ty, true); + self.ensure_entry(Direction::Output, &ty, true); matched = true; - } else if let Some((e, loc)) = self.enums.get(&ident).cloned() { - self.scan_enum(&e, &loc)?; - self.ensure_entry(Direction::Input, &ty, true, &loc); - self.ensure_entry(Direction::Output, &ty, true, &loc); + } else if let Some((e, _)) = self.enums.get(&ident).cloned() { + self.scan_enum(&e)?; + self.ensure_entry(Direction::Input, &ty, true); + self.ensure_entry(Direction::Output, &ty, true); matched = true; } } @@ -1110,9 +1174,8 @@ impl Registry { // `ptr_class(ZKeyExpr<'static>)` on a re-exported // foreign type). Still mark required so the resolver // tries to produce a converter for it. - let loc = self.type_locations.get(key).cloned().unwrap_or_default(); - self.ensure_entry(Direction::Input, &ty, true, &loc); - self.ensure_entry(Direction::Output, &ty, true, &loc); + self.ensure_entry(Direction::Input, &ty, true); + self.ensure_entry(Direction::Output, &ty, true); } } @@ -1182,12 +1245,8 @@ impl Registry { let mut skipped_consts: Vec = self .consts .keys() - // Unnamed consts (`const _`, e.g. the injected feature - // guard) are infrastructure: not declarable, always emitted - // verbatim — never a skip. .filter(|k| { - *k != "_" - && !decl_consts.contains(*k) + !decl_consts.contains(*k) && !declared.ignored_consts.contains(*k) && !pred_ignored(&k.to_string()) }) @@ -1205,16 +1264,8 @@ impl Registry { Ok(()) } - /// True iff the key was scanned as a top-level fn-signature input type. - pub fn is_required_input_at_scan(&self, key: &TypeKey) -> bool { - self.required_inputs_scan.contains(key) - } - pub fn is_required_output_at_scan(&self, key: &TypeKey) -> bool { - self.required_outputs_scan.contains(key) - } - /// Direction-indexed read access to the type-resolution tables. - pub(crate) fn type_table(&self, dir: Direction) -> &HashMap>> { + pub(crate) fn type_table(&self, dir: Direction) -> &HashMap> { match dir { Direction::Input => &self.input_types, Direction::Output => &self.output_types, @@ -1222,10 +1273,7 @@ impl Registry { } /// Direction-indexed mutable access to the type-resolution tables. - pub(crate) fn type_table_mut( - &mut self, - dir: Direction, - ) -> &mut HashMap>> { + pub(crate) fn type_table_mut(&mut self, dir: Direction) -> &mut HashMap> { match dir { Direction::Input => &mut self.input_types, Direction::Output => &mut self.output_types, @@ -1238,30 +1286,30 @@ impl Registry { /// its wire form. pub fn input_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { let key = TypeKey::from_type(ty); - self.type_table(Direction::Input).get(&key)?.as_ref() + self.type_table(Direction::Input).get(&key)?.entry.as_ref() } /// Look up the resolved output entry for `ty`. See [`Self::input_entry`]. pub fn output_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { let key = TypeKey::from_type(ty); - self.type_table(Direction::Output).get(&key)?.as_ref() + self.type_table(Direction::Output).get(&key)?.entry.as_ref() } /// Register `ty` (and its nested positions) as a required **input** so /// the resolver produces a converter for it. Used by /// [`crate::api::core::expand`] to pull in the leaf types a fold needs. - pub(crate) fn require_input(&mut self, ty: &syn::Type, loc: &SourceLocation) { + pub(crate) fn require_input(&mut self, ty: &syn::Type) { // Leaf/expansion types are concrete (no disallowed `impl Trait`), so // the recursive registration cannot fail here. - let _ = self.register_type_recursive(Direction::Input, ty, true, loc); + let _ = self.register_type_recursive(Direction::Input, ty, true); } /// Register `ty` (and its nested positions) as a required **output** so the /// resolver produces a converter for it. The output-side peer of /// [`Self::require_input`]; used by [`crate::api::core::unfold`] to pull in /// the leaf types a decomposition delivers. - pub(crate) fn require_output(&mut self, ty: &syn::Type, loc: &SourceLocation) { - let _ = self.register_type_recursive(Direction::Output, ty, true, loc); + pub(crate) fn require_output(&mut self, ty: &syn::Type) { + let _ = self.register_type_recursive(Direction::Output, ty, true); } /// Drop `ty` from the required-output scan set. The type's table entry is @@ -1274,7 +1322,7 @@ impl Registry { /// `Vec` it cannot resolve at all (a `jlong` wire is not /// JObject-shaped), so requiring it would wrongly fail resolution. pub(crate) fn unrequire_output(&mut self, ty: &syn::Type) { - self.required_outputs_scan.remove(&TypeKey::from_type(ty)); + self.clear_root(Direction::Output, ty); } /// Drop `ty` from the required-input scan set — the input-side peer of @@ -1284,14 +1332,19 @@ impl Registry { /// converter is genuinely not needed (and for an undeclared type cannot /// resolve at all). pub(crate) fn unrequire_input(&mut self, ty: &syn::Type) { - self.required_inputs_scan.remove(&TypeKey::from_type(ty)); + self.clear_root(Direction::Input, ty); } - fn scan_fn_signature( - &mut self, - f: &syn::ItemFn, - loc: &SourceLocation, - ) -> Result<(), ScanError> { + /// Stop treating `ty` as a root. The cell stays, so the resolver still fills + /// it if it can — only the demand that it *must* resolve is dropped. + fn clear_root(&mut self, dir: Direction, ty: &syn::Type) { + let key = TypeKey::from_type(ty); + if let Some(cell) = self.type_table_mut(dir).get_mut(&key) { + cell.root = false; + } + } + + fn scan_fn_signature(&mut self, f: &syn::ItemFn) -> Result<(), ScanError> { // Mechanical: register every fn-signature type as the user wrote it. // No semantic transformations (no &T→T strip, no ZResult→T strip, // no skip for () / ZResult<()>). The adapter handles structural @@ -1306,7 +1359,7 @@ impl Registry { match input { syn::FnArg::Receiver(_) => continue, syn::FnArg::Typed(pt) => { - self.register_type_recursive(Direction::Input, &pt.ty, true, loc)?; + self.register_type_recursive(Direction::Input, &pt.ty, true)?; } } } @@ -1314,51 +1367,50 @@ impl Registry { syn::ReturnType::Default => syn::parse_quote!(()), syn::ReturnType::Type(_, ty) => (**ty).clone(), }; - self.register_type_recursive(Direction::Output, &ret_ty, true, loc)?; + self.register_type_recursive(Direction::Output, &ret_ty, true)?; Ok(()) } - fn scan_struct(&mut self, s: &syn::ItemStruct, loc: &SourceLocation) -> Result<(), ScanError> { + fn scan_struct(&mut self, s: &syn::ItemStruct) -> Result<(), ScanError> { // The struct itself can appear in either direction. let ty: syn::Type = crate::api::core::types_util::type_from_ident(&s.ident); - self.ensure_entry(Direction::Input, &ty, false, loc); - self.ensure_entry(Direction::Output, &ty, false, loc); + self.ensure_entry(Direction::Input, &ty, false); + self.ensure_entry(Direction::Output, &ty, false); if let syn::Fields::Named(named) = &s.fields { for field in &named.named { - self.register_type_recursive(Direction::Input, &field.ty, false, loc)?; - self.register_type_recursive(Direction::Output, &field.ty, false, loc)?; + self.register_type_recursive(Direction::Input, &field.ty, false)?; + self.register_type_recursive(Direction::Output, &field.ty, false)?; } } Ok(()) } - fn scan_enum(&mut self, e: &syn::ItemEnum, loc: &SourceLocation) -> Result<(), ScanError> { + fn scan_enum(&mut self, e: &syn::ItemEnum) -> Result<(), ScanError> { let ty: syn::Type = crate::api::core::types_util::type_from_ident(&e.ident); - self.ensure_entry(Direction::Input, &ty, false, loc); - self.ensure_entry(Direction::Output, &ty, false, loc); + self.ensure_entry(Direction::Input, &ty, false); + self.ensure_entry(Direction::Output, &ty, false); for variant in &e.variants { for field in &variant.fields { - self.register_type_recursive(Direction::Input, &field.ty, false, loc)?; - self.register_type_recursive(Direction::Output, &field.ty, false, loc)?; + self.register_type_recursive(Direction::Input, &field.ty, false)?; + self.register_type_recursive(Direction::Output, &field.ty, false)?; } } Ok(()) } - /// Register `ty` as an entry in the given direction, then recurse into - /// every nested position. `top_required` applies only to `ty` itself; - /// nested positions are always recorded as not-required. + /// Register `ty` as a cell in the given direction, then recurse into every + /// nested position. `root` applies only to `ty` itself — a nested position is + /// never something the binding asked for directly. fn register_type_recursive( &mut self, dir: Direction, ty: &syn::Type, - top_required: bool, - loc: &SourceLocation, + root: bool, ) -> Result<(), ScanError> { let mut visited: HashSet = HashSet::new(); - self.register_type_inner(dir, ty, top_required, loc, &mut visited) + self.register_type_inner(dir, ty, root, &mut visited) } fn register_type_inner( @@ -1366,7 +1418,6 @@ impl Registry { dir: Direction, ty: &syn::Type, is_top: bool, - loc: &SourceLocation, visited: &mut HashSet, ) -> Result<(), ScanError> { // A disallowed `impl Trait` cannot reach here: every fn whose signature @@ -1379,33 +1430,35 @@ impl Registry { return Ok(()); // cycle guard } - self.ensure_entry(dir, ty, is_top, loc); + self.ensure_entry(dir, ty, is_top); for (child_dir, sub) in self.immediate_edges(dir, ty) { - self.register_type_inner(child_dir, &sub, false, loc, visited)?; + self.register_type_inner(child_dir, &sub, false, visited)?; } Ok(()) } - fn ensure_entry( - &mut self, - dir: Direction, - ty: &syn::Type, - required: bool, - loc: &SourceLocation, - ) { + /// Create the cell for `ty` in `dir` if it has none, and mark it a root when + /// the binding asked for it directly. + /// + /// The one place a cell is born, which is what lets the subject be decided + /// once: the model's reading if the flat API mentions this type, an + /// adapter-authored type otherwise. + fn ensure_entry(&mut self, dir: Direction, ty: &syn::Type, root: bool) { let key = TypeKey::from_type(ty); - let table = self.type_table_mut(dir); - table.entry(key.clone()).or_insert(None); - if required { - match dir { - Direction::Input => self.required_inputs_scan.insert(key.clone()), - Direction::Output => self.required_outputs_scan.insert(key.clone()), - }; - } - self.type_locations + let subject = match self.type_refs.get(&key) { + Some(t) => TypeSubject::Source(t.clone()), + None => TypeSubject::Adapter(key.to_type()), + }; + let cell = self + .type_table_mut(dir) .entry(key) - .or_insert_with(|| loc.clone()); + .or_insert_with(|| TypeCell { + subject, + root: false, + entry: None, + }); + cell.root |= root; } /// Enumerate the immediate type-graph edges out of `(dir, ty)`: @@ -1430,20 +1483,24 @@ impl Registry { for sub in positions { out.push((child_dir, sub)); } + // A declared type's own fields, read off the element rather than off its + // `syn::Fields`: a positional field is an ordinary `Field` there, so the + // named-only asymmetry the syntax walk had does not arise. An `Enum` has + // no fields and an `Extern` declares none, which is what makes both + // contribute nothing here. if let Some(name) = bare_path_ident(ty) { - if let Some((s, _)) = self.structs.get(&name) { - if let syn::Fields::Named(named) = &s.fields { - for field in &named.named { - out.push((dir, field.ty.clone())); - } - } - } - if let Some((e, _)) = self.enums.get(&name) { - for variant in &e.variants { - for field in &variant.fields { - out.push((dir, field.ty.clone())); - } - } + use crate::api::core::flat::{Field, Type}; + let fields: Vec<&Field> = match self.flat.declared_type(&name.to_string()) { + Some(Type::Struct(s)) => s.fields.iter().collect(), + Some(Type::Variant(v)) => v + .alternatives + .iter() + .flat_map(|a| a.fields.iter()) + .collect(), + Some(Type::Enum(_) | Type::Extern(_)) | None => Vec::new(), + }; + for field in fields { + out.push((dir, field.ty.origin.syntax.clone())); } } out @@ -1573,10 +1630,9 @@ impl Registry { // Adapter-derived extra requirements (registry-aware — e.g. the // other-side types of `convert!` conversion fns, per direction). for (dir, ty) in ext.extra_required_types(self) { - let loc = SourceLocation::default(); match dir { - Direction::Input => self.require_input(&ty, &loc), - Direction::Output => self.require_output(&ty, &loc), + Direction::Input => self.require_input(&ty), + Direction::Output => self.require_output(&ty), } } // Boundary-only types: every crossing is now covered by a plan (fold diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 1110b79d..bf0d7495 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -93,8 +93,8 @@ fn scan_declared_empty_ext_marks_nothing_required() { let mut reg: Registry<()> = Registry::from_items(items).unwrap(); let ext = StubExt::default(); reg.scan_declared(&ext).expect("empty ext = no scan"); - assert!(reg.required_inputs_scan.is_empty()); - assert!(reg.required_outputs_scan.is_empty()); + assert!(!reg.input_types.values().any(|c| c.root)); + assert!(!reg.output_types.values().any(|c| c.root)); } #[test] @@ -107,18 +107,14 @@ fn scan_declared_marks_types_required_only_for_declared_fns() { let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("a").unwrap()); reg.scan_declared(&ext).unwrap(); - assert!(reg - .required_inputs_scan - .contains(&TypeKey::parse("u64").expect("test type"))); - assert!(reg - .required_outputs_scan - .contains(&TypeKey::parse("u64").expect("test type"))); - assert!(!reg - .required_inputs_scan - .contains(&TypeKey::parse("u32").expect("test type"))); - assert!(!reg - .required_outputs_scan - .contains(&TypeKey::parse("u32").expect("test type"))); + let is_root = |t: &HashMap>, k: &str| { + t.get(&TypeKey::parse(k).expect("test type")) + .is_some_and(|c| c.root) + }; + assert!(is_root(®.input_types, "u64")); + assert!(is_root(®.output_types, "u64")); + assert!(!is_root(®.input_types, "u32")); + assert!(!is_root(®.output_types, "u32")); } #[test] @@ -239,8 +235,8 @@ fn scan_declared_accepts_ignore_predicates() { ext.ignored_name_predicates .push(std::sync::Arc::new(|n: &str| n.starts_with("nothing_"))); reg.scan_declared(&ext).expect("predicates must scan clean"); - // Nothing was declared, so nothing became required. - assert!(reg.required_inputs_scan.is_empty()); + // Nothing was declared, so nothing became a root. + assert!(!reg.input_types.values().any(|c| c.root)); } #[test] @@ -274,7 +270,6 @@ fn type_entry_helpers_expose_converter_chain_contract() { TypeKey::parse("Rust").expect("test type"), TypeKey::parse("Mid").expect("test type"), ], - required: true, niches: Niches::empty(), metadata: (), }; @@ -527,16 +522,13 @@ fn qualified_signature_matches_bare_declaration() { ext.types .insert(TypeKey::parse("Thing").expect("test type")); reg.scan_declared(&ext).unwrap(); - assert!(reg - .required_inputs_scan - .contains(&TypeKey::parse("&Thing").expect("test type"))); - assert!(reg - .required_outputs_scan - .contains(&TypeKey::parse("Vec").expect("test type"))); + assert!(reg.input_types[&TypeKey::parse("&Thing").expect("test type")].root); + assert!(reg.output_types[&TypeKey::parse("Vec").expect("test type")].root); // No spelling-variant duplicate cells survive anywhere. - let no_paths = |set: &HashSet| !set.iter().any(|k| k.as_str().contains("::")); - assert!(no_paths(®.required_inputs_scan)); - assert!(no_paths(®.required_outputs_scan)); + let no_paths = + |t: &HashMap>| !t.keys().any(|k| k.as_str().contains("::")); + assert!(no_paths(®.input_types)); + assert!(no_paths(®.output_types)); } #[test] @@ -562,12 +554,8 @@ fn multi_source_rename_cross_reference_normalizes() { ext.types .insert(TypeKey::parse("TypeB").expect("test type")); reg.scan_declared(&ext).unwrap(); - assert!(reg - .required_inputs_scan - .contains(&TypeKey::parse("&TypeA").expect("test type"))); - assert!(reg - .required_outputs_scan - .contains(&TypeKey::parse("TypeB").expect("test type"))); + assert!(reg.input_types[&TypeKey::parse("&TypeA").expect("test type")].root); + assert!(reg.output_types[&TypeKey::parse("TypeB").expect("test type")].root); } #[test] @@ -605,8 +593,8 @@ fn foreign_qualified_declared_type_stays_supported() { ext.types.insert(foreign.clone()); reg.scan_declared(&ext) .expect("foreign qualified declaration is supported"); - assert!(reg.required_inputs_scan.contains(&foreign)); - assert!(reg.required_outputs_scan.contains(&foreign)); + assert!(reg.input_types[&foreign].root); + assert!(reg.output_types[&foreign].root); } // ── The directory-reading builder ────────────────────────────────────── @@ -762,7 +750,65 @@ fn builder_and_from_items_agree() { built.default_module().map(module), streamed.default_module().map(module) ); - assert_eq!(built.passthrough.len(), streamed.passthrough.len()); + assert_eq!(built.guards.len(), streamed.guards.len()); +} + +// ── What a table cell knows about its type ───────────────────────────── + +/// A cell for a type the source wrote carries the **frontend's own** reading: +/// the same classification the element holds, and the item's location. Not a +/// re-derivation — the registry looks the model up rather than lowering twice. +#[test] +fn a_source_type_cell_carries_the_models_typeref() { + use crate::api::core::flat::TypeKind; + + let loc = SourceLocation { + file: "src/lib.rs".into(), + line: 42, + column: 7, + crate_name: Some("myflat".into()), + }; + let item: syn::Item = syn::parse_str("pub fn f(v: Option) -> u64 { v.unwrap() }").unwrap(); + let mut reg: Registry<()> = Registry::from_items([(item, loc.clone())]).unwrap(); + + let mut ext = StubExt::default(); + ext.functions.insert(syn::parse_str("f").unwrap()); + reg.scan_declared(&ext).unwrap(); + + let key = TypeKey::parse("Option").expect("test type"); + let cell = ®.input_types[&key]; + assert!(cell.root, "a top-level parameter is a root"); + assert!( + matches!(cell.subject.kind(), Some(TypeKind::Optional(_))), + "the frontend classified it, so the cell has that classification" + ); + // One location per cell, and it is the model's — not a copy the scan made. + assert_eq!(cell.subject.location(), Some(&loc)); + + // The nested position is in the model too, and is not a root. + let inner = ®.input_types[&TypeKey::parse("u64").expect("test type")]; + assert!(!inner.root); + assert!(matches!(inner.subject.kind(), Some(TypeKind::Scalar(_)))); +} + +/// A type only the binding authored has **no** reading and no location — a fact +/// about it, not information that went missing. Declaring a type the source +/// never mentions is the ordinary way to reach this state. +#[test] +fn an_adapter_authored_type_cell_has_no_source_reading() { + let items = vec![fn_item("fn f(x: u64) -> u64 { x }")]; + let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + + let mut ext = StubExt::default(); + ext.types + .insert(TypeKey::parse("Foreign").expect("test type")); + reg.scan_declared(&ext).unwrap(); + + let cell = ®.input_types[&TypeKey::parse("Foreign").expect("test type")]; + assert!(cell.root, "the binding asked for it directly"); + assert!(matches!(cell.subject, TypeSubject::Adapter(_))); + assert!(cell.subject.kind().is_none()); + assert_eq!(cell.subject.location(), None); } // ── The projection itself ────────────────────────────────────────────── @@ -859,7 +905,11 @@ fn from_flat_projects_each_element_kind() { assert!(reg.enums.contains_key(&id("Sum")), "a sum is an enum here"); assert!(reg.enums.contains_key(&id("Flags"))); assert!(reg.consts.contains_key(&id("K"))); - assert_eq!(reg.passthrough.len(), 2, "one guard per source"); + assert_eq!( + reg.guards.len(), + 2, + "both anonymous consts, in stream order" + ); assert!( !reg.structs.contains_key(&id("Handle")) && !reg.enums.contains_key(&id("Handle")), "an Extern names a type; it declares no body to index" @@ -983,3 +1033,52 @@ fn a_well_formed_binding_local_fn_passes() { reg.resolve(ext) .expect("a grammatical local fn passes, undeclared types and all"); } + +/// A guard is not a const, structurally — so nothing that consumes the const +/// surface has to remember it exists. +/// +/// The three `c.ident == "_"` sentinel checks this replaced had gone **dead** +/// without anyone noticing: once ingestion routed unnamed consts away from +/// `consts`, they guarded a state the pipeline could no longer produce. This is +/// the assertion that would have caught that, and that keeps a future +/// reclassification honest. +#[test] +fn a_guard_never_reaches_the_const_surface() { + let loc = SourceLocation::default(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::parse_quote!( + const _: () = (); + ), + loc.clone(), + ), + ( + syn::parse_quote!( + pub const REAL: u64 = 7; + ), + loc.clone(), + ), + // A second source's guard: several coexist, having no address to collide on. + ( + syn::parse_quote!( + const _: () = (); + ), + loc.clone(), + ), + ]; + let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + + assert_eq!(reg.guards.len(), 2); + assert_eq!(reg.consts.len(), 1); + assert!(reg + .consts + .contains_key(&syn::parse_str::("REAL").unwrap())); + + // An adapter WITH a const mechanism that declares nothing warns about `REAL` + // only: a guard is not undeclared API, it is not API. + let ext = StubExt { + consts: Some(HashSet::new()), + ..Default::default() + }; + reg.scan_declared(&ext).expect("guards are not declarable"); +} diff --git a/prebindgen/src/api/core/resolve.rs b/prebindgen/src/api/core/resolve.rs index 5b2cb8d2..5155790a 100644 --- a/prebindgen/src/api/core/resolve.rs +++ b/prebindgen/src/api/core/resolve.rs @@ -15,11 +15,14 @@ //! args resolve in the opposite direction). New slots only go `None → Some`, so //! the loop terminates. //! -//! After the loop, [`propagate_required`] performs a BFS from the scan-time -//! required entries through `subs` edges; the final invariant is that every -//! `required: true && None` is reported as an error. +//! After the loop, [`required_set`] performs a BFS from the **root** cells — the +//! ones the binding asked for directly — through `subs` edges. It returns the +//! reachable set rather than storing it: needing a converter is a property of the +//! graph, so it is derived once at the end instead of written back into every +//! cell it was computed from. The final invariant is that every reachable-but- +//! unresolved cell is reported as an error. -use std::collections::VecDeque; +use std::collections::{HashSet, VecDeque}; use crate::{ api::core::{ @@ -105,7 +108,6 @@ pub fn resolve( apply_deltas(registry, Direction::Input, deltas_in); apply_deltas(registry, Direction::Output, deltas_out); } - propagate_required(registry); final_invariant_check(registry) } @@ -119,15 +121,11 @@ fn collect_deltas( let mut deltas: Vec<(TypeKey, TypeEntry)> = Vec::new(); let table = registry.type_table(dir); for (key, slot) in table { - if slot.is_some() { + if slot.entry.is_some() { continue; } let key_ty = key.to_type(); - let scan_required = match dir { - Direction::Input => registry.is_required_input_at_scan(key), - Direction::Output => registry.is_required_output_at_scan(key), - }; - if let Some(entry) = resolve_one(ext, &key_ty, dir, scan_required, registry) { + if let Some(entry) = resolve_one(ext, &key_ty, dir, registry) { deltas.push((key.clone(), entry)); } } @@ -144,9 +142,9 @@ fn apply_deltas( ) { let table = registry.type_table_mut(dir); for (key, entry) in deltas { - if let Some(slot) = table.get_mut(&key) { - if slot.is_none() { - *slot = Some(entry); + if let Some(cell) = table.get_mut(&key) { + if cell.entry.is_none() { + cell.entry = Some(entry); } } } @@ -160,7 +158,6 @@ fn resolve_one( ext: &E, key_ty: &syn::Type, dir: Direction, - scan_required: bool, registry: &Registry, ) -> Option> { let conv: Option> = match dir { @@ -183,7 +180,6 @@ fn resolve_one( function: c.function, pre_stages: c.pre_stages, subs: c.subs.iter().map(TypeKey::from_type).collect(), - required: scan_required, niches: c.niches, metadata: c.metadata, }) @@ -193,77 +189,47 @@ fn resolve_one( // Required-flag propagation (BFS from required entries through `subs`) // ────────────────────────────────────────────────────────────────────── -fn propagate_required(registry: &mut Registry) { - // Seed the queue from scan-time required keys plus any `required: true` - // already on resolved entries. +/// The cells a converter must exist for: every root, plus everything reachable +/// from one through a resolved converter's `subs`. +/// +/// Derived, never stored. Needing a converter is a property of the graph, and the +/// graph is not complete until resolution has run — so computing it once here +/// beats maintaining a flag that every edge discovery has to write back. +fn required_set(registry: &Registry) -> HashSet<(Direction, TypeKey)> { + let mut required: HashSet<(Direction, TypeKey)> = HashSet::new(); let mut queue: VecDeque<(Direction, TypeKey)> = VecDeque::new(); - for k in ®istry.required_inputs_scan { - queue.push_back((Direction::Input, k.clone())); - } - for k in ®istry.required_outputs_scan { - queue.push_back((Direction::Output, k.clone())); - } - - while let Some((dir, key)) = queue.pop_front() { - // Mark this entry's `required: true` if it's resolved. - let subs = mark_and_get_subs(registry, dir, &key); - // Subs travel in the same direction as the parent — they're the - // inner converters this body delegates to. - for sub_key in subs { - if !is_required_resolved(registry, dir, &sub_key) { - set_required(registry, dir, &sub_key); - queue.push_back((dir, sub_key)); + for dir in [Direction::Input, Direction::Output] { + for (key, cell) in registry.type_table(dir) { + if cell.root && required.insert((dir, key.clone())) { + queue.push_back((dir, key.clone())); } } } -} -fn mark_and_get_subs(registry: &mut Registry, dir: Direction, key: &TypeKey) -> Vec { - let table = registry.type_table_mut(dir); - match table.get_mut(key) { - Some(Some(entry)) => { - entry.required = true; - entry.subs.clone() - } - _ => vec![], - } -} - -fn is_required_resolved(registry: &Registry, dir: Direction, key: &TypeKey) -> bool { - let table = registry.type_table(dir); - table - .get(key) - .and_then(|slot| slot.as_ref()) - .is_some_and(|e| e.required) -} - -fn set_required(registry: &mut Registry, dir: Direction, key: &TypeKey) { - match dir { - Direction::Input => { - registry.required_inputs_scan.insert(key.clone()); - } - Direction::Output => { - registry.required_outputs_scan.insert(key.clone()); + while let Some((dir, key)) = queue.pop_front() { + // Subs travel in the same direction as the parent — they are the inner + // converters this body delegates to. An unresolved cell has none to give, + // which is why this cannot run before the fixed-point loop. + let Some(entry) = registry + .type_table(dir) + .get(&key) + .and_then(|c| c.entry.as_ref()) + else { + continue; + }; + for sub_key in &entry.subs { + if required.insert((dir, sub_key.clone())) { + queue.push_back((dir, sub_key.clone())); + } } } - let table = registry.type_table_mut(dir); - if let Some(Some(entry)) = table.get_mut(key) { - entry.required = true; - } -} - -fn lookup_slot<'a, M>( - registry: &'a Registry, - dir: Direction, - key: &TypeKey, -) -> Option<&'a Option>> { - registry.type_table(dir).get(key) + required } /// BFS from unresolved required-roots through the type graph, surfacing /// further unresolved entries reachable through struct fields, enum variants, /// generic args, and `impl Fn(...)` args. Stops at resolved nodes — their -/// `subs` were already walked by `propagate_required`, so traversing through +/// `subs` were already walked by `required_set`, so traversing through /// them risks reporting dependents the resolved converter doesn't actually /// need. fn collect_unresolved_descendants( @@ -292,13 +258,13 @@ fn collect_unresolved_descendants( } while let Some((dir, key)) = queue.pop_front() { - match lookup_slot(registry, dir, &key) { - Some(None) => { + match registry.type_table(dir).get(&key) { + Some(cell) if cell.entry.is_none() => { // Registered but unresolved — report it and keep walking. out.push(UnresolvedEntry { key: key.clone(), direction: dir, - location: registry.type_locations.get(&key).cloned(), + location: cell.subject.location().cloned(), }); enqueue_edges_from(dir, &key, &mut queue, seen); } @@ -308,50 +274,36 @@ fn collect_unresolved_descendants( // include registered-but-unresolved types worth flagging. enqueue_edges_from(dir, &key, &mut queue, seen); } - Some(Some(_)) => { - // Resolved — `propagate_required` already walked its `subs`. - // Stop here to avoid spurious reports for descendants the - // resolved converter doesn't need. + Some(_) => { + // Resolved — `required_set` already walked its `subs`. Stop here + // to avoid spurious reports for descendants the resolved + // converter doesn't need. } } } } fn final_invariant_check(registry: &Registry) -> Result<(), ResolveError> { + let required = required_set(registry); let mut entries: Vec = Vec::new(); - let scan_required_input = ®istry.required_inputs_scan; - let scan_required_output = ®istry.required_outputs_scan; let mut unresolved_required_roots: Vec<(Direction, TypeKey)> = Vec::new(); - let mut seen_unresolved: std::collections::HashSet<(Direction, TypeKey)> = - std::collections::HashSet::new(); + let mut seen_unresolved: HashSet<(Direction, TypeKey)> = HashSet::new(); - for (key, slot) in ®istry.input_types { - let needs = match slot { - Some(e) => e.required, - None => scan_required_input.contains(key), - }; - if needs && slot.is_none() { - unresolved_required_roots.push((Direction::Input, key.clone())); - seen_unresolved.insert((Direction::Input, key.clone())); - entries.push(UnresolvedEntry { - key: key.clone(), - direction: Direction::Input, - location: registry.type_locations.get(key).cloned(), - }); - } - } - for (key, slot) in ®istry.output_types { - let needs = match slot { - Some(e) => e.required, - None => scan_required_output.contains(key), - }; - if needs && slot.is_none() { - unresolved_required_roots.push((Direction::Output, key.clone())); - seen_unresolved.insert((Direction::Output, key.clone())); + for dir in [Direction::Input, Direction::Output] { + // Sorted, so a build that fails reports the same list every time. + let mut keys: Vec<&TypeKey> = registry.type_table(dir).keys().collect(); + keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); + for key in keys { + let cell = ®istry.type_table(dir)[key]; + if cell.entry.is_some() || !required.contains(&(dir, key.clone())) { + continue; + } + unresolved_required_roots.push((dir, key.clone())); + seen_unresolved.insert((dir, key.clone())); entries.push(UnresolvedEntry { key: key.clone(), - direction: Direction::Output, - location: registry.type_locations.get(key).cloned(), + direction: dir, + location: cell.subject.location().cloned(), }); } } diff --git a/prebindgen/src/api/core/resolve/tests.rs b/prebindgen/src/api/core/resolve/tests.rs index 38efd77f..947ba6fe 100644 --- a/prebindgen/src/api/core/resolve/tests.rs +++ b/prebindgen/src/api/core/resolve/tests.rs @@ -1,4 +1,5 @@ use super::*; +use crate::api::test_util::cell; /// Regression: when a required type is itself unresolved AND has fields /// that are also unresolved, the diagnostic must list both. Previously @@ -9,29 +10,17 @@ use super::*; fn final_invariant_reports_unresolved_field_of_unresolved_struct() { use crate::api::core::registry::{Registry, TypeKey}; - let mut reg: Registry<()> = Registry::empty(); - - // Index a struct `Outer { inner: ZKeyExpr }` so the BFS can walk - // into its field. `ZKeyExpr` itself stays *unindexed* (the user's - // build.rs forgot to declare it), but it does appear in the type - // tables because scan-recursion would have registered it as a field - // of `Outer`. Simulate the post-scan registry state directly. - let outer_struct: syn::ItemStruct = syn::parse_str("struct Outer { inner: ZKeyExpr }").unwrap(); - reg.structs.insert( - outer_struct.ident.clone(), - (outer_struct, SourceLocation::default()), - ); - - // `Outer` is a required INPUT, unresolved (slot stays `None`). - let outer_key = TypeKey::parse("Outer").expect("test type"); - reg.input_types.insert(outer_key.clone(), None); - reg.required_inputs_scan.insert(outer_key.clone()); + // A struct whose field type the build.rs forgot to declare. Registering + // `Outer` as a root walks into the field through the model, so `ZKeyExpr` + // gets a cell that is NOT a root — exactly the case the BFS is here to + // catch. Driven through the real scan rather than simulated, so the state + // under test is one the pipeline can actually produce. + let mut reg: Registry<()> = + crate::api::test_util::reg_with(&["pub struct Outer { pub inner: ZKeyExpr }"]); + reg.require_input(&syn::parse_quote!(Outer)); - // `ZKeyExpr` is also in the type table (scan recursed into the - // field) but unresolved and NOT marked required at scan time — - // exactly the case the BFS is here to catch. let zke_key = TypeKey::parse("ZKeyExpr").expect("test type"); - reg.input_types.insert(zke_key.clone(), None); + assert!(!reg.input_types[&zke_key].root, "the field is not a root"); let err = final_invariant_check(®).expect_err("must surface unresolved"); let ResolveError::Unresolved { entries } = err; @@ -76,25 +65,29 @@ fn final_invariant_stops_at_resolved_nodes() { let inner_key = TypeKey::parse("Inner").expect("test type"); let unrelated_key = TypeKey::parse("Unrelated").expect("test type"); - reg.input_types.insert(outer_key.clone(), None); - reg.required_inputs_scan.insert(outer_key.clone()); + reg.input_types + .insert(outer_key.clone(), cell(&outer_key, true, None)); reg.input_types.insert( inner_key.clone(), - Some(TypeEntry { - destination: syn::parse_quote!(i64), - function: syn::parse_quote!( - fn __dummy() {} - ), - pre_stages: vec![], - subs: vec![], - required: false, - niches: crate::api::core::niches::Niches::empty(), - metadata: (), - }), + cell( + &inner_key, + false, + Some(TypeEntry { + destination: syn::parse_quote!(i64), + function: syn::parse_quote!( + fn __dummy() {} + ), + pre_stages: vec![], + subs: vec![], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), + ), ); - reg.input_types.insert(unrelated_key.clone(), None); + reg.input_types + .insert(unrelated_key.clone(), cell(&unrelated_key, false, None)); let err = final_invariant_check(®).expect_err("must surface Outer"); let ResolveError::Unresolved { entries } = err; @@ -111,3 +104,49 @@ fn final_invariant_stops_at_resolved_nodes() { ); let _ = Direction::Input; // keep import used } + +/// A type nothing declares directly, reached only through a resolved converter's +/// `subs`, must still fail the build when it has no converter of its own. +/// +/// This is the half of the old `required` flag that is derived rather than +/// stored: `Mid` is not a root, and only `required_set`'s walk through `Outer`'s +/// `subs` makes it something a converter must exist for. +#[test] +fn a_type_reachable_only_through_subs_must_still_resolve() { + use crate::api::{ + core::registry::{Registry, TypeKey}, + test_util::cell, + }; + + let mut reg: Registry<()> = Registry::empty(); + let outer = TypeKey::parse("Outer").expect("test type"); + let mid = TypeKey::parse("Mid").expect("test type"); + + // `Outer` is a root AND resolved — so it is not itself reportable — but its + // converter delegates to `Mid`. + reg.input_types.insert( + outer.clone(), + cell( + &outer, + true, + Some(TypeEntry { + destination: syn::parse_quote!(i64), + function: syn::parse_quote!( + fn __outer() {} + ), + pre_stages: vec![], + subs: vec![mid.clone()], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), + ), + ); + // `Mid` is present, unresolved, and NOT a root. + reg.input_types.insert(mid.clone(), cell(&mid, false, None)); + + let err = final_invariant_check(®).expect_err("Mid must be reported"); + let ResolveError::Unresolved { entries } = err; + let reported: std::collections::HashSet = + entries.iter().map(|e| e.key.to_string()).collect(); + assert_eq!(reported, ["Mid".to_string()].into_iter().collect()); +} diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index 93cc4e09..bcaca847 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -393,7 +393,7 @@ pub fn apply( // (there is no return-value lane in a callback invocation). A type without // a default deconstructor gets no plan and is delivered whole. for func in declared_fns { - let Some((item_fn, loc)) = registry.functions.get(func).cloned() else { + let Some((item_fn, _)) = registry.functions.get(func).cloned() else { continue; }; for input in &item_fn.sig.inputs { @@ -455,7 +455,7 @@ pub fn apply( continue; } for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty, &loc); + registry.require_output(&leaf.out_ty); } registry.callback_arg_plans.insert(key, plan); } @@ -601,7 +601,7 @@ fn wire_fixed_returns( no_converter: bool, ) { for func in declared_fns { - let Some((item_fn, loc)) = registry.functions.get(func).cloned() else { + let Some((item_fn, _)) = registry.functions.get(func).cloned() else { continue; }; let ret = fn_return(&item_fn); @@ -651,7 +651,7 @@ fn wire_fixed_returns( registry.unrequire_output(&core); } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty, &loc); + registry.require_output(&leaf.out_ty); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -682,7 +682,7 @@ fn wire_fixed_callbacks( declared_fns: &std::collections::HashSet, ) -> Result<(), UnfoldError> { for func in declared_fns { - let Some((item_fn, loc)) = registry.functions.get(func).cloned() else { + let Some((item_fn, _)) = registry.functions.get(func).cloned() else { continue; }; for input in &item_fn.sig.inputs { @@ -718,7 +718,7 @@ fn wire_fixed_callbacks( continue; } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty, &loc); + registry.require_output(&leaf.out_ty); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -765,7 +765,7 @@ pub fn apply_leaf_vec_folds( // Is the leading-`&`-peeled `bare` one of the nominated single-leaf elements? let is_nominated = |bare: &syn::Type| elem_keys.contains(&TypeKey::from_type(bare)); for func in declared_fns { - let Some((item_fn, loc)) = registry.functions.get(func).cloned() else { + let Some((item_fn, _)) = registry.functions.get(func).cloned() else { continue; }; // Output position: `Vec` / `Option>` return. Skip if a plan @@ -785,7 +785,7 @@ pub fn apply_leaf_vec_folds( } else { inner_shape }; - registry.require_output(&vec_elem, &loc); + registry.require_output(&vec_elem); // The fold delivers the return element-by-element, so the // whole `Vec` / `Option>` converter is not needed. // De-require it: for String / scalar elements it still @@ -821,7 +821,7 @@ pub fn apply_leaf_vec_folds( if registry.callback_arg_plans.contains_key(&key) { continue; } - registry.require_output(&elem, &loc); + registry.require_output(&elem); let plan = whole_leaf_fold_plan(&elem, UnfoldShape::Iterable(Box::new(UnfoldShape::Base))); registry.callback_arg_plans.insert(key, plan); @@ -938,7 +938,7 @@ fn process_decl( ed: &OutputDecl, ) -> Result<(), UnfoldError> { { - let (item_fn, loc) = registry + let (item_fn, _) = registry .functions .get(&ed.func) .cloned() @@ -1016,7 +1016,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, &element)?; let plan = build_plan(acc, registry, ed, by_ref, &element, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty, &loc); + registry.require_output(&leaf.out_ty); } plan } else { @@ -1025,7 +1025,7 @@ fn process_decl( // No declaration is involved (`decon: None`) — the element // crosses whole through its own converter. let by_ref = matches!(&inner, syn::Type::Reference(_)); - registry.require_output(&inner, &loc); + registry.require_output(&inner); UnfoldPlan { source: inner.clone(), decon: None, @@ -1065,7 +1065,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, &source)?; let plan = build_plan(acc, registry, ed, by_ref, &source, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty, &loc); + registry.require_output(&leaf.out_ty); } plan }; @@ -1099,7 +1099,7 @@ fn process_decl( } else { leaf_ty }; - registry.require_output(&cv_ty, &loc); + registry.require_output(&cv_ty); UnfoldPlan { delivery: Delivery::Return, convert_out_ty: Some(cv_ty), diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index d0e8a224..31d8d238 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -93,9 +93,10 @@ fn accessor_optional_primitive() { "z_timestamp_ntp64" ); assert_eq!(plan.leaves[0].out_ty.to_token_stream().to_string(), "i64"); - assert!(reg - .required_outputs_scan - .contains(&TypeKey::from_type(&syn::parse_quote!(i64)))); + assert!( + reg.output_types[&TypeKey::from_type(&syn::parse_quote!(i64))].root, + "the leaf type must be a root" + ); } #[test] @@ -161,9 +162,7 @@ fn accessor_plan_byref() { // Leaf out_tys registered as required outputs so the resolver builds // their converters. - assert!(reg - .required_outputs_scan - .contains(&TypeKey::from_type(&syn::parse_quote!(&str)))); + assert!(reg.output_types[&TypeKey::from_type(&syn::parse_quote!(&str))].root); } #[test] @@ -756,9 +755,7 @@ fn iterable_whole_element_plan() { .map(|t| t.to_token_stream().to_string()), Some("ZZenohId".to_string()) ); - assert!(reg - .required_outputs_scan - .contains(&TypeKey::from_type(&syn::parse_quote!(ZZenohId)))); + assert!(reg.output_types[&TypeKey::from_type(&syn::parse_quote!(ZZenohId))].root); } #[test] @@ -863,9 +860,7 @@ fn convert_output_single_value() { Some("Option < i64 >".to_string()) ); // The shaped convert type is registered as a required output. - assert!(reg - .required_outputs_scan - .contains(&TypeKey::from_type(&syn::parse_quote!(Option)))); + assert!(reg.output_types[&TypeKey::from_type(&syn::parse_quote!(Option))].root); } #[test] @@ -1354,12 +1349,8 @@ fn callback_arg_plan_derived() { "SampleKind" ); // Leaf out_tys registered so the resolver builds their converters. - assert!(reg - .required_outputs_scan - .contains(&TypeKey::from_type(&syn::parse_quote!(&str)))); - assert!(reg - .required_outputs_scan - .contains(&TypeKey::from_type(&syn::parse_quote!(SampleKind)))); + assert!(reg.output_types[&TypeKey::from_type(&syn::parse_quote!(&str))].root); + assert!(reg.output_types[&TypeKey::from_type(&syn::parse_quote!(SampleKind))].root); // No return-position plan was created for the declaring fn. assert!(reg.unfold_plans.is_empty()); } @@ -1740,8 +1731,9 @@ fn sum_return_is_a_fixed_builder_plan() { assert!(!plan.leaves[0].has_converter()); assert!(plan.leaves[1].has_converter()); assert!( - !reg.required_outputs_scan - .contains(&TypeKey::from_type(&syn::parse_quote!(Reading))), + !reg.output_types + .get(&TypeKey::from_type(&syn::parse_quote!(Reading))) + .is_some_and(|c| c.root), "a sum has no whole-value converter, so its return must not require one" ); } @@ -1771,7 +1763,9 @@ fn sum_return_layers_ride_the_shape_fold() { for ty in ["Option", "Vec", "Reading"] { let ty: syn::Type = syn::parse_str(ty).unwrap(); assert!( - !reg.required_outputs_scan.contains(&TypeKey::from_type(&ty)), + !reg.output_types + .get(&TypeKey::from_type(&ty)) + .is_some_and(|c| c.root), "no layer of a sum return may require a whole-value converter: {}", ty.to_token_stream() ); @@ -1793,10 +1787,9 @@ fn sum_return_layers_ride_the_shape_fold() { fn a_vec_only_sum_return_drops_the_bare_requirement() { let mut reg = reg_with(&["fn read_all(n: i32) -> Vec { todo!() }"]); let bare: syn::Type = syn::parse_quote!(Reading); - reg.require_output(&bare, &crate::SourceLocation::default()); + reg.require_output(&bare); assert!( - reg.required_outputs_scan - .contains(&TypeKey::from_type(&bare)), + reg.output_types[&TypeKey::from_type(&bare)].root, "fixture precondition: the bare element starts out required" ); @@ -1807,7 +1800,9 @@ fn a_vec_only_sum_return_drops_the_bare_requirement() { for ty in ["Vec", "Reading"] { let ty: syn::Type = syn::parse_str(ty).unwrap(); assert!( - !reg.required_outputs_scan.contains(&TypeKey::from_type(&ty)), + !reg.output_types + .get(&TypeKey::from_type(&ty)) + .is_some_and(|c| c.root), "no layer of a sum return may require a whole-value converter: {}", ty.to_token_stream() ); diff --git a/prebindgen/src/api/core/write.rs b/prebindgen/src/api/core/write.rs index ab556810..2d593c5a 100644 --- a/prebindgen/src/api/core/write.rs +++ b/prebindgen/src/api/core/write.rs @@ -2,7 +2,7 @@ //! //! `write_rust` collects every resolved input/output converter (each entry //! already carries its full `ItemFn`), every per-item `on_` output, -//! and every passthrough item; concatenates them; and hands them to +//! and every anonymous const; concatenates them; and hands them to //! `Destination::write` (which does prettyplease formatting and //! resolves the path against `OUT_DIR`). @@ -105,27 +105,26 @@ pub fn write_rust, E: Prebindgen>( // Consts: an adapter WITH a const declaration mechanism // (`declared_consts() == Some(set)`) emits declared consts only, // symmetric with functions; an adapter without one (`None`) gets every - // const passed through verbatim via the default `on_const`. Unnamed - // consts (`const _`, e.g. the injected `konst::assertc_eq!` feature - // guard) are infrastructure, not declarable API — they bypass the gate - // and always emit. + // const passed through verbatim via the default `on_const`. Prebindgen's + // own injected feature guards are not consts at all — see `guards` below. let declared_consts = ext.declared_consts(); items.extend(parse_items_from_tokens( "on_const", sorted_items_by_ident(®istry.consts) .into_iter() .filter(|(ident, _)| { - *ident == "_" - || declared_consts - .as_ref() - .is_none_or(|set| set.contains(*ident)) + declared_consts + .as_ref() + .is_none_or(|set| set.contains(*ident)) }) .map(|(_, (item, _))| ext.on_const(item, registry)), )?); - // 3. Passthrough items verbatim. - for (item, _) in ®istry.passthrough { - items.push(item.clone()); + // 3. Anonymous consts, verbatim. Last, and in stream order. Ungated on + // purpose: with no name there is nothing for an adapter to declare, so + // the const gate above cannot apply to them. + for guard in ®istry.guards { + items.push(syn::Item::Const(guard.origin.syntax.clone())); } // 4. Cross-cutting post-process pass. Adapters use this to qualify @@ -162,13 +161,13 @@ pub fn collect_converter_items(registry: &Registry) -> Vec<(syn::Ident, sy } fn walk_resolved)>( - table: &std::collections::HashMap>>, + table: &std::collections::HashMap>, mut f: F, ) { let mut keys: Vec<&TypeKey> = table.keys().collect(); keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); for key in keys { - if let Some(Some(entry)) = table.get(key) { + if let Some(entry) = table.get(key).and_then(|c| c.entry.as_ref()) { f(key, entry); } } diff --git a/prebindgen/src/api/core/write/tests.rs b/prebindgen/src/api/core/write/tests.rs index b1353085..20e7e976 100644 --- a/prebindgen/src/api/core/write/tests.rs +++ b/prebindgen/src/api/core/write/tests.rs @@ -7,7 +7,7 @@ use proc_macro2::TokenStream; use quote::ToTokens; use super::*; -use crate::SourceLocation; +use crate::{api::test_util::cell, SourceLocation}; struct IdentityExt; @@ -66,35 +66,41 @@ fn dedup_and_sort() { reg.input_types.insert( key_a.clone(), - Some(TypeEntry { - destination: wire.clone(), - function: syn::parse_quote!( - fn handle_to_u64_aaaa(v: i64) -> u64 { - v as u64 - } - ), - pre_stages: vec![], - subs: vec![], - required: true, - niches: crate::api::core::niches::Niches::empty(), - metadata: (), - }), + cell( + &key_a, + true, + Some(TypeEntry { + destination: wire.clone(), + function: syn::parse_quote!( + fn handle_to_u64_aaaa(v: i64) -> u64 { + v as u64 + } + ), + pre_stages: vec![], + subs: vec![], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), + ), ); reg.input_types.insert( key_b.clone(), - Some(TypeEntry { - destination: wire2.clone(), - function: syn::parse_quote!( - fn Ptr_to_Sample_bbbb(v: *const u8) -> Sample { - decode_sample(v) - } - ), - pre_stages: vec![], - subs: vec![], - required: true, - niches: crate::api::core::niches::Niches::empty(), - metadata: (), - }), + cell( + &key_b, + true, + Some(TypeEntry { + destination: wire2.clone(), + function: syn::parse_quote!( + fn Ptr_to_Sample_bbbb(v: *const u8) -> Sample { + decode_sample(v) + } + ), + pre_stages: vec![], + subs: vec![], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), + ), ); let items = collect_converter_items(®); @@ -216,3 +222,100 @@ fn bad_generated_tokens_report_emission_phase() { err ); } + +/// An adapter with a const mechanism gates **named** consts and cannot gate +/// guards — pinned at the emission site, not just in the registry. +/// +/// `a_guard_never_reaches_the_const_surface` proves the maps are separate, but it +/// never calls `write_rust`. This is what would catch a change that keeps +/// `Registry::guards` populated and then forgets to emit them, or re-gates them +/// on the way out. +#[test] +fn guards_emit_ungated_and_in_stream_order() { + /// Declares a const mechanism (`Some`) and declares nothing through it. + struct ConstGatingExt; + + impl Prebindgen for ConstGatingExt { + type Metadata = (); + + fn declared_consts(&self) -> Option> { + // The gate exists and is empty: `KEPT_OUT` must not emit. + Some(HashSet::new()) + } + fn on_function(&self, f: &syn::ItemFn, _r: &Registry<()>) -> TokenStream { + f.to_token_stream() + } + fn on_struct(&self, s: &syn::ItemStruct, _r: &Registry<()>) -> TokenStream { + s.to_token_stream() + } + fn on_enum(&self, e: &syn::ItemEnum, _r: &Registry<()>) -> TokenStream { + e.to_token_stream() + } + fn on_input_type( + &self, + _ty: &syn::Type, + _r: &Registry<()>, + ) -> Option> { + None + } + fn on_output_type( + &self, + _ty: &syn::Type, + _r: &Registry<()>, + ) -> Option> { + None + } + } + + let loc = SourceLocation::default(); + // Two distinguishable guards, straddling the named const, so the assertion + // below pins order rather than merely presence. + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::parse_quote!( + const _: () = { + first_check(); + }; + ), + loc.clone(), + ), + ( + syn::parse_quote!( + pub const KEPT_OUT: u64 = 7; + ), + loc.clone(), + ), + ( + syn::parse_quote!( + const _: () = { + second_check(); + }; + ), + loc.clone(), + ), + ]; + let registry: Registry<()> = Registry::from_items(items).expect("index"); + assert_eq!(registry.guards.len(), 2); + + let dir = crate::api::test_util::unique_test_dir("write_guards"); + std::fs::create_dir_all(&dir).unwrap(); + let path = registry + .resolve(ConstGatingExt) + .expect("resolve") + .write_rust(dir.join("gen.rs")) + .expect("write_rust"); + let src = std::fs::read_to_string(&path).unwrap(); + + // The named const is gated out; both guards emit regardless. + assert!( + !src.contains("KEPT_OUT"), + "declared_consts is empty:\n{src}" + ); + let first = src + .find("first_check") + .unwrap_or_else(|| panic!("guard 1 missing:\n{src}")); + let second = src + .find("second_check") + .unwrap_or_else(|| panic!("guard 2 missing:\n{src}")); + assert!(first < second, "guards must keep stream order:\n{src}"); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index c7eb965b..ee9b700a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -315,7 +315,7 @@ pub(crate) fn validate_bindings( let mut const_idents: Vec<&syn::Ident> = registry.consts.keys().collect(); const_idents.sort(); for ident in const_idents { - if *ident == "_" || !declared_consts.contains(ident) { + if !declared_consts.contains(ident) { continue; } let (item_const, _) = ®istry.consts[ident]; diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs index a26bdc1a..6e23b58e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs @@ -8,7 +8,7 @@ use crate::{ niches::{NicheSlot, Niches}, registry::{Registry, TypeEntry, TypeKey}, }, - test_util::unique_test_dir, + test_util::{cell, unique_test_dir}, }, SourceLocation, }; @@ -55,7 +55,6 @@ fn entry(wire: syn::Type, conv_name: &str, niches: Niches) -> TypeEntry, ) { + let key = TypeKey::parse(ty_str).expect("test type"); reg.input_types - .insert(TypeKey::parse(ty_str).expect("test type"), Some(e)); + .insert(key.clone(), cell(&key, true, Some(e))); } fn install_output( @@ -77,6 +77,7 @@ fn install_output( _rank: usize, e: TypeEntry, ) { + let key = TypeKey::parse(ty_str).expect("test type"); reg.output_types - .insert(TypeKey::parse(ty_str).expect("test type"), Some(e)); + .insert(key.clone(), cell(&key, true, Some(e))); } diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 4e0f88b6..8215332c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -1587,12 +1587,6 @@ impl Prebindgen for JniGen { /// const's type flows through the ordinary output-converter machinery); /// only the callee expression differs — a path to the const, not a call. fn on_const(&self, c: &syn::ItemConst, registry: &Registry) -> TokenStream { - // Unnamed infrastructure consts (`const _`, e.g. the injected - // `konst::assertc_eq!` feature guard) pass through verbatim — no - // getter, no Kotlin surface. - if c.ident == "_" { - return c.to_token_stream(); - } reject_handle_const(self, c); let getter = const_getter_fn(c); let const_ident = &c.ident; diff --git a/prebindgen/src/api/test_util.rs b/prebindgen/src/api/test_util.rs index 833e64a3..c1c6cda7 100644 --- a/prebindgen/src/api/test_util.rs +++ b/prebindgen/src/api/test_util.rs @@ -6,7 +6,20 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, }; -use crate::api::core::registry::Registry; +use crate::api::core::registry::{Registry, TypeCell, TypeEntry, TypeKey, TypeSubject}; + +/// A type-table cell for a fixture. +/// +/// The subject is always [`TypeSubject::Adapter`]: a hand-built table has no +/// `Flat` behind it, so no key in one has a source reading. A test that cares +/// about the `Source` side builds its registry from items instead. +pub(crate) fn cell(key: &TypeKey, root: bool, entry: Option>) -> TypeCell { + TypeCell { + subject: TypeSubject::Adapter(key.to_type()), + root, + entry, + } +} /// Index a `Registry` from a list of Rust item sources. /// diff --git a/prebindgen/src/lib.rs b/prebindgen/src/lib.rs index 9097ac62..630f2055 100644 --- a/prebindgen/src/lib.rs +++ b/prebindgen/src/lib.rs @@ -296,7 +296,7 @@ pub mod core { pub use crate::api::core::{ ConverterImpl, Direction, DomainScalar, Element, Flat, Generation, Gravestone, NicheSlot, Niches, Prebindgen, Registry, RegistryBuilder, RepresentationDomain, ScalarValue, - ScanError, Stage, Transmute, TypeEntry, TypeKey, WriteRustError, + ScanError, Stage, Transmute, TypeCell, TypeEntry, TypeKey, TypeSubject, WriteRustError, }; } From 4be973dcca94797e98d437c125cf3a6ff9e20e27 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 31 Jul 2026 10:42:36 +0200 Subject: [PATCH 08/52] Flat is the only index; Registry stops keeping a second one (#243) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Flat is the only index; Registry stops keeping a second one `Registry` held five maps — `functions`, `structs`, `enums`, `consts`, `guards` — plus `item_origins` and `source_modules`. Every one was built in `from_flat` by walking `flat.elements()`, and every one duplicated something `Flat` already had, indexed the same way. L1 made the registry a projection of the model; a projection that copies is still two stores that can disagree. All seven are deleted. `from_flat` is now the expressibility check, the type-ref index, and storing the model — nothing else. Two facts found by measuring the call sites first: * **The `SourceLocation` half of every entry was dead.** All 44 `.get()` sites destructured `(item, _)`; every `values()` and index site bound `_loc`. Nothing had read it since L1 moved locations onto elements. * **`guards` had one reader** and was already `Vec` — the flat type, copied out of the model verbatim. `Flat` grows what the maps were providing, beside its existing typed accessors: `struct_type`, `enum_item` (either enum shape — the merge the old `enums` map made, which 30 adapter reads depend on), and `source_modules`. `Registry` keeps `origin_module`, `default_module`, `all_source_modules` and `named_item_idents` as methods — they are questions, not storage — now answered off `flat`. **No mirrored accessors on `Registry`**: one door, so there are not two interfaces to keep in agreement. **Binding-local fns move into the model.** They were the one population `Flat` did not have — a `sig!(..)` is written in a build script and was inserted straight into `registry.functions` — so deleting that map would have left `flat.function()` incomplete and "one index" a lie. `check_signature` already lowered one through `lower_fn` and discarded it; it returns the `Function` now, and a `pub(crate)` `add_local_function` admits it with the adapter's origin crate stamped where `origin_module` already looks. The public surface does not grow. Three invariants would have moved generated output silently. Each is now tested, and each test was checked against its own violation: * `named_item_idents` must keep excluding `Extern`. Its caller decides which names generated Rust qualifies, so including an alias would move output. * `source_modules` must not see binding-local fns — it decides `default_module`, which is what an unqualified reference resolves against. Fixed by construction: `Flat` freezes it in `build()` from the captured stream, and `add_local_function` does not touch it. * `item_origins` must keep seeing them — the mirror of the above, and what qualifies a local fn's generated call. **Ledger 204 → 202**, the first movement in this program: `accessor_signature` and `accessor_consumes` peel a borrow by reading `TypeKind::Ref` instead of matching `syn::Type::Reference`. `ctor_signature` and two return-type walks likewise read `params`/`ret` off the element rather than re-deriving them from the signature — which also drops three hand-rolled copies of "an elided return is `()`", a fact the model states once. Generation byte-identical **with `cargo clean -p example-cbindgen -p example-flat` first** — the check only regenerates what cargo decides to rebuild, so a cached run proves nothing. 589 + 517 tests, covertest 48 sections, clippy clean in three configs. Co-Authored-By: Claude Opus 5 * Spell the negated lookups as `is_none` `!x.is_some()` from the mechanical rewrite. `clippy::nonminimal_bool` on the MSRV toolchain rejects it; the newer clippy I had been checking with does not, so this reached CI. The gap was in the verification, not the code: CI's clippy step is `--no-default-features --all-features` **together** and runs on 1.85.0, and I had been running the two flags separately on nightly. `cargo +1.85 clippy --all-targets --no-default-features --all-features -- --deny warnings` reproduces it exactly and is now clean. Co-Authored-By: Claude Opus 5 * A lookup takes the name the caller already holds Making `Flat` the only index left every lookup spelling its argument `&x.to_string()` — 73 call sites — because the accessors take `&str` while callers hold a `syn::Ident`. One fact from `proc_macro2` decides the fix, and it is the opposite of what the obvious move suggests: **the allocation cannot be removed, only moved.** `impl Hash for Ident` hashes via `self.to_string()`, so re-keying `by_name` as `HashMap` would allocate on every lookup *and* every insert, and would make the `&str` callers start paying too. `Ident` has no `Borrow` and no `as_str()`, so no borrow-based path exists either. So this is call-site noise, not cost, and it belongs in the API: pub trait Name: sealed::Sealed { fn as_name(&self) -> Cow<'_, str>; } impl Name for str // Borrowed — free impl Name for String // Borrowed — free impl Name for syn::Ident // Owned — the allocation, moved inside impl Name for &T The six accessors — `element`, `function`, `declared_type`, `constant`, `struct_type`, `enum_item` — take `&N: Name + ?Sized`. `Cow` rather than a simpler `impl Display` + `format!` because two callers must stay allocation-free: `immediate_edges` runs per type-graph edge across both the scan and the resolver's BFS, and `Flat::resolve` runs per reference. Both hold a `String` or `&str` and keep `Cow::Borrowed`. The blanket `&T` impl is what let the migration be one mechanical rule (`&X.to_string()` → `&X`): without it, the sites where `X` is already a reference would have produced `&&Ident`. Sealed, so the one new public name cannot grow a second meaning from outside. That is the trade against the alternative — a `*_by_name` twin for each accessor, twelve names instead of seven, two spellings per concept to keep in agreement. Signature change only: no generated byte and no test assertion moves. The call sites lose 22 net lines; `flat/mod.rs` is the only file that gains any. Verified with `regen-check` after `cargo clean -p example-cbindgen -p example-flat`, `cargo +1.85 clippy --all-targets --no-default-features --all-features`, 589 + 517 tests, and covertest's 48 sections. A doc-test on `Name` pins that both spellings reach the same element. Co-Authored-By: Claude Opus 5 * Address review: docs, one alias answer, and no self-inflicted expects **1. Stranded docs.** `declared_type_idents` landed between `named_item_idents`' doc block and its signature, so a private helper carried three stacked blocks while two public methods carried none — and `origin_module`'s doc had already been stranded there before this branch. Each doc now sits on its own method. The inherited text also still described `item_origins`, which this PR deletes; that sentence is gone, and the alias exclusion is stated where the arm performing it can be seen. **2. Two sibling checks disagreed about an alias.** `scan_declared_items`' path-qualified warning became `declared_type(..).is_some()`, which answers `Some` for an `Extern`; the `ignored_types` check sixty lines down kept `struct_type(..) || enum_item(..)`, which does not. Both were `structs || enums` on the base, so I had changed one and not the other. Chosen answer: **an alias does not count**, restoring both to the base's behaviour. Firing is arguably more correct — an alias *is* a captured item declaring that name — but this PR claims to move no behaviour, and that claim is what makes `regen-check` meaningful as its proof. A warning that starts firing is still a change, and it belongs in a PR that argues for it and tests it. Both sites now share `declares_type_body`, so they cannot drift apart again. **3. Three self-inflicted `expect`s.** Each loop collected `Vec<&syn::Ident>`, sorted, then looked every name back up — manufacturing an infallible lookup the type system could not see was infallible. They hold the elements instead (`Vec<&Function>` / `Vec<&Constant>`, sorted by `name`), which deletes the `expect`, a second hash per iteration, and a `to_string()` per iteration. Ordering is unchanged: `Ident: Ord` is the string order. This restores the standing rule that the working path carries no `expect`. **4. `__f` / `__s` / `__c` closure bindings**, 29 sites, artifacts of the mechanical rewrite dodging an outer `f`/`s`. Now `func` / `st` / `konst`. The 13 `__e`/`__v`/`__x` are pre-existing and left alone. **5. `check_signature` → `lower_signature`.** It returns the lowered `Function` and the caller keeps it; the name should say lowering-that-validates rather than checking. Re-applied on top of the #244 merge rather than rebased: #244 rewrote most of the same lines, so replaying produced 13 conflicts against a branch whose content I could reproduce exactly. `declares_type_body` needs no `to_string()` here, since `Name` landed with #244. Byte-identical generation (after `cargo clean -p example-cbindgen -p example-flat`), 589 + 517 tests, covertest 48 sections, `cargo +1.85 clippy --all-targets --no-default-features --all-features` clean. Co-Authored-By: Claude Opus 5 * An alias counts as a declaration of its name The follow-up #243's review asked for: there, both type-diagnostic sites were restored to `structs || enums` because that PR's claim was that it moved no behaviour. This is the change on its merits. Two sites ask "does the source declare a type under this name?": * the **path-qualified** heuristic — `ptr_class!(foreign::Handle)` warns "a captured item `Handle` exists — declare it by its bare name"; * the **ignored-type** check — `ignore_types(Handle)` warns "not found among `#[prebindgen]` items". Both answered "no" for an alias, and both were wrong to. `#[prebindgen] pub type Handle = ..` **is** a declaration of the name `Handle`, and an adapter may declare it bare — that lands in the no-indexed-body branch, which is exactly what `ptr_class(ZKeyExpr<'static>)` relies on. So the first suppressed a fix-it that would have worked, and the second called a captured item missing. The exclusion was never a decision. It is an artefact of where the answer used to come from: the pre-`Flat` code asked the `structs`/`enums` maps, which never held an alias because the registry had no map for one. #243 moved the lookup to the model and the artefact became visible. `declares_type_body` → `declares_type`, and it is `flat.declared_type(..).is_some()`. **`declared_type_idents` deliberately keeps excluding aliases.** It is the sibling that looks like it should change and must not: it feeds *"skipping undeclared `#[prebindgen]` struct/enum"*, which asks what an adapter left unclaimed and names a kind an alias is not. Warning about unclaimed aliases may be worth doing, but it needs its own message and is a different question. Both halves are pinned by the test, and both were checked against their own violation. **Nothing in-tree exercises this.** The four example crates emit 251 of these warnings and the set is byte-identical before and after — measured, not assumed. So the tests are the only proof, and they construct the case directly rather than leaning on the examples. The warning *text* is `cargo:warning=` on stdout and is not captured; what the second test pins is that an alias reaches both sites through `scan_declared` without tripping the `QualifiedDeclaredTypes` hard error. Said plainly rather than claimed as coverage it does not have. 591 + 519 tests, generation byte-identical after a forced rebuild, covertest 48 sections, MSRV clippy clean. Co-Authored-By: Claude Opus 5 * Name the body-only helper for its population `declared_type_idents` read as the iterator form of `declares_type`, which it is not: the predicate counts every declared type including aliases, the iterator excludes them. Review's point — the pairing invites exactly the accidental widening the rest of this PR documents against. `struct_enum_idents` names the population instead, and matches word-for-word the warning it feeds ("skipping undeclared `#[prebindgen]` struct/enum"), so the reason for the exclusion is visible at the call site. The doc says outright that it is not the iterator form of the predicate. The existing test already calls the helper directly, so the clearer name is pinned too. Co-Authored-By: Claude Opus 5 * Point the JNI docs at the model, not the deleted maps Five comments still named maps this PR removes, so the documentation described an architecture that no longer exists: * `jni/classify.rs` — "`registry.structs` probes" → `registry.flat()` type probes, which is what `type_kind` actually does * `jni/mod.rs` ×2 — `registry.functions[ident]` → `registry.flat().function(ident)`, matching the lookup `kotlin_emit`/`symbols` perform on a `FunctionEntry` * `jni/emit/struct_out.rs` — "`registry.structs`" → the parsed model; the claim "populated before `resolve`" still holds, the model more plainly than the maps did * `jni/trait_impl.rs` — the fourth the review did not name: it explained the default-module fallback in terms of "items `item_origins` never sees", and `item_origins` is gone. Restated as what the fallback now turns on — an element whose location carries no crate name. Each new claim was checked against the code rather than assumed: `classify.rs:67` probes `flat().struct_type`, the `FunctionEntry` lookups are `flat().function(&entry.rust_ident)`, and `struct_out` reaches the model through `ext.type_kind` (indirect, as the original comment also was). Docs only — no code, no behaviour. 591 tests, generation byte-identical after a forced rebuild, MSRV clippy clean, doc-link warnings unchanged at 16. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- prebindgen/src/api/core/expand.rs | 53 ++-- prebindgen/src/api/core/flat/boundary.ledger | 4 +- prebindgen/src/api/core/flat/mod.rs | 171 +++++++++++- prebindgen/src/api/core/registry.rs | 256 +++++++----------- prebindgen/src/api/core/registry/tests.rs | 222 +++++++++++++-- prebindgen/src/api/core/resolve/tests.rs | 22 +- prebindgen/src/api/core/unfold.rs | 93 ++++--- prebindgen/src/api/core/write.rs | 53 ++-- prebindgen/src/api/core/write/tests.rs | 33 +-- prebindgen/src/api/lang/cbindgen/convert.rs | 16 +- prebindgen/src/api/lang/cbindgen/emit.rs | 18 +- prebindgen/src/api/lang/cbindgen/mod.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/builder.rs | 52 ++-- .../src/api/lang/jnigen/jni/classify.rs | 8 +- .../api/lang/jnigen/jni/emit/flat_input.rs | 8 +- .../api/lang/jnigen/jni/emit/struct_out.rs | 6 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 2 +- .../src/api/lang/jnigen/jni/emit/vec_build.rs | 6 +- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 23 +- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 81 ++++-- prebindgen/src/api/lang/jnigen/jni/mod.rs | 4 +- .../src/api/lang/jnigen/jni/overloads.rs | 13 +- prebindgen/src/api/lang/jnigen/jni/render.rs | 21 +- prebindgen/src/api/lang/jnigen/jni/report.rs | 6 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/symbols.rs | 24 +- .../api/lang/jnigen/jni/tests/callbacks.rs | 7 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 54 ++-- 28 files changed, 827 insertions(+), 433 deletions(-) diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index b384134f..55cbdb1a 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -26,7 +26,7 @@ use std::collections::HashSet; -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use quote::quote; use crate::api::core::{ @@ -190,10 +190,10 @@ pub fn apply( // (`Option`/`&`) type must equal the decl's declared type — the // typo guard for both coordinates of `.expand_param(name, decl)`. if let Some(declared) = &ed.declared_target { - let (item_fn, _) = registry - .functions - .get(&ed.func) - .cloned() + let item_fn = registry + .flat() + .function(&ed.func) + .map(|f| f.origin.syntax.clone()) .ok_or_else(|| ExpandError::UnknownFunction(ed.func.clone()))?; let param_ty = find_param_type(&item_fn, &ed.param) .ok_or_else(|| ExpandError::UnknownParam(ed.func.clone(), ed.param.clone()))?; @@ -237,7 +237,11 @@ pub fn apply( if accessor_fns.contains(func) { continue; } - let Some((item_fn, _)) = registry.functions.get(func).cloned() else { + let Some(item_fn) = registry + .flat() + .function(&func) + .map(|f| f.origin.syntax.clone()) + else { continue; }; // A method's receiver (first param of its class type) binds to `this` @@ -299,10 +303,10 @@ fn process_expand( exp: &Expansions, ed: &ExpandDecl, ) -> Result<(), ExpandError> { - let (item_fn, _) = registry - .functions - .get(&ed.func) - .cloned() + let item_fn = registry + .flat() + .function(&ed.func) + .map(|f| f.origin.syntax.clone()) .ok_or_else(|| ExpandError::UnknownFunction(ed.func.clone()))?; let param_ty = find_param_type(&item_fn, &ed.param) @@ -370,25 +374,20 @@ fn resolve_constructor( /// Constructor signature: parameter `(name, type)` pairs, the produced /// (`Ok`) target type, and whether it is fallible (`-> Result<_, _>`). fn ctor_signature(registry: &Registry, func: &syn::Ident) -> Result { - let (item_fn, _) = registry - .functions - .get(func) + // Read off the element rather than re-walked from the signature: `params` + // and `ret` are the same facts, already decided once — including that an + // elided return and a written `-> ()` are one thing. + let f = registry + .flat() + .function(&func) .ok_or_else(|| ExpandError::UnknownConstructor(func.clone()))?; - let mut params: Vec<(syn::Ident, syn::Type)> = Vec::new(); - for input in &item_fn.sig.inputs { - if let syn::FnArg::Typed(pt) = input { - let name = match &*pt.pat { - syn::Pat::Ident(pi) => pi.ident.clone(), - _ => syn::Ident::new("arg", Span::call_site()), - }; - params.push((name, (*pt.ty).clone())); - } - } - let ret: syn::Type = match &item_fn.sig.output { - syn::ReturnType::Default => syn::parse_quote!(()), - syn::ReturnType::Type(_, t) => (**t).clone(), - }; + let params: Vec<(syn::Ident, syn::Type)> = f + .params + .iter() + .map(|p| (p.name.clone(), p.ty.origin.syntax.clone())) + .collect(); + let ret: syn::Type = f.ret.origin.syntax.clone(); let (target, fallible) = match result_ok_type(&ret) { Some(ok) => (ok, true), None => (ret, false), diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 9a90f23d..988c22fc 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -44,7 +44,7 @@ 4 api/core/expand.rs 11 api/core/registry.rs 40 api/core/types_util.rs -18 api/core/unfold.rs +16 api/core/unfold.rs 8 api/lang/cbindgen/builder.rs 1 api/lang/cbindgen/convert.rs 5 api/lang/cbindgen/emit.rs @@ -70,4 +70,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 204 +# total: 202 diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index 913a6a96..95255bd8 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -378,7 +378,21 @@ impl FlatBuilder { .enumerate() .filter_map(|(i, e)| e.name().map(|n| (n.to_string(), i))) .collect(); - Ok(Flat { elements, by_name }) + // Frozen here, from the captured stream alone. See the field's docs. + let mut source_modules: Vec = Vec::new(); + for element in &elements { + if let Some(crate_name) = element.location().crate_name.as_ref() { + let module = crate_name.replace('-', "_"); + if !source_modules.contains(&module) { + source_modules.push(module); + } + } + } + Ok(Flat { + elements, + by_name, + source_modules, + }) } } @@ -407,6 +421,15 @@ impl FlatBuilder { pub struct Flat { /// Source order, so iteration reports items as the sources were fed. elements: Vec, + /// Module name of every **captured** source, in first-seen order (crate + /// names, dashes normalized to underscores). The first doubles as the + /// default module for a reference with no recorded origin. + /// + /// Computed once in [`FlatBuilder::build`] and frozen: it is a property of + /// the ingested stream, so a binding-local function added later must not + /// extend it — that would change which module an unqualified reference + /// resolves against. + source_modules: Vec, /// Name → position in [`Self::elements`]. /// /// A map rather than a scan because every typed accessor and every @@ -417,6 +440,71 @@ pub struct Flat { by_name: std::collections::HashMap, } +/// A name a lookup can be performed with. +/// +/// Exists because callers hold different spellings of the same fact: an adapter +/// walking captured items has a `syn::Ident`, a resolved reference has the +/// `String` inside a [`TypeId`], and a test has a literal. One accessor takes all +/// three rather than each call site converting. +/// +/// **The conversion is moved, not removed.** `proc_macro2::Ident` hashes by +/// `to_string()` and offers no borrow as `str`, so an `Ident` lookup allocates +/// wherever it happens; doing it here keeps `&str` and `&String` callers — among +/// them the per-edge and per-reference lookups in the scan and the resolver — +/// allocation-free. +/// +/// Sealed: what may name an element is the language's business, not a caller's. +/// +/// ``` +/// # prebindgen::Source::init_doctest_simulate(); +/// use prebindgen::core::flat::Flat; +/// +/// let flat = Flat::builder().source("source_ffi").build()?; +/// let ident = quote::format_ident!("test_function"); +/// +/// // The same element, whichever spelling the caller happens to hold. +/// assert!(flat.function("test_function").is_some()); +/// assert!(flat.function(&ident).is_some()); +/// # Ok::<_, prebindgen::core::flat::ParseError>(()) +/// ``` +pub trait Name: sealed::Sealed { + /// The name as a string, borrowed when the caller already holds one. + fn as_name(&self) -> std::borrow::Cow<'_, str>; +} + +mod sealed { + pub trait Sealed {} + impl Sealed for str {} + impl Sealed for String {} + impl Sealed for syn::Ident {} + impl Sealed for &T {} +} + +impl Name for str { + fn as_name(&self) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Borrowed(self) + } +} + +impl Name for String { + fn as_name(&self) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Borrowed(self) + } +} + +impl Name for syn::Ident { + fn as_name(&self) -> std::borrow::Cow<'_, str> { + std::borrow::Cow::Owned(self.to_string()) + } +} + +/// So a caller already holding a reference does not have to reborrow. +impl Name for &T { + fn as_name(&self) -> std::borrow::Cow<'_, str> { + T::as_name(self) + } +} + impl Flat { /// Start collecting what to parse. pub fn builder() -> FlatBuilder { @@ -431,11 +519,12 @@ impl Flat { /// The element with this name, whatever kind it is — including an /// [`Element::Unsupported`], which still holds its name against the /// namespace. - pub fn element(&self, name: &str) -> Option<&Element> { - self.elements.get(*self.by_name.get(name)?) + pub fn element(&self, name: &N) -> Option<&Element> { + self.elements + .get(*self.by_name.get(name.as_name().as_ref())?) } - pub fn function(&self, name: &str) -> Option<&Function> { + pub fn function(&self, name: &N) -> Option<&Function> { match self.element(name)? { Element::Function(f) => Some(f), _ => None, @@ -447,14 +536,14 @@ impl Flat { /// Named `declared_type` because `type` is a keyword; it is the accessor a /// resolved [`TypeKind::Named`] reference leads to, and [`Self::resolve`] is /// the same lookup taking a [`TypeId`]. - pub fn declared_type(&self, name: &str) -> Option<&Type> { + pub fn declared_type(&self, name: &N) -> Option<&Type> { match self.element(name)? { Element::Type(t) => Some(t), _ => None, } } - pub fn constant(&self, name: &str) -> Option<&Constant> { + pub fn constant(&self, name: &N) -> Option<&Constant> { match self.element(name)? { Element::Constant(c) => Some(c), _ => None, @@ -500,6 +589,40 @@ impl Flat { .flat_map(TypeRef::walk) } + /// The `struct` declared under this name, or `None` for any other shape. + /// + /// A tuple struct is an [`Extern`] rather than a `Struct`, so this answers + /// only for a product of fields that cross the boundary. + pub fn struct_type(&self, name: &N) -> Option<&Struct> { + match self.declared_type(name)? { + Type::Struct(s) => Some(s), + _ => None, + } + } + + /// The `syn::ItemEnum` behind **either** enum shape. + /// + /// A sum and a C-style enum are different elements — numbered differently + /// and consumed as different constructs — but both were spelled `enum` in + /// Rust and both keep that item. A consumer re-emitting the source wants the + /// item without caring which shape it is; one that acts on the distinction + /// reaches for [`Self::declared_type`]. + pub fn enum_item(&self, name: &N) -> Option<&syn::ItemEnum> { + match self.declared_type(name)? { + Type::Variant(v) => Some(&v.origin.syntax), + Type::Enum(e) => Some(&e.origin.syntax), + _ => None, + } + } + + /// Module name of every captured source, in first-seen order. + /// + /// The first entry is the default module for a reference with no recorded + /// origin. Empty for a hand-built stream that carried no crate stamps. + pub fn source_modules(&self) -> &[String] { + &self.source_modules + } + /// Every anonymous const, in stream order — **zero or more**. /// /// Not part of the flat API — see [`Guard`] — but ingested with it, and a @@ -524,7 +647,7 @@ impl Flat { }) } - /// Check a function signature against the source language's grammar. + /// Lower a function signature written outside the captured stream. /// /// For the **one input that does not come through this module**: a binding's /// `local_functions`, whose signatures are written by hand in a build script @@ -532,10 +655,12 @@ impl Flat { /// lowered here, so this exists to keep the grammar decided in one place /// rather than re-checked at the far end. /// - /// Grammar only. Whether the types it names are *declared* is a whole-model - /// question ([`resolve_references`]), and a binding-local fn may legitimately - /// name types the source crate never did. - pub fn check_signature(&self, f: &syn::ItemFn) -> Result<(), ItemError> { + /// Grammar only, and it **validates by lowering**: an `Err` is a shape the + /// language cannot express, an `Ok` is the element to admit. Whether the types + /// it names are *declared* is a whole-model question ([`resolve_references`]), + /// and a binding-local fn may legitimately name types the source crate never + /// did. + pub fn lower_signature(&self, f: &syn::ItemFn) -> Result { // Rebuilt from the model rather than kept: this runs once per local fn, // and a stored index would be a second copy of what `constants()` says. let consts = ConstIndex::new(self.constants().map(|c| { @@ -545,9 +670,29 @@ impl Flat { c.origin.crate_name().map(str::to_owned), ) })); - // A synthesized fn has no captured location; the caller names it. + // A synthesized fn has no captured location, but it does have an origin + // crate — the caller supplies it, and `add_local_function` records it. let at = Rc::new(SourceLocation::default()); - lower_fn(f, &at, &consts).map(|_| ()) + lower_fn(f, &at, &consts) + } + + /// Admit a binding-local function: one a build script wrote via `sig!(..)` + /// rather than one a source crate marked. + /// + /// The model is the pipeline's only index, so a function nothing captured + /// still has to live here or nothing downstream can find it. `crate_name` is + /// the module its generated call qualifies against, stamped onto the + /// element's location where [`Element::location`] already looks for it. + /// + /// Deliberately does **not** extend [`Self::source_modules`]: see that + /// field's docs. + pub(crate) fn add_local_function(&mut self, mut f: Function, crate_name: String) { + f.origin.location = Rc::new(SourceLocation { + crate_name: Some(crate_name), + ..SourceLocation::default() + }); + self.by_name.insert(f.name.to_string(), self.elements.len()); + self.elements.push(Element::Function(f)); } /// The declaration a reference denotes. diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs index baf063e1..6991a3a2 100644 --- a/prebindgen/src/api/core/registry.rs +++ b/prebindgen/src/api/core/registry.rs @@ -293,34 +293,6 @@ pub struct Registry { /// later stage can ask it what a name means through the registry it already /// has — see [`Self::flat`]. flat: crate::api::core::flat::Flat, - pub functions: HashMap, - pub structs: HashMap, - pub enums: HashMap, - pub consts: HashMap, - /// Anonymous consts, in stream order, re-emitted verbatim — **zero or - /// more**. Not API: having no name, they cannot be declared, so they are - /// neither gated nor addressable. See - /// [`Guard`](crate::api::core::flat::Guard) for what produces them. - pub guards: Vec, - - /// Origin crate name of each named item (fn/struct/enum/const), - /// recorded by [`Self::from_items`] from each item's - /// [`SourceLocation::crate_name`] stamp (absent for hand-built, - /// origin-less item streams). Adapters - /// consult [`Self::origin_module`] so generated references qualify each - /// item with the module of the crate that actually defines it — the - /// multi-source model, where a binding layers helper `#[prebindgen]` - /// crates on top of the flat crate. - pub(crate) item_origins: HashMap, - - /// Module name of every ingested source, in first-seen stream order - /// (crate names, dashes normalized to underscores). The FIRST entry - /// doubles as the **default module** for references with no recorded - /// origin (e.g. a declared type with no `#[prebindgen]` item); - /// origin-less hand-built streams leave it empty and adapters fall - /// back to `crate`. - pub(crate) source_modules: Vec, - /// Type tables, one per direction. Each scanned type gets a [`TypeCell`] /// holding what the key names, whether the binding asks for it directly, and /// the resolved [`TypeEntry`] once the structural resolver fills it. @@ -385,13 +357,6 @@ impl Registry { pub(crate) fn empty() -> Self { Self { flat: crate::api::core::flat::Flat::default(), - functions: HashMap::new(), - structs: HashMap::new(), - enums: HashMap::new(), - consts: HashMap::new(), - guards: Vec::new(), - item_origins: HashMap::new(), - source_modules: Vec::new(), input_types: Default::default(), output_types: Default::default(), type_refs: HashMap::new(), @@ -780,7 +745,7 @@ impl Registry { /// // Annotated only because nothing here resolves: in a build script `M` is /// // fixed by the adapter passed to `resolve`, so no call site names it. /// let registry: Registry<()> = Registry::builder().source("source_ffi").build()?; - /// assert!(registry.functions.contains_key("e::format_ident!("test_function"))); + /// assert!(registry.flat().function("test_function").is_some()); /// # Ok::<_, prebindgen::core::ScanError>(()) /// ``` /// @@ -858,8 +823,6 @@ impl Registry { /// a source crate that needs migrating sees one list instead of one rebuild /// per item. pub fn from_flat(flat: crate::api::core::flat::Flat) -> Result { - use crate::api::core::flat::{Element, Type}; - let entries: Vec = flat .unsupported() .map(|u| NotExpressibleEntry { @@ -884,67 +847,6 @@ impl Registry { .or_insert_with(|| ty.clone()); } - // First-seen order, which is what makes the first entry the default - // module. Derived from the elements rather than stored twice. - for element in flat.elements() { - if let Some(crate_name) = element.location().crate_name.as_ref() { - let module = crate_name.replace('-', "_"); - if !registry.source_modules.contains(&module) { - registry.source_modules.push(module); - } - } - } - - for element in flat.elements() { - let crate_name = element.location().crate_name.clone(); - let named = element.name().cloned(); - match element { - Element::Function(f) => { - registry.functions.insert( - f.name.clone(), - (f.origin.syntax.clone(), element.location().clone()), - ); - } - Element::Type(Type::Struct(t)) => { - registry.structs.insert( - t.name.clone(), - (t.origin.syntax.clone(), element.location().clone()), - ); - } - Element::Type(Type::Variant(t)) => { - registry.enums.insert( - t.name.clone(), - (t.origin.syntax.clone(), element.location().clone()), - ); - } - Element::Type(Type::Enum(t)) => { - registry.enums.insert( - t.name.clone(), - (t.origin.syntax.clone(), element.location().clone()), - ); - } - // An anonymous const, re-emitted verbatim. No name means nothing - // can declare it, which is why it is in none of the API maps. - Element::Guard(g) => registry.guards.push(g.clone()), - Element::Constant(c) => { - registry.consts.insert( - c.name.clone(), - (c.origin.syntax.clone(), element.location().clone()), - ); - } - // An `Extern` states that a name exists and its contents do not - // cross. There is no map for that, and adapters have never seen - // one — a type alias was already a no-op here. Reachable through - // [`Self::flat`] for the stages that will want it. - Element::Type(Type::Extern(_)) => {} - // Refused above. - Element::Unsupported(_) => unreachable!("checked before indexing"), - } - if let (Some(ident), Some(crate_name)) = (named, crate_name) { - registry.item_origins.insert(ident, crate_name); - } - } - registry.flat = flat; Ok(registry) } @@ -954,30 +856,66 @@ impl Registry { &self.flat } - /// The origin crate's **module path** for an item ingested via - /// the item's [`SourceLocation`] stamp, or `None` when unknown — - /// callers then fall - /// back to [`Self::default_module`]. - /// Every **named** item the registry indexes — functions, structs, enums, - /// consts — regardless of whether the stream carried an origin stamp. + /// Every **named** item the model holds — functions, structs, either enum + /// shape, consts — regardless of whether the stream carried an origin stamp. /// - /// Lives here, beside the maps, so an adapter that needs "anything the - /// source crate defines" does not enumerate item kinds itself: a new kind - /// is added once, here, instead of drifting in each adapter. Deliberately - /// NOT keyed off [`Self::item_origins`], which holds only the items whose - /// [`SourceLocation::crate_name`] was set — an origin-less hand-built - /// stream indexes items that map never sees, and callers are expected to - /// pair this with `origin_module(..).unwrap_or_else(default_module)`. + /// Lives here so an adapter that needs "anything the source crate defines" + /// does not enumerate element kinds itself: a new kind is taught here once + /// instead of drifting in each adapter. An **alias is deliberately absent** + /// — see the arm below — and callers are expected to pair this with + /// `origin_module(..).unwrap_or_else(default_module)`. pub fn named_item_idents(&self) -> impl Iterator { - self.functions - .keys() - .chain(self.structs.keys()) - .chain(self.enums.keys()) - .chain(self.consts.keys()) + use crate::api::core::flat::{Element, Type}; + self.flat.elements().filter_map(|e| match e { + // An `Extern` names a type without declaring a body, and is + // deliberately absent: its caller decides which names to qualify in + // generated Rust, and qualifying an alias would move that output. + Element::Type(Type::Extern(_)) => None, + Element::Function(_) | Element::Type(_) | Element::Constant(_) => e.name(), + Element::Guard(_) | Element::Unsupported(_) => None, + }) + } + + /// Every **struct or enum** name — either enum shape, never an alias. + /// + /// Named for its population rather than as the iterator form of + /// [`Self::declares_type`], which it is **not**: that predicate counts every + /// declared type, aliases included. This one feeds *"skipping undeclared + /// `#[prebindgen]` struct/enum"*, which names a kind an alias is not — so the + /// two answer differently on purpose, and the names now say so. + fn struct_enum_idents(&self) -> impl Iterator { + use crate::api::core::flat::Type; + self.flat.types().filter_map(|t| match t { + Type::Struct(_) | Type::Variant(_) | Type::Enum(_) => Some(t.name()), + Type::Extern(_) => None, + }) + } + + /// Whether the source declares a type under this name — **including an + /// alias**. + /// + /// The question both type-diagnostic sites ask, shared so they cannot drift. + /// An alias counts because `#[prebindgen] pub type Handle = ..` *is* a + /// declaration of that name: it can be declared bare by an adapter (landing + /// in the no-indexed-body branch above, which is what + /// `ptr_class(ZKeyExpr<'static>)` relies on), so a diagnostic that says + /// "no such captured item" would be false. + /// + /// Distinct from [`Self::struct_enum_idents`], which excludes aliases + /// because it feeds a *"skipping undeclared struct/enum"* warning — a + /// different question, about what an adapter left unclaimed. + fn declares_type(&self, ident: &syn::Ident) -> bool { + self.flat.declared_type(ident).is_some() } + /// The origin crate's **module path** for an item, read off the element's + /// own [`SourceLocation`] stamp, or `None` when unknown — callers then fall + /// back to [`Self::default_module`]. pub fn origin_module(&self, ident: &syn::Ident) -> Option { - let crate_name = self.item_origins.get(ident)?; + // Off the element's own location, which covers both populations: a + // captured item stamped at capture time, and a binding-local fn stamped + // by `add_local_function`. + let crate_name = self.flat.element(&ident)?.location().crate_name.as_ref()?; let module = crate_name.replace('-', "_"); syn::parse_str(&module).ok() } @@ -990,7 +928,8 @@ impl Registry { /// registry-level override could only fix ONE module, which is /// incomplete with chained multi-source streams. pub fn default_module(&self) -> Option { - self.source_modules + self.flat + .source_modules() .first() .and_then(|m| syn::parse_str(m).ok()) } @@ -998,7 +937,8 @@ impl Registry { /// Module paths of every ingested source, ingestion order — e.g. for a /// glob import that must see all sources' items. pub fn all_source_modules(&self) -> Vec { - self.source_modules + self.flat + .source_modules() .iter() .filter_map(|m| syn::parse_str(m).ok()) .collect() @@ -1069,10 +1009,9 @@ impl Registry { .ident .to_string(); let last = tp.path.segments.last().expect("len checked"); - if self.source_modules.contains(&head) { + if self.flat.source_modules().contains(&head) { qualified.push((key.to_string(), last.to_token_stream().to_string())); - } else if self.structs.contains_key(&last.ident) || self.enums.contains_key(&last.ident) - { + } else if self.declares_type(&last.ident) { println!( "cargo:warning=prebindgen: declared type `{}` is path-qualified, but a \ captured #[prebindgen] item `{}` exists — if you meant the source item, \ @@ -1094,7 +1033,7 @@ impl Registry { // Scan declared functions. for ident in &declared.functions { - if let Some((item_fn, _)) = self.functions.get(ident).cloned() { + if let Some(item_fn) = self.flat.function(&ident).map(|f| f.origin.syntax.clone()) { self.scan_fn_signature(&item_fn)?; } else { missing.push(("function", ident.to_string())); @@ -1102,7 +1041,7 @@ impl Registry { } for ident in &declared.ignored_functions { - if !self.functions.contains_key(ident) { + if self.flat.function(&ident).is_none() { println!( "cargo:warning=prebindgen: ignored function `{}` not found among #[prebindgen] items", ident @@ -1115,7 +1054,7 @@ impl Registry { // `extra_required_types`) — but they are referenced by name from // adapter declarations, so a missing one is a hard error. for ident in &declared.helper_functions { - if !self.functions.contains_key(ident) { + if self.flat.function(&ident).is_none() { missing.push(("helper function", ident.to_string())); } } @@ -1125,14 +1064,16 @@ impl Registry { // so the type is required in the output direction only. if let Some(decl_consts) = &declared.consts { for ident in decl_consts { - if let Some((item_const, _)) = self.consts.get(ident).cloned() { + if let Some(item_const) = + self.flat.constant(&ident).map(|c| c.origin.syntax.clone()) + { self.ensure_entry(Direction::Output, &item_const.ty, true); } else { missing.push(("constant", ident.to_string())); } } for ident in &declared.ignored_consts { - if !self.consts.contains_key(ident) { + if self.flat.constant(&ident).is_none() { println!( "cargo:warning=prebindgen: ignored const `{}` not found among #[prebindgen] items", ident @@ -1157,12 +1098,16 @@ impl Registry { let ty = key.to_type(); let mut matched = false; if let Some(ident) = bare_path_ident(&ty) { - if let Some((s, _)) = self.structs.get(&ident).cloned() { + if let Some(s) = self + .flat + .struct_type(&ident) + .map(|s| s.origin.syntax.clone()) + { self.scan_struct(&s)?; self.ensure_entry(Direction::Input, &ty, true); self.ensure_entry(Direction::Output, &ty, true); matched = true; - } else if let Some((e, _)) = self.enums.get(&ident).cloned() { + } else if let Some(e) = self.flat.enum_item(&ident).cloned() { self.scan_enum(&e)?; self.ensure_entry(Direction::Input, &ty, true); self.ensure_entry(Direction::Output, &ty, true); @@ -1181,9 +1126,7 @@ impl Registry { for key in &declared.ignored_types { let ty = key.to_type(); - let matched = bare_path_ident(&ty).is_some_and(|ident| { - self.structs.contains_key(&ident) || self.enums.contains_key(&ident) - }); + let matched = bare_path_ident(&ty).is_some_and(|ident| self.declares_type(&ident)); if !matched { println!( "cargo:warning=prebindgen: ignored type `{}` not found among #[prebindgen] items", @@ -1202,8 +1145,9 @@ impl Registry { && declared.ignored_name_predicates.iter().any(|p| p(name)) }; let mut skipped_fns: Vec = self - .functions - .keys() + .flat + .functions() + .map(|f| &f.name) .filter(|k| { !declared.functions.contains(*k) && !declared.ignored_functions.contains(*k) @@ -1226,7 +1170,7 @@ impl Registry { || declared.ignored_types.contains(key) || declared.boundary_only_types.contains(key) }; - for ident in self.structs.keys().chain(self.enums.keys()) { + for ident in self.struct_enum_idents() { let name = ident.to_string(); let key = TypeKey::from_ident(ident); if !type_acknowledged(&key) && !pred_ignored(&name) { @@ -1243,8 +1187,9 @@ impl Registry { if let Some(decl_consts) = &declared.consts { let mut skipped_consts: Vec = self - .consts - .keys() + .flat + .constants() + .map(|c| &c.name) .filter(|k| { !decl_consts.contains(*k) && !declared.ignored_consts.contains(*k) @@ -1354,7 +1299,7 @@ impl Registry { // No receiver or non-ident pattern can reach here: a captured item was // refused by the frontend and `from_flat` failed before indexing it, and // a binding-local fn was checked against the same grammar - // (`Flat::check_signature`) when `resolve` synthesized it. + // (`Flat::lower_signature`) when `resolve` synthesized it. for input in &f.sig.inputs { match input { syn::FnArg::Receiver(_) => continue, @@ -1490,7 +1435,7 @@ impl Registry { // contribute nothing here. if let Some(name) = bare_path_ident(ty) { use crate::api::core::flat::{Field, Type}; - let fields: Vec<&Field> = match self.flat.declared_type(&name.to_string()) { + let fields: Vec<&Field> = match self.flat.declared_type(&name) { Some(Type::Struct(s)) => s.fields.iter().collect(), Some(Type::Variant(v)) => v .alternatives @@ -1531,18 +1476,21 @@ impl Registry { for (item_fn, origin) in adapter.local_functions() { let ident = item_fn.sig.ident.clone(); // The one input that does not come through `Flat`: a `sig!(..)` is - // written by hand in a build script and inserted straight into the - // maps, so the grammar has to be checked here or nowhere. Silently - // dropping a `self` receiver or a pattern parameter would surface as - // an arity mismatch out of rustc on generated code, which is the - // wrong end of the pipeline to learn about a build.rs typo. - if let Err(error) = self.flat.check_signature(&item_fn) { - return Err(ScanError::AdapterInvariant { - message: format!("binding-local fn `{ident}`: {error}"), + // written by hand in a build script, so the grammar has to be checked + // here or nowhere. Silently dropping a `self` receiver or a pattern + // parameter would surface as an arity mismatch out of rustc on + // generated code, which is the wrong end of the pipeline to learn + // about a build.rs typo. + let lowered = match self.flat.lower_signature(&item_fn) { + Ok(f) => f, + Err(error) => { + return Err(ScanError::AdapterInvariant { + message: format!("binding-local fn `{ident}`: {error}"), + } + .into()) } - .into()); - } - if self.functions.contains_key(&ident) { + }; + if self.flat.element(&ident).is_some() { return Err(ScanError::AdapterInvariant { message: format!( "binding-local fn `{ident}` collides with a `#[prebindgen]` item — \ @@ -1552,9 +1500,9 @@ impl Registry { } .into()); } - self.functions - .insert(ident.clone(), (item_fn, crate::SourceLocation::default())); - self.item_origins.insert(ident, origin); + // Into the model, so every downstream stage finds it exactly where it + // finds a captured fn — there is one index, and this is it. + self.flat.add_local_function(lowered, origin); } let declared = DeclaredItems::from_adapter(&adapter)?; self.scan_declared_items(&declared)?; diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index bf0d7495..ddada5fe 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -645,7 +645,10 @@ fn builder_reads_a_source_directory() { let dir = write_source_dir("plain", "flat-crate", "marked_fn"); let registry: Registry<()> = Registry::builder().source(&dir).build().expect("indexes"); - assert!(registry.functions.contains_key(&fn_ident("marked_fn"))); + assert!(registry + .flat() + .function(&fn_ident("marked_fn").to_string()) + .is_some()); // Dashes normalize to underscores, as they must to be a Rust module path. assert_eq!( registry.default_module().map(module), @@ -708,7 +711,13 @@ fn builder_composes_directories_and_streams() { .expect("indexes"); for name in ["flat_fn", "helper_fn", "synthetic"] { - assert!(registry.functions.contains_key(&fn_ident(name)), "{name}"); + assert!( + registry + .flat() + .function(&fn_ident(name).to_string()) + .is_some(), + "{name}" + ); } // Each directory keeps its own origin; the stream item has none to keep. assert_eq!( @@ -745,12 +754,18 @@ fn builder_and_from_items_agree() { let streamed: Registry<()> = Registry::from_items(crate::Source::new(&dir).items_all()).expect("indexes"); - assert_eq!(built.functions.len(), streamed.functions.len()); + assert_eq!( + built.flat().functions().count(), + streamed.flat().functions().count() + ); assert_eq!( built.default_module().map(module), streamed.default_module().map(module) ); - assert_eq!(built.guards.len(), streamed.guards.len()); + assert_eq!( + built.flat().guards().count(), + streamed.flat().guards().count() + ); } // ── What a table cell knows about its type ───────────────────────────── @@ -900,18 +915,22 @@ fn from_flat_projects_each_element_kind() { let reg: Registry<()> = Registry::from_flat(flat).expect("project"); let id = |n: &str| syn::parse_str::(n).unwrap(); - assert!(reg.functions.contains_key(&id("f"))); - assert!(reg.structs.contains_key(&id("S"))); - assert!(reg.enums.contains_key(&id("Sum")), "a sum is an enum here"); - assert!(reg.enums.contains_key(&id("Flags"))); - assert!(reg.consts.contains_key(&id("K"))); + assert!(reg.flat().function(&id("f").to_string()).is_some()); + assert!(reg.flat().struct_type(&id("S").to_string()).is_some()); + assert!( + reg.flat().enum_item(&id("Sum").to_string()).is_some(), + "a sum is an enum here" + ); + assert!(reg.flat().enum_item(&id("Flags").to_string()).is_some()); + assert!(reg.flat().constant(&id("K").to_string()).is_some()); assert_eq!( - reg.guards.len(), + reg.flat().guards().count(), 2, "both anonymous consts, in stream order" ); assert!( - !reg.structs.contains_key(&id("Handle")) && !reg.enums.contains_key(&id("Handle")), + reg.flat().struct_type(&id("Handle").to_string()).is_none() + && reg.flat().enum_item(&id("Handle").to_string()).is_none(), "an Extern names a type; it declares no body to index" ); @@ -1068,11 +1087,9 @@ fn a_guard_never_reaches_the_const_surface() { ]; let mut reg: Registry<()> = Registry::from_items(items).unwrap(); - assert_eq!(reg.guards.len(), 2); - assert_eq!(reg.consts.len(), 1); - assert!(reg - .consts - .contains_key(&syn::parse_str::("REAL").unwrap())); + assert_eq!(reg.flat().guards().count(), 2); + assert_eq!(reg.flat().constants().count(), 1); + assert!(reg.flat().constant("REAL").is_some()); // An adapter WITH a const mechanism that declares nothing warns about `REAL` // only: a guard is not undeclared API, it is not API. @@ -1082,3 +1099,176 @@ fn a_guard_never_reaches_the_const_surface() { }; reg.scan_declared(&ext).expect("guards are not declarable"); } + +// ── One index: what the deleted maps used to guarantee ───────────────── + +/// `named_item_idents` must **not** name an alias. +/// +/// It used to derive from the four maps, and an `Extern` was in none of them. +/// Derived from the model it would include alias names unless filtered — and its +/// caller uses it to decide which names generated Rust qualifies, so including +/// one would move generated output. This is the assertion that keeps the filter. +#[test] +fn named_item_idents_omits_aliases() { + let reg: Registry<()> = crate::api::test_util::reg_with(&[ + "pub fn f(x: u64) -> u64 { x }", + "pub struct S { pub a: u64 }", + "pub enum E { A }", + "pub const K: u64 = 7;", + "pub type Handle = other::Inner;", + ]); + let names: HashSet = reg.named_item_idents().map(|i| i.to_string()).collect(); + assert_eq!( + names, + ["f", "S", "E", "K"].map(String::from).into_iter().collect(), + "an alias names a type but declares no body; it must not be qualified" + ); +} + +/// A binding-local fn joins the one index, carries its adapter-supplied origin +/// crate — and does **not** join the source-module list. +/// +/// The last part is the subtle one: `source_modules` decides `default_module`, +/// which is what an unqualified reference resolves against. If a fn a build +/// script invented could extend it, adding one would silently change how +/// captured items are qualified. +#[test] +fn a_binding_local_fn_joins_the_index_but_not_the_source_modules() { + let at = SourceLocation { + crate_name: Some("myflat".into()), + ..SourceLocation::default() + }; + let reg: Registry<()> = Registry::from_items(vec![( + syn::parse_quote!( + pub fn captured(x: u64) -> u64 { + x + } + ), + at, + )]) + .unwrap(); + let before_default = reg.default_module(); + let before_all = reg.all_source_modules(); + + let ext = StubExt { + local_fns: vec![( + syn::parse_str("fn helper(v: u64) -> u64 { v }").unwrap(), + "my-helpers".into(), + )], + ..Default::default() + }; + let gen = reg.resolve(ext).expect("resolve"); + let reg = gen.registry(); + + // In the one index, reachable exactly like a captured fn. + assert!(reg.flat().function("helper").is_some()); + // With its own origin crate, which is what qualifies its generated call. + assert_eq!( + reg.origin_module(&syn::parse_str::("helper").unwrap()), + Some(syn::parse_quote!(my_helpers)) + ); + // But the module list is untouched. + assert_eq!(reg.default_module(), before_default); + assert_eq!(reg.all_source_modules(), before_all); + assert!(!reg + .flat() + .source_modules() + .contains(&"my_helpers".to_string())); +} + +/// Both enum shapes answer to `enum_item` — the merge the old `enums` map made, +/// which 30 adapter reads depend on. +#[test] +fn enum_item_answers_for_both_shapes() { + let reg: Registry<()> = crate::api::test_util::reg_with(&[ + "pub enum Sum { A(u64), B }", + "pub enum Flags { X = 1, Y = 2 }", + "pub struct S { pub a: u64 }", + ]); + assert!(reg.flat().enum_item("Sum").is_some(), "a sum"); + assert!(reg.flat().enum_item("Flags").is_some(), "a C-style enum"); + assert!(reg.flat().enum_item("S").is_none(), "not a struct"); + assert!(reg.flat().struct_type("S").is_some()); + assert!(reg.flat().struct_type("Sum").is_none()); +} + +// ── An alias is a declaration of its name ────────────────────────────── + +/// The predicate both type diagnostics gate on counts **every** declared type, +/// alias included. +/// +/// An alias was excluded because the pre-`Flat` code asked the `structs`/`enums` +/// maps, which never held one. That was an artefact of where the answer came +/// from, not a decision: `#[prebindgen] pub type Handle = ..` declares the name +/// `Handle`, an adapter may declare it bare, and a diagnostic that says "no such +/// captured item" about it is simply false. +#[test] +fn every_declared_type_counts_including_an_alias() { + let reg: Registry<()> = crate::api::test_util::reg_with(&[ + "pub struct S { pub a: u64 }", + "pub enum Sum { A(u64), B }", + "pub enum Flags { X = 1 }", + "pub type Handle = other::Inner;", + "pub fn f(x: u64) -> u64 { x }", + "pub const K: u64 = 7;", + ]); + let id = |n: &str| syn::parse_str::(n).unwrap(); + + for name in ["S", "Sum", "Flags", "Handle"] { + assert!( + reg.declares_type(&id(name)), + "`{name}` is a declared type and must count" + ); + } + // Not types: a fn and a const share the flat namespace but declare no type. + for name in ["f", "K", "Absent"] { + assert!(!reg.declares_type(&id(name)), "`{name}` declares no type"); + } + + // The sibling that must NOT change: it feeds a "skipping undeclared + // struct/enum" warning, so an alias — which is neither — stays out. + let bodies: HashSet = reg.struct_enum_idents().map(|i| i.to_string()).collect(); + assert_eq!( + bodies, + ["S", "Sum", "Flags"] + .map(String::from) + .into_iter() + .collect(), + "struct_enum_idents feeds a struct/enum message and must exclude aliases" + ); +} + +/// Both diagnostic sites reach the predicate for an alias, and neither errors. +/// +/// `scan_declared` is the entry point for both: a path-qualified declared type +/// whose tail names an alias (the "did you mean the bare name?" heuristic) and +/// an ignored type that names one (the "not found among #[prebindgen] items" +/// check). The messages themselves are `cargo:warning=` on stdout and are not +/// captured here — what this pins is that an alias flows through the same path a +/// struct does, without the `QualifiedDeclaredTypes` hard error. +#[test] +fn an_alias_flows_through_both_type_diagnostics() { + let build = |declare_qualified: bool| { + let reg: Registry<()> = crate::api::test_util::reg_with(&[ + "pub type Handle = other::Inner;", + "pub fn f(x: u64) -> u64 { x }", + ]); + let mut ext = StubExt::default(); + if declare_qualified { + // Head is NOT a source module, so this is the warn-and-pass-through + // branch rather than the hard error. + ext.types + .insert(TypeKey::parse("foreign::Handle").expect("test type")); + } else { + ext.ignored_types + .insert(TypeKey::parse("Handle").expect("test type")); + } + (reg, ext) + }; + + for qualified in [true, false] { + let (mut reg, ext) = build(qualified); + reg.scan_declared(&ext) + .expect("an alias is a captured item; neither site may fail"); + } +} diff --git a/prebindgen/src/api/core/resolve/tests.rs b/prebindgen/src/api/core/resolve/tests.rs index 947ba6fe..6df599d8 100644 --- a/prebindgen/src/api/core/resolve/tests.rs +++ b/prebindgen/src/api/core/resolve/tests.rs @@ -44,20 +44,14 @@ fn final_invariant_reports_unresolved_field_of_unresolved_struct() { /// that the resolved converter doesn't actually depend on. #[test] fn final_invariant_stops_at_resolved_nodes() { - use crate::{ - api::core::registry::{Direction, Registry, TypeEntry, TypeKey}, - SourceLocation as Loc, - }; - - let mut reg: Registry<()> = Registry::empty(); - - let outer_struct: syn::ItemStruct = syn::parse_str("struct Outer { inner: Inner }").unwrap(); - let inner_struct: syn::ItemStruct = - syn::parse_str("struct Inner { unused: Unrelated }").unwrap(); - reg.structs - .insert(outer_struct.ident.clone(), (outer_struct, Loc::default())); - reg.structs - .insert(inner_struct.ident.clone(), (inner_struct, Loc::default())); + use crate::api::core::registry::{Direction, Registry, TypeEntry, TypeKey}; + + // Through the real scan, so the state under test is one the pipeline can + // actually produce: `Unrelated` is a field type nothing declares. + let mut reg: Registry<()> = crate::api::test_util::reg_with(&[ + "pub struct Outer { pub inner: Inner }", + "pub struct Inner { pub unused: Unrelated }", + ]); // `Outer` required & unresolved; `Inner` RESOLVED (with a dummy // entry); `Unrelated` unresolved but only reachable through Inner. diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index bcaca847..daf80029 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -296,10 +296,10 @@ pub fn apply( // fn's peeled (`Option`/`Vec`/`&`) return type — the typo guard for // `.expand_return(expand_return!(T)…)`. if let Some(declared) = &ed.declared_source { - let (item_fn, _) = registry - .functions - .get(&ed.func) - .cloned() + let item_fn = registry + .flat() + .function(&ed.func) + .map(|f| f.origin.syntax.clone()) .ok_or_else(|| UnfoldError::UnknownFunction(ed.func.clone()))?; let ret = fn_return(&item_fn); if !returns_type(&ret, &TypeKey::from_type(declared)) { @@ -340,7 +340,11 @@ pub fn apply( if accessor_fns.contains(func) { continue; } - let Some((item_fn, _)) = registry.functions.get(func).cloned() else { + let Some(item_fn) = registry + .flat() + .function(&func) + .map(|f| f.origin.syntax.clone()) + else { continue; }; let ret = fn_return(&item_fn); @@ -393,7 +397,11 @@ pub fn apply( // (there is no return-value lane in a callback invocation). A type without // a default deconstructor gets no plan and is delivered whole. for func in declared_fns { - let Some((item_fn, _)) = registry.functions.get(func).cloned() else { + let Some(item_fn) = registry + .flat() + .function(&func) + .map(|f| f.origin.syntax.clone()) + else { continue; }; for input in &item_fn.sig.inputs { @@ -601,7 +609,11 @@ fn wire_fixed_returns( no_converter: bool, ) { for func in declared_fns { - let Some((item_fn, _)) = registry.functions.get(func).cloned() else { + let Some(item_fn) = registry + .flat() + .function(&func) + .map(|f| f.origin.syntax.clone()) + else { continue; }; let ret = fn_return(&item_fn); @@ -682,7 +694,11 @@ fn wire_fixed_callbacks( declared_fns: &std::collections::HashSet, ) -> Result<(), UnfoldError> { for func in declared_fns { - let Some((item_fn, _)) = registry.functions.get(func).cloned() else { + let Some(item_fn) = registry + .flat() + .function(&func) + .map(|f| f.origin.syntax.clone()) + else { continue; }; for input in &item_fn.sig.inputs { @@ -765,7 +781,11 @@ pub fn apply_leaf_vec_folds( // Is the leading-`&`-peeled `bare` one of the nominated single-leaf elements? let is_nominated = |bare: &syn::Type| elem_keys.contains(&TypeKey::from_type(bare)); for func in declared_fns { - let Some((item_fn, _)) = registry.functions.get(func).cloned() else { + let Some(item_fn) = registry + .flat() + .function(&func) + .map(|f| f.origin.syntax.clone()) + else { continue; }; // Output position: `Vec` / `Option>` return. Skip if a plan @@ -938,10 +958,10 @@ fn process_decl( ed: &OutputDecl, ) -> Result<(), UnfoldError> { { - let (item_fn, _) = registry - .functions - .get(&ed.func) - .cloned() + let item_fn = registry + .flat() + .function(&ed.func) + .map(|f| f.origin.syntax.clone()) .ok_or_else(|| UnfoldError::UnknownFunction(ed.func.clone()))?; // The value to decompose: the success return (`Output`) or the @@ -1691,29 +1711,23 @@ fn accessor_signature( registry: &Registry, func: &syn::Ident, ) -> Result<(syn::Type, syn::Type), UnfoldError> { - let (item_fn, _) = registry - .functions - .get(func) + let f = registry + .flat() + .function(&func) .ok_or_else(|| UnfoldError::UnknownAccessor(func.clone()))?; - // First parameter is the receiver `&T`; peel the borrow to get `T`. - let takes = item_fn - .sig - .inputs - .iter() - .find_map(|input| match input { - syn::FnArg::Typed(pt) => Some((*pt.ty).clone()), - _ => None, - }) + // First parameter is the receiver `&T`; peel the borrow to get `T`. The + // borrow is `TypeKind::Ref`, so the peel reads the classification instead of + // re-deciding it from `syn::Type::Reference`. + let first = f + .params + .first() .ok_or_else(|| UnfoldError::UnknownAccessor(func.clone()))?; - let takes = match takes { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other, - }; - let ret: syn::Type = match &item_fn.sig.output { - syn::ReturnType::Default => syn::parse_quote!(()), - syn::ReturnType::Type(_, t) => (**t).clone(), + let takes = match &first.ty.kind { + crate::api::core::flat::TypeKind::Ref { inner, .. } => inner.origin.syntax.clone(), + _ => first.ty.origin.syntax.clone(), }; + let ret: syn::Type = f.ret.origin.syntax.clone(); Ok((takes, ret)) } @@ -1744,16 +1758,11 @@ fn place_is_owned(hoists: &[Hoist], path_prefix: &[PathStep], by_ref: bool) -> b /// compare target types, so `f(v: T)` and `f(v: &T)` are indistinguishable /// there by design. fn accessor_consumes(registry: &Registry, func: &syn::Ident) -> bool { - registry.functions.get(func).is_some_and(|(f, _)| { - f.sig - .inputs - .iter() - .find_map(|input| match input { - syn::FnArg::Typed(pt) => Some(!matches!(*pt.ty, syn::Type::Reference(_))), - _ => None, - }) - .unwrap_or(false) - }) + registry + .flat() + .function(&func) + .and_then(|f| f.params.first()) + .is_some_and(|p| !matches!(p.ty.kind, crate::api::core::flat::TypeKind::Ref { .. })) } fn check_takes( diff --git a/prebindgen/src/api/core/write.rs b/prebindgen/src/api/core/write.rs index 2d593c5a..cbbe747c 100644 --- a/prebindgen/src/api/core/write.rs +++ b/prebindgen/src/api/core/write.rs @@ -7,7 +7,7 @@ //! resolves the path against `OUT_DIR`). use std::{ - collections::{BTreeMap, HashMap}, + collections::BTreeMap, path::{Path, PathBuf}, }; @@ -81,49 +81,60 @@ pub fn write_rust, E: Prebindgen>( // via `cargo:warning=` in `Registry::scan_declared`. let declared_fns = ext.declared_functions(); let declared_types = ext.declared_types(); + let flat = registry.flat(); items.extend(parse_items_from_tokens( "on_function", - sorted_items_by_ident(®istry.functions) + sorted_by_name(flat.functions().map(|f| (&f.name, &f.origin.syntax))) .into_iter() .filter(|(ident, _)| declared_fns.contains(*ident)) - .map(|(_, (item, _))| ext.on_function(item, registry)), + .map(|(_, item)| ext.on_function(item, registry)), )?); items.extend(parse_items_from_tokens( "on_struct", - sorted_items_by_ident(®istry.structs) - .into_iter() - .filter(|(ident, _)| declared_types.contains(&TypeKey::from_ident(ident))) - .map(|(_, (item, _))| ext.on_struct(item, registry)), + sorted_by_name(flat.types().filter_map(|t| match t { + crate::api::core::flat::Type::Struct(s) => Some((&s.name, &s.origin.syntax)), + _ => None, + })) + .into_iter() + .filter(|(ident, _)| declared_types.contains(&TypeKey::from_ident(ident))) + .map(|(_, item)| ext.on_struct(item, registry)), )?); + // Both enum shapes emit through `on_enum` and sort together: they were one + // map here before they were two elements, and an adapter re-emitting the + // item does not branch on the distinction. items.extend(parse_items_from_tokens( "on_enum", - sorted_items_by_ident(®istry.enums) - .into_iter() - .filter(|(ident, _)| declared_types.contains(&TypeKey::from_ident(ident))) - .map(|(_, (item, _))| ext.on_enum(item, registry)), + sorted_by_name(flat.types().filter_map(|t| match t { + crate::api::core::flat::Type::Variant(v) => Some((&v.name, &v.origin.syntax)), + crate::api::core::flat::Type::Enum(e) => Some((&e.name, &e.origin.syntax)), + _ => None, + })) + .into_iter() + .filter(|(ident, _)| declared_types.contains(&TypeKey::from_ident(ident))) + .map(|(_, item)| ext.on_enum(item, registry)), )?); // Consts: an adapter WITH a const declaration mechanism // (`declared_consts() == Some(set)`) emits declared consts only, // symmetric with functions; an adapter without one (`None`) gets every // const passed through verbatim via the default `on_const`. Prebindgen's - // own injected feature guards are not consts at all — see `guards` below. + // own injected feature guards are not consts at all — see the guards loop. let declared_consts = ext.declared_consts(); items.extend(parse_items_from_tokens( "on_const", - sorted_items_by_ident(®istry.consts) + sorted_by_name(flat.constants().map(|c| (&c.name, &c.origin.syntax))) .into_iter() .filter(|(ident, _)| { declared_consts .as_ref() .is_none_or(|set| set.contains(*ident)) }) - .map(|(_, (item, _))| ext.on_const(item, registry)), + .map(|(_, item)| ext.on_const(item, registry)), )?); // 3. Anonymous consts, verbatim. Last, and in stream order. Ungated on // purpose: with no name there is nothing for an adapter to declare, so // the const gate above cannot apply to them. - for guard in ®istry.guards { + for guard in flat.guards() { items.push(syn::Item::Const(guard.origin.syntax.clone())); } @@ -173,8 +184,16 @@ fn walk_resolved)>( } } -fn sorted_items_by_ident(map: &HashMap) -> Vec<(&syn::Ident, &T)> { - let mut items: Vec<(&syn::Ident, &T)> = map.iter().collect(); +/// Name-sorted, because emission order is part of the generated file and the +/// model is in source order. Was `sorted_items_by_ident` over the registry's +/// maps; same ordering, read from the one index. +fn sorted_by_name<'a, T>( + items: impl Iterator, +) -> Vec<(&'a syn::Ident, &'a T)> +where + T: 'a, +{ + let mut items: Vec<(&syn::Ident, &T)> = items.collect(); items.sort_by_key(|(left, _)| left.to_string()); items } diff --git a/prebindgen/src/api/core/write/tests.rs b/prebindgen/src/api/core/write/tests.rs index 20e7e976..44f2c057 100644 --- a/prebindgen/src/api/core/write/tests.rs +++ b/prebindgen/src/api/core/write/tests.rs @@ -113,47 +113,34 @@ fn dedup_and_sort() { #[test] fn write_rust_sorts_declared_items_by_ident() { - let mut reg: Registry<()> = Registry::empty(); + // Fed in a deliberately un-sorted order: the assertion below is that + // emission sorts by name, and the model preserves stream order. let loc = SourceLocation::default(); - - reg.functions.insert( - syn::parse_quote!(b_fn), + let items: Vec<(syn::Item, SourceLocation)> = vec![ ( syn::parse_quote!( fn b_fn() {} ), loc.clone(), ), - ); - reg.functions.insert( - syn::parse_quote!(a_fn), ( syn::parse_quote!( fn a_fn() {} ), loc.clone(), ), - ); - reg.structs.insert( - syn::parse_quote!(BStruct), ( syn::parse_quote!( pub struct BStruct; ), loc.clone(), ), - ); - reg.structs.insert( - syn::parse_quote!(AStruct), ( syn::parse_quote!( pub struct AStruct; ), loc.clone(), ), - ); - reg.enums.insert( - syn::parse_quote!(BEnum), ( syn::parse_quote!( pub enum BEnum { @@ -162,9 +149,6 @@ fn write_rust_sorts_declared_items_by_ident() { ), loc.clone(), ), - ); - reg.enums.insert( - syn::parse_quote!(AEnum), ( syn::parse_quote!( pub enum AEnum { @@ -173,25 +157,20 @@ fn write_rust_sorts_declared_items_by_ident() { ), loc.clone(), ), - ); - reg.consts.insert( - syn::parse_quote!(B_CONST), ( syn::parse_quote!( pub const B_CONST: u32 = 2; ), loc.clone(), ), - ); - reg.consts.insert( - syn::parse_quote!(A_CONST), ( syn::parse_quote!( pub const A_CONST: u32 = 1; ), loc, ), - ); + ]; + let reg: Registry<()> = Registry::from_items(items).expect("index"); let unique = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -295,7 +274,7 @@ fn guards_emit_ungated_and_in_stream_order() { ), ]; let registry: Registry<()> = Registry::from_items(items).expect("index"); - assert_eq!(registry.guards.len(), 2); + assert_eq!(registry.flat().guards().count(), 2); let dir = crate::api::test_util::unique_test_dir("write_guards"); std::fs::create_dir_all(&dir).unwrap(); diff --git a/prebindgen/src/api/lang/cbindgen/convert.rs b/prebindgen/src/api/lang/cbindgen/convert.rs index afadaece..9732e57e 100644 --- a/prebindgen/src/api/lang/cbindgen/convert.rs +++ b/prebindgen/src/api/lang/cbindgen/convert.rs @@ -193,10 +193,10 @@ impl Cbindgen { match spec { ConvertSpec::PrebindgenFn(f) => { let item = ®istry - .functions - .get(f) - .unwrap_or_else(|| panic!("Cbindgen conversion function {} was not found", f)) - .0; + .flat() + .function(&f) + .map(|func| &func.origin.syntax) + .unwrap_or_else(|| panic!("Cbindgen conversion function {} was not found", f)); let (repr, by_ref) = one_param(item); let ret = fn_ret(item); let (ok, fallible) = match result_parts(&ret) { @@ -237,10 +237,10 @@ impl Cbindgen { match spec { ConvertSpec::PrebindgenFn(f) => { let item = ®istry - .functions - .get(f) - .unwrap_or_else(|| panic!("Cbindgen conversion function {} was not found", f)) - .0; + .flat() + .function(&f) + .map(|func| &func.origin.syntax) + .unwrap_or_else(|| panic!("Cbindgen conversion function {} was not found", f)); let (param, by_ref) = one_param(item); assert_eq!(TypeKey::from_type(¶m), decl.key); let ret = fn_ret(item); diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index a6ed2e59..5bb74f83 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -55,7 +55,7 @@ impl Cbindgen { ty: &syn::Type, ) -> Option> { let ident = type_path_tail(ty)?; - let (item, _) = registry.enums.get(&ident)?; + let item = registry.flat().enum_item(&ident)?; Some(item.variants.iter().cloned().collect()) } @@ -284,12 +284,11 @@ impl Cbindgen { pub(super) fn produces_array(&self, registry: &Registry<()>) -> bool { self.functions.keys().any(|orig| { registry - .functions - .get(orig) - .map(|(f, _)| match &f.sig.output { - syn::ReturnType::Type(_, ty) => type_contains_vec(ty), - syn::ReturnType::Default => false, - }) + .flat() + .function(&orig) + // The model already decided that an elided return and `-> ()` + // are one thing, so there is no second arm to write here. + .map(|f| type_contains_vec(&f.ret.origin.syntax)) .unwrap_or(false) }) } @@ -303,7 +302,10 @@ impl Cbindgen { ty: &syn::Type, ) -> Option> { let ident = type_path_tail(ty)?; - let (item, _) = registry.structs.get(&ident)?; + let item = registry + .flat() + .struct_type(&ident) + .map(|st| &st.origin.syntax)?; if let syn::Fields::Named(named) = &item.fields { Some( named diff --git a/prebindgen/src/api/lang/cbindgen/mod.rs b/prebindgen/src/api/lang/cbindgen/mod.rs index dd4d4c9d..899bb402 100644 --- a/prebindgen/src/api/lang/cbindgen/mod.rs +++ b/prebindgen/src/api/lang/cbindgen/mod.rs @@ -393,7 +393,7 @@ fn type_short(ty: &syn::Type) -> String { /// The indexed `syn::ItemEnum` for a declared enum type, by tail ident. fn enum_item<'r>(registry: &'r Registry<()>, ty: &syn::Type) -> Option<&'r syn::ItemEnum> { let ident = type_path_tail(ty)?; - registry.enums.get(&ident).map(|(e, _)| e) + registry.flat().enum_item(&ident) } /// Hard error when a `.tagged_union()`-declared enum is unit-only. The diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 47257a51..bdbcdba4 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -810,15 +810,19 @@ impl JniGen { decl: &FieldsDecl, ) -> Vec { let func = &decl.func; - let (item_fn, _) = registry.functions.get(func).unwrap_or_else(|| { - panic!( - "expand_return!({}).fields(fields!({func})): no `#[prebindgen]` function \ + let item_fn = registry + .flat() + .function(&func) + .map(|func| &func.origin.syntax) + .unwrap_or_else(|| { + panic!( + "expand_return!({}).fields(fields!({func})): no `#[prebindgen]` function \ `{func}` — a value form is an accessor `fn {func}(v: &{}) -> {}Struct`", - key.as_str(), - key.as_str(), - key.as_str(), - ) - }); + key.as_str(), + key.as_str(), + key.as_str(), + ) + }); let ret: syn::Type = match &item_fn.sig.output { syn::ReturnType::Type(_, t) => crate::api::core::unfold::peel_ref(t), syn::ReturnType::Default => panic!( @@ -1029,9 +1033,9 @@ impl JniGen { dotted, ); let ident = bare_path_ident(&probe).expect("a sum type is a path type"); - let (item_enum, _) = registry - .enums - .get(&ident) + let item_enum = registry + .flat() + .enum_item(&ident) .expect("TypeKind::Sum implies an indexed enum"); let sum_cfg = self.types[&TypeKey::from_type(&probe)] .sum() @@ -1392,12 +1396,16 @@ impl JniGen { let target = key.to_type(); let result = match decl.input.as_ref()? { ConvertSpec::PrebindgenFn(f) => { - let (item_fn, _) = registry.functions.get(f).unwrap_or_else(|| { - panic!( - "convert!({}).input({f}): function not found among #[prebindgen] items", - key.as_str() - ) - }); + let item_fn = registry + .flat() + .function(&f) + .map(|func| &func.origin.syntax) + .unwrap_or_else(|| { + panic!( + "convert!({}).input({f}): function not found among #[prebindgen] items", + key.as_str() + ) + }); let (param_ty, by_ref) = convert_single_param(key, f, item_fn, "input"); // Return: `T` (infallible) or `Result` (fallible — E // routes to the caller's error handler via the exc slot). @@ -1464,12 +1472,16 @@ impl JniGen { let target = key.to_type(); let result = match decl.output.as_ref()? { ConvertSpec::PrebindgenFn(g) => { - let (item_fn, _) = registry.functions.get(g).unwrap_or_else(|| { - panic!( + let item_fn = registry + .flat() + .function(&g) + .map(|func| &func.origin.syntax) + .unwrap_or_else(|| { + panic!( "convert!({}).output({g}): function not found among #[prebindgen] items", key.as_str() ) - }); + }); let (param_ty, by_ref) = convert_single_param_any(g, item_fn); assert!( TypeKey::from_type(¶m_ty) == *key, diff --git a/prebindgen/src/api/lang/jnigen/jni/classify.rs b/prebindgen/src/api/lang/jnigen/jni/classify.rs index 3a7b3c41..a7256ac6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/classify.rs +++ b/prebindgen/src/api/lang/jnigen/jni/classify.rs @@ -1,6 +1,6 @@ //! One-stop classification of how a bare Rust type is declared to this //! adapter — the single precedence every emitter agrees on instead of each -//! re-deriving it from `TypeConfig` flags and `registry.structs` probes. +//! re-deriving it from `TypeConfig` flags and `registry.flat()` type probes. use super::*; @@ -63,7 +63,11 @@ impl JniGen { } } if let Some(name) = bare_path_ident(bare) { - if let Some((st, _)) = registry.structs.get(&name) { + if let Some(st) = registry + .flat() + .struct_type(&name) + .map(|st| &st.origin.syntax) + { return TypeKind::DataStruct { st, cfg }; } } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 3a79130d..9d6cd965 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -882,7 +882,7 @@ fn build_flat_sum_field( use crate::api::core::types_util::SumSpec; let ident = bare_path_ident(sum_ty)?; - let (item_enum, _) = registry.enums.get(&ident)?; + let item_enum = registry.flat().enum_item(&ident)?; let cfg = ext.types.get(&TypeKey::from_ident(&ident))?; let sum_cfg = cfg.sum()?; let iface_fqn = cfg.name_spec.as_ref().map(|s| ext.fqn_of(s))?; @@ -1133,7 +1133,11 @@ pub(crate) fn build_flat_input_plan( let Some(name) = bare_path_ident(&struct_ty) else { return Ok(None); }; - let Some((st, _)) = registry.structs.get(&name) else { + let Some(st) = registry + .flat() + .struct_type(&name) + .map(|st| &st.origin.syntax) + else { return Ok(None); }; let key = TypeKey::from_type(&struct_ty); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index da7b6be6..dfb6cf38 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -76,9 +76,9 @@ pub(crate) fn primitive_default_for_descriptor(sig: &str) -> TokenStream { /// data-class behind `Option` / `Vec`. (Those are handled by the slower /// [`struct_output_body`] until the synthesizer is widened to wrap them.) /// -/// Classification reads only `ext.types` (`opaque`/`enum_cfg`) and -/// `registry.structs` — both populated before `resolve` — never the output -/// converter table (not yet built at this stage). +/// Classification reads only `ext.types` (`opaque`/`enum_cfg`) and the parsed +/// model (`registry.flat()`) — both populated before `resolve` — never the +/// output converter table (not yet built at this stage). pub(crate) fn synth_value_struct_leaves( ext: &JniGen, registry: &Registry, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index ffb28ac6..8b350f1b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -222,7 +222,7 @@ pub(crate) fn encode_sum_group( let tag_id = &obj_idents[tag_idx]; // A unit variant contributes no leaf, so the arm list is driven by the // enum's own variants, not by the grouped leaves. - let (item_enum, _) = registry.enums.get(&ident).unwrap_or_else(|| { + let item_enum = registry.flat().enum_item(&ident).unwrap_or_else(|| { panic!("jnigen sum unfold: no indexed enum `{ident}` for the decomposed sum") }); let spec = crate::api::core::types_util::SumSpec::from_item_enum(item_enum); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs index e2e4a9b9..c6f672d7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs @@ -65,7 +65,11 @@ pub(crate) fn collect_vec_build_elem_types( ) -> Vec { let declared = ext.declared_functions(); let mut seen: std::collections::BTreeMap = std::collections::BTreeMap::new(); - for (ident, (item_fn, _)) in ®istry.functions { + for (ident, item_fn) in registry + .flat() + .functions() + .map(|f| (&f.name, &f.origin.syntax)) + { if !declared.contains(ident) { continue; } diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index ee9b700a..b4d6f6b8 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -296,13 +296,17 @@ pub(crate) fn validate_bindings( // Declared functions (incl. binding-local synthetics and fn-backed // constants), in deterministic ident order. let declared = ext.declared_functions(); - let mut fn_idents: Vec<&syn::Ident> = registry.functions.keys().collect(); - fn_idents.sort(); - for ident in fn_idents { + // The elements themselves, not their names: looking a name back up would be + // a second hash and an infallible-lookup-that-is-not. `Ident: Ord` is the + // string order, so the sort is unchanged. + let mut fns: Vec<&crate::api::core::flat::Function> = registry.flat().functions().collect(); + fns.sort_by(|a, b| a.name.cmp(&b.name)); + for f in fns { + let ident = &f.name; if !declared.contains(ident) { continue; } - let (item_fn, _) = ®istry.functions[ident]; + let item_fn = &f.origin.syntax; match ext.fn_plan(registry, item_fn) { Ok(plan) => record_symbol(&plan.native_symbol, ident.to_string(), &mut errors), Err(e) => errors.push(e.message(ident)), @@ -312,14 +316,15 @@ pub(crate) fn validate_bindings( // Declared consts: their synthetic nullary getters run through the same // plan machinery (`JniGen::on_const`). if let Some(declared_consts) = ext.declared_consts() { - let mut const_idents: Vec<&syn::Ident> = registry.consts.keys().collect(); - const_idents.sort(); - for ident in const_idents { + let mut consts: Vec<&crate::api::core::flat::Constant> = + registry.flat().constants().collect(); + consts.sort_by(|a, b| a.name.cmp(&b.name)); + for c in consts { + let ident = &c.name; if !declared_consts.contains(ident) { continue; } - let (item_const, _) = ®istry.consts[ident]; - let getter = const_getter_fn(item_const); + let getter = const_getter_fn(&c.origin.syntax); match ext.fn_plan(registry, &getter) { Ok(plan) => record_symbol(&plan.native_symbol, ident.to_string(), &mut errors), Err(e) => errors.push(e.message(&getter.sig.ident)), diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 1f7a3dd2..3e42be84 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -492,7 +492,7 @@ impl JniGen { }) else { continue; }; - let Some((item_enum, _)) = registry.enums.get(&ident) else { + let Some(item_enum) = registry.flat().enum_item(&ident) else { continue; }; let (package, class_name) = match kotlin_fqn.rsplit_once('.') { @@ -548,7 +548,7 @@ impl JniGen { let Some(ident) = bare_path_ident(&ty) else { continue; }; - let Some((item_enum, _)) = registry.enums.get(&ident) else { + let Some(item_enum) = registry.flat().enum_item(&ident) else { continue; }; assert!( @@ -893,7 +893,11 @@ impl JniGen { }) else { continue; }; - let Some((item_struct, _)) = registry.structs.get(&ident) else { + let Some(item_struct) = registry + .flat() + .struct_type(&ident) + .map(|st| &st.origin.syntax) + else { continue; }; @@ -921,7 +925,11 @@ impl JniGen { imports.insert(format!("{}.{}", self.package, self.jni_native_class_name())); } for m in members.iter().filter(|m| m.kind == MemberKind::Method) { - if let Some((item_fn, _)) = registry.functions.get(&m.rust_ident) { + if let Some(item_fn) = registry + .flat() + .function(&m.rust_ident) + .map(|func| &func.origin.syntax) + { if let Some(f) = crate::api::lang::jnigen::jni::render_wrapper_fn( self, item_fn, @@ -951,7 +959,11 @@ impl JniGen { .map(|c| *c) .unwrap_or_else(|| KtClass::companion_object().vis(Vis::Public)); for m in ctors { - if let Some((item_fn, _)) = registry.functions.get(&m.rust_ident) { + if let Some(item_fn) = registry + .flat() + .function(&m.rust_ident) + .map(|func| &func.origin.syntax) + { if let Some(f) = crate::api::lang::jnigen::jni::render_wrapper_fn( self, item_fn, @@ -1067,7 +1079,11 @@ impl JniGen { .collect(); for ident in &declared_idents { { - let Some((item_fn, _loc)) = registry.functions.get(ident) else { + let Some(item_fn) = registry + .flat() + .function(&ident) + .map(|func| &func.origin.syntax) + else { continue; }; for input in &item_fn.sig.inputs { @@ -1433,9 +1449,9 @@ impl JniGen { let iface_short = register_fqn(&iface_fqn, imports); let ident = bare_path_ident(source) .unwrap_or_else(|| panic!("sum builder: `{key}` is not a path type")); - let (item_enum, _) = registry - .enums - .get(&ident) + let item_enum = registry + .flat() + .enum_item(&ident) .unwrap_or_else(|| panic!("sum builder: no indexed enum `{ident}`")); let sum_cfg = self.types[&key] .sum() @@ -1577,9 +1593,9 @@ impl JniGen { let mut file = kt::KtFile::new(&package); let mut imports: BTreeSet = BTreeSet::new(); for entry in &pkg_cfg.functions { - let (item_fn, _loc) = registry - .functions - .get(&entry.rust_ident) + let item_fn = ®istry + .flat() + .function(&entry.rust_ident) .unwrap_or_else(|| { panic!( "write_jni_package: function `{}` registered via .function(...) is \ @@ -1588,6 +1604,7 @@ impl JniGen { entry.rust_ident, ) }); + let item_fn = &item_fn.origin.syntax; let kotlin_name = self.effective_function_name(subpackage, entry); if let Some(f) = render_wrapper_fn(self, item_fn, registry, Some(&kotlin_name), None) { // #52: idiomatic typed overloads for `.split_on_param` @@ -1601,14 +1618,18 @@ impl JniGen { // Declared consts: a private nullary helper + the public // lazily-initialized `val` (see `render_const_val`). for entry in &pkg_cfg.constants { - let (item_const, _loc) = registry.consts.get(&entry.rust_ident).unwrap_or_else(|| { - panic!( - "write_jni_package: const `{}` registered via .constant(...) is \ + let item_const = registry + .flat() + .constant(&entry.rust_ident) + .map(|konst| &konst.origin.syntax) + .unwrap_or_else(|| { + panic!( + "write_jni_package: const `{}` registered via .constant(...) is \ not in the prebindgen registry — check the spelling against the \ matching `#[prebindgen]` Rust const name.", - entry.rust_ident, - ) - }); + entry.rust_ident, + ) + }); reject_handle_const(self, item_const); if let Some((helper, prop)) = render_const_val( self, @@ -1626,9 +1647,9 @@ impl JniGen { // `val` (see `render_constant_fn_val`). The JNINative extern and the // Rust wrapper are the plain declared-function ones. for entry in &pkg_cfg.constant_functions { - let (item_fn, _loc) = registry - .functions - .get(&entry.rust_ident) + let item_fn = ®istry + .flat() + .function(&entry.rust_ident) .unwrap_or_else(|| { panic!( "write_jni_package: constant fn `{}` registered via .constant_fun(...) \ @@ -1637,6 +1658,7 @@ impl JniGen { entry.rust_ident, ) }); + let item_fn = &item_fn.origin.syntax; validate_constant_fn(self, item_fn); if let Some((helper, prop)) = render_constant_fn_val( self, @@ -1689,14 +1711,13 @@ impl JniGen { // shortens types, collects imports, and wraps long signatures (no // derivation-time import set). let mut externs: Vec = Vec::new(); - let mut idents: Vec<&syn::Ident> = registry.functions.keys().collect(); - idents.sort(); - for ident in idents { - if !declared.contains(ident) { + let mut fns: Vec<&crate::api::core::flat::Function> = registry.flat().functions().collect(); + fns.sort_by(|a, b| a.name.cmp(&b.name)); + for f in fns { + if !declared.contains(&f.name) { continue; } - let (item_fn, _loc) = ®istry.functions[ident]; - if let Some(fun) = render_extern_decl(self, item_fn, registry) { + if let Some(fun) = render_extern_decl(self, &f.origin.syntax, registry) { externs.push(fun); } } @@ -1712,7 +1733,11 @@ impl JniGen { .collect(); const_idents.sort_by_key(|i| i.to_string()); for ident in const_idents { - let Some((item_const, _loc)) = registry.consts.get(ident) else { + let Some(item_const) = registry + .flat() + .constant(&ident) + .map(|konst| &konst.origin.syntax) + else { continue; // missing decl already warned by the scan }; let getter = crate::api::lang::jnigen::jni::const_getter_fn(item_const); diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index 7d927f87..5ae4abd0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -126,7 +126,7 @@ pub(crate) struct SumConfig { pub struct FunctionEntry { /// Rust function ident — must match a `#[prebindgen]`-marked free /// function in the registered source module. Looked up by - /// `registry.functions[ident]`. + /// `registry.flat().function(ident)`. pub rust_ident: syn::Ident, /// Kotlin-side name override, set by chaining `.name("...")` after /// the entry's registration. `None` = derive from `rust_ident` via @@ -306,7 +306,7 @@ pub(crate) enum MemberKind { /// JSONL). #[derive(Clone, Debug)] pub(crate) struct ClassMember { - /// Rust function ident (`registry.functions[ident]`). + /// Rust function ident (`registry.flat().function(ident)`). pub rust_ident: syn::Ident, /// Per-member `.name()` override, stored RAW — the effective Kotlin /// name is derived at point of use by [`JniGen::class_method_kotlin_name`] diff --git a/prebindgen/src/api/lang/jnigen/jni/overloads.rs b/prebindgen/src/api/lang/jnigen/jni/overloads.rs index 86128e22..11e94523 100644 --- a/prebindgen/src/api/lang/jnigen/jni/overloads.rs +++ b/prebindgen/src/api/lang/jnigen/jni/overloads.rs @@ -111,8 +111,12 @@ fn arm_erased_sig( ctor: Option<&syn::Ident>, ) -> Vec { match ctor { - Some(cf) => match registry.functions.get(cf) { - Some((item_fn, _)) => item_fn + Some(cf) => match registry + .flat() + .function(&cf) + .map(|func| &func.origin.syntax) + { + Some(item_fn) => item_fn .sig .inputs .iter() @@ -251,7 +255,10 @@ fn variant_typed_params( let origin_kt = kt_param_name(&origin.to_string()); let (names, optional): (Vec, Vec) = match &variant.ctor { Some(cf) => { - let (item_fn, _) = registry.functions.get(cf)?; + let item_fn = registry + .flat() + .function(&cf) + .map(|func| &func.origin.syntax)?; let optional = item_fn .sig .inputs diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index e2b2e511..65ac6ca5 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -309,7 +309,11 @@ pub(crate) fn build_typed_handle( .param(kt::KtParam::new("ptr", kt::KtType::long())), ); for m in members.iter().filter(|m| m.kind == MemberKind::Constructor) { - if let Some((item_fn, _)) = registry.functions.get(&m.rust_ident) { + if let Some(item_fn) = registry + .flat() + .function(&m.rust_ident) + .map(|func| &func.origin.syntax) + { if let Some(f) = render_wrapper_fn( ext, item_fn, @@ -418,7 +422,11 @@ pub(crate) fn build_typed_handle( // (receiver bound to `this`), delegating to the same centralized // `JNINative` extern as a free wrapper would. for m in members.iter().filter(|m| m.kind == MemberKind::Method) { - if let Some((item_fn, _)) = registry.functions.get(&m.rust_ident) { + if let Some(item_fn) = registry + .flat() + .function(&m.rust_ident) + .map(|func| &func.origin.syntax) + { if let Some(f) = render_wrapper_fn( ext, item_fn, @@ -2324,10 +2332,11 @@ fn shape_notes(f: &syn::ItemFn, registry: &Registry) -> Option(registry: &Registry, key: &TypeKey) -> Option { let ident = bare_path_ident(&key.to_type())?; + let name = ident.to_string(); let attrs = registry - .structs - .get(&ident) - .map(|(s, _)| s.attrs.as_slice()) - .or_else(|| registry.enums.get(&ident).map(|(e, _)| e.attrs.as_slice()))?; + .flat() + .struct_type(&name) + .map(|s| s.origin.syntax.attrs.as_slice()) + .or_else(|| registry.flat().enum_item(&name).map(|e| e.attrs.as_slice()))?; crate::api::lang::jnigen::util::doc_string(attrs) } diff --git a/prebindgen/src/api/lang/jnigen/jni/report.rs b/prebindgen/src/api/lang/jnigen/jni/report.rs index eae0ae5f..7b42148c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/report.rs +++ b/prebindgen/src/api/lang/jnigen/jni/report.rs @@ -188,7 +188,11 @@ impl crate::api::core::Generation { ) { let ext = self.adapter(); let registry = self.registry(); - let Some((item_fn, _)) = registry.functions.get(rust_ident) else { + let Some(item_fn) = registry + .flat() + .function(&rust_ident) + .map(|func| &func.origin.syntax) + else { return; }; let Some(f) = render_wrapper_fn(ext, item_fn, registry, kotlin_name, receiver_key) else { diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index ff9dcf4a..ce8694f1 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -488,7 +488,7 @@ fn sum_plan_kind( let ident = bare_path_ident(ty).unwrap_or_else(|| { panic!("fromParts bridge: sealed-class field `{owner}` is not a path type") }); - let (item_enum, _) = registry.enums.get(&ident).unwrap_or_else(|| { + let item_enum = registry.flat().enum_item(&ident).unwrap_or_else(|| { panic!("fromParts bridge: sealed-class field `{owner}` has no indexed enum `{ident}`") }); let key = TypeKey::from_ident(&ident); diff --git a/prebindgen/src/api/lang/jnigen/jni/symbols.rs b/prebindgen/src/api/lang/jnigen/jni/symbols.rs index ef4e8f4e..89975286 100644 --- a/prebindgen/src/api/lang/jnigen/jni/symbols.rs +++ b/prebindgen/src/api/lang/jnigen/jni/symbols.rs @@ -127,8 +127,8 @@ pub(crate) fn validate_symbols(ext: &JniGen, registry: &Registry) -> short.to_string(), format!("sealed class `{key}` itself (its variants' supertype)"), )]); - if let Some((item_enum, _)) = - bare_path_ident(&key.to_type()).and_then(|i| registry.enums.get(&i)) + if let Some(item_enum) = + bare_path_ident(&key.to_type()).and_then(|i| registry.flat().enum_item(&i)) { for v in &item_enum.variants { let name = ext.sum_variant_class_name(sum_cfg, &v.ident); @@ -172,7 +172,11 @@ pub(crate) fn validate_symbols(ext: &JniGen, registry: &Registry) -> // surface signature (base + `.split_on_param` shells) comes from // the SAME `build_wrapper_surface` emission uses — a body-less // prototype, so the validator doesn't pay for body codegen. - if let Some((item_fn, _)) = registry.functions.get(&entry.rust_ident) { + if let Some(item_fn) = registry + .flat() + .function(&entry.rust_ident) + .map(|func| &func.origin.syntax) + { if let Some(s) = build_wrapper_surface(ext, item_fn, registry, Some(&name), None) { for ov in render_param_overloads(ext, item_fn, registry, &s.fun) { add_overload(&fn_scope, &ov, &origin, &mut errors); @@ -213,7 +217,11 @@ pub(crate) fn validate_symbols(ext: &JniGen, registry: &Registry) -> for m in &ext.class_members[key] { let name = ext.effective_method_name(key, m); check_ident(&name, &format!("method `{}`", m.rust_ident), &mut errors); - let Some((item_fn, _)) = registry.functions.get(&m.rust_ident) else { + let Some(item_fn) = registry + .flat() + .function(&m.rust_ident) + .map(|func| &func.origin.syntax) + else { continue; }; let (scope, receiver) = match m.kind { @@ -272,7 +280,11 @@ fn warn_derived_name_changes(ext: &JniGen, registry: &Registry) { Some(i) => i, None => continue, }; - if let Some((s, _)) = registry.structs.get(&ident) { + if let Some(s) = registry + .flat() + .struct_type(&ident) + .map(|st| &st.origin.syntax) + { for f in &s.fields { if let Some(fname) = &f.ident { let camel = kt_snake_to_camel(&fname.to_string()); @@ -285,7 +297,7 @@ fn warn_derived_name_changes(ext: &JniGen, registry: &Registry) { } } } - if let Some((e, _)) = registry.enums.get(&ident) { + if let Some(e) = registry.flat().enum_item(&ident) { for v in &e.variants { let screaming = crate::api::lang::jnigen::util::camel_to_screaming_snake(&v.ident.to_string()); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs index f175c6bb..52b9660c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs @@ -565,10 +565,11 @@ fn fn_plan_memo_shares_one_derivation() { let gen = registry.resolve(jni).expect("resolve"); let (ext, registry) = (gen.adapter(), gen.registry()); let f = ®istry - .functions - .get(&syn::parse_str::("z_do_thing").unwrap()) + .flat() + .function("z_do_thing") .expect("indexed") - .0; + .origin + .syntax; // The plan is already in the memo (populated at resolve by validation) — // repeated lookups return the SAME allocation, and it equals what a fresh diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 8215332c..80211279 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -387,9 +387,9 @@ impl JniGen { // (`named_item_idents`) where a new kind is added once. // // The NAME SET is independent of origin stamps and the VALUE falls back - // to the default module: an origin-less hand-built stream indexes items - // that `item_origins` never sees, and those still need qualifying (core - // documents `crate` as their module). + // to the default module: an origin-less hand-built stream holds elements + // whose location carries no crate name, and those still need qualifying + // (core documents `crate` as their module). let length_names: std::collections::HashMap = registry .named_item_idents() .map(|ident| { @@ -988,7 +988,10 @@ impl Prebindgen for JniGen { registry: &Registry, ) -> Vec { let mut out = Vec::new(); - for (ident, (item_struct, _loc)) in ®istry.structs { + for (ident, item_struct) in registry.flat().types().filter_map(|t| match t { + crate::api::core::flat::Type::Struct(s) => Some((&s.name, &s.origin.syntax)), + _ => None, + }) { let source: syn::Type = syn::parse_quote!(#ident); let key = TypeKey::from_type(&source); // A `data_class` is a registered type that is neither an opaque @@ -1044,7 +1047,7 @@ impl Prebindgen for JniGen { let Some(ident) = bare_path_ident(&source) else { continue; }; - let Some((item_enum, _)) = registry.enums.get(&ident) else { + let Some(item_enum) = registry.flat().enum_item(&ident) else { continue; }; out.push(crate::api::core::unfold::SumDecon { @@ -1073,11 +1076,14 @@ impl Prebindgen for JniGen { out.push(bare); } }; - for (item_fn, _loc) in registry.functions.values() { - // `Vec` / `Option>` return. - if let syn::ReturnType::Type(_, ret) = &item_fn.sig.output { + for f in registry.flat().functions() { + let item_fn = &f.origin.syntax; + // `Vec` / `Option>` return. The model's `ret` already + // normalizes an elided return to `()`, so there is no arm for it. + { + let ret = &f.ret.origin.syntax; let after_opt = - crate::api::core::types_util::option_inner_type(ret).unwrap_or((**ret).clone()); + crate::api::core::types_util::option_inner_type(ret).unwrap_or(ret.clone()); if let Some(elem) = crate::api::core::types_util::vec_inner_type(&after_opt) { consider(peel_leading_ref(&elem)); } @@ -1210,7 +1216,11 @@ impl Prebindgen for JniGen { for (key, members) in &self.class_members { for m in members { // A registry-absent fn already hard-errored in the scan. - let Some((item_fn, _)) = registry.functions.get(&m.rust_ident) else { + let Some(item_fn) = registry + .flat() + .function(&m.rust_ident) + .map(|func| &func.origin.syntax) + else { continue; }; match m.kind { @@ -1277,7 +1287,11 @@ impl Prebindgen for JniGen { // something that must not exist. Reject them here, where the message // can say what is actually unsupported and what to write instead. for ident in self.declared_functions() { - let Some((item_fn, _)) = registry.functions.get(&ident) else { + let Some(item_fn) = registry + .flat() + .function(&ident) + .map(|func| &func.origin.syntax) + else { continue; }; // (1) A sum in the `Ok` position of a fallible return. A sum is @@ -1670,7 +1684,7 @@ impl JniGen { if let Some(cfg) = self.types.get(&key) { if cfg.is_enum_class() { if let Some(name) = bare_path_ident(ty) { - if let Some((e, _)) = registry.enums.get(&name) { + if let Some(e) = registry.flat().enum_item(&name) { let (wire, body) = enum_input_body(self, registry, e); let niches = default_niches_for_wire(&wire); let kotlin_name = cfg @@ -1772,7 +1786,7 @@ impl JniGen { // OUTPUT direction has no counterpart: a sum crosses Rust → // Kotlin flattened, always.) if self.types.get(&key).is_some_and(|c| c.sum().is_some()) { - if let Some((e, _)) = registry.enums.get(&name) { + if let Some(e) = registry.flat().enum_item(&name) { let (wire, body) = sum_input_body(self, e, registry)?; // The wire's own null niche, exactly as a data class gets // — that is what lets `Option` fold with JVM null as @@ -1793,7 +1807,11 @@ impl JniGen { }); } } - if let Some((s, _)) = registry.structs.get(&name) { + if let Some(s) = registry + .flat() + .struct_type(&name) + .map(|st| &st.origin.syntax) + { let (wire, body) = struct_input_body(self, s, registry)?; let niches = default_niches_for_wire(&wire); // Auto-generated struct: the value-context Kotlin name is @@ -1884,7 +1902,7 @@ impl JniGen { if let Some(cfg) = self.types.get(&key) { if cfg.is_enum_class() { if let Some(name) = bare_path_ident(ty) { - if let Some((e, _)) = registry.enums.get(&name) { + if let Some(e) = registry.flat().enum_item(&name) { let (wire, body) = enum_output_body(self, e); let niches = default_niches_for_wire(&wire); let kotlin_name = cfg @@ -1980,7 +1998,11 @@ impl JniGen { }); } if let Some(name) = bare_path_ident(ty) { - if let Some((s, _)) = registry.structs.get(&name) { + if let Some(s) = registry + .flat() + .struct_type(&name) + .map(|st| &st.origin.syntax) + { let (wire, body) = struct_output_body(self, s, registry)?; let niches = default_niches_for_wire(&wire); let kotlin_name = self From 5852c75c257e77c1cf59410b9c0be03ec38e057c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 31 Jul 2026 11:42:10 +0200 Subject: [PATCH 09/52] Flat owns the type index too (#246) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Flat owns the type index too `Registry::type_refs` was a `HashMap` built in `from_flat` by walking `flat.type_refs()`, with one consumer: `ensure_entry`, deciding whether a cell's subject is the frontend's reading or an adapter-authored type. An index over `Flat`'s own content, held outside `Flat` — and `Flat::type_refs()` had no other caller, so the public iterator existed only to feed it. #243 made `Flat` the only *item* index; this was the last one left. `Flat` gains `by_type` and `type_ref(&syn::Type) -> Option<&TypeRef>`, so **`from_flat` collapses to what it always should have been**: check expressibility, store the model. Everything still in `Registry` is now genuinely its own — the two type tables and the five adapter-declared plan maps. **A binding-local fn's parameter types are now indexed**, which the old ordering got wrong: the index was built in `from_flat`, local fns are inserted later by `resolve`, so their types missed it and their cells came out `Adapter` — "no frontend reading" — though `lower_signature` had produced `TypeRef`s for them. `add_local_function` feeds the index. Deliberately unlike `source_modules`, which stays frozen because it decides `default_module` and would change how *captured* items are qualified; this only makes a cell tell the truth about a reading that already exists. **One definition of canonical spelling.** Two things must agree on what a type is called — this index and `TypeKey`. Adding a second copy of "prelude-normalize, then token string" would have made that worse, so it moved to `types_util::{canonical_type, canonical_spelling}` and `TypeKey::from_type` now derives from it. Fewer definitions than before, not more. Generated output is byte-identical and covertest passes 48 sections — reported because it is worth knowing, not because it was a design constraint: an architecture change whose output moves without changing semantics or performance would have been equally fine. 592 + 520 tests. The new one covers the case the local-fn fix exists for and fails when `add_local_function` stops feeding the index. Co-Authored-By: Claude Opus 5 * An absent source position stays absent in diagnostics Review found a failure-path regression this PR introduced, and reproducing it against the base showed it was two faults, not one: | | base | this PR before the fix | |---|---|---| | a type only a local fn writes | `error: …` ✓ | `:0:0: error: …` ← the regression | | a captured item with no position | `:0:0: error: …` | `:0:0: error: …` ← pre-existing | `lower_signature` lowers a `sig!(..)` against `SourceLocation::default()` — `Origin` requires a location and a build-script signature has no file. Indexing those types flipped their cells from `Adapter` to `Source`, and `TypeSubject::location()` returned the default unconditionally, so the diagnostic printed a position that reads as real. The same fault already showed for any hand-built stream, whose captured items carry default locations too. **Having a reading and having a reportable position are different facts.** The classification fix stays — those types genuinely do have readings — and `SourceLocation::has_position()` names the other one, on the type that owns the question. `TypeSubject::location()` filters on it, which fixes the regression and the pre-existing case through the same path. The test pins both directions: a local-only type reports without a position while still being reported at all, and a captured item with a real position still prints `src/lib.rs:12:3`. It fails if the filter is removed **and** if `has_position` starts answering `false` for everything. Also: `Flat::type_refs()` is deleted rather than re-documented. It was added in #239 to feed the registry's index, that index now lives inside `Flat`, and it has zero callers — a public iterator whose docs told consumers to build exactly the map `type_ref` now is. `index_types_of` walks `element_type_refs` directly, so nothing depended on it. 593 + 521 tests, byte-identical generation, covertest 48 sections, MSRV clippy clean, doc-link warnings unchanged at 16. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- prebindgen/src/api/core/flat/mod.rs | 74 +++++++--- prebindgen/src/api/core/registry.rs | 37 ++--- prebindgen/src/api/core/registry/tests.rs | 163 ++++++++++++++++++++++ prebindgen/src/api/core/types_util.rs | 23 +++ prebindgen/src/api/record.rs | 14 ++ 5 files changed, 263 insertions(+), 48 deletions(-) diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index 95255bd8..a905a9ce 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -388,11 +388,16 @@ impl FlatBuilder { } } } - Ok(Flat { + let mut flat = Flat { elements, by_name, source_modules, - }) + by_type: std::collections::HashMap::new(), + }; + for i in 0..flat.elements.len() { + flat.index_types_of(i); + } + Ok(flat) } } @@ -438,6 +443,21 @@ pub struct Flat { /// API. Positions rather than clones, so there is one copy of each element /// and source order stays available. by_name: std::collections::HashMap, + /// Normalized type spelling → this module's reading of it. + /// + /// Every type the API **mentions** — a parameter, a return, a field, a + /// constant's type, and everything nested inside those — keyed so a consumer + /// holding a `syn::Type` can ask what the frontend made of it without + /// lowering it a second time. + /// + /// A type mentioned in several places keeps the **first mention in element + /// order**, a property of the model rather than of ingestion order. + /// + /// Unlike [`Self::source_modules`] this *is* extended by + /// [`Self::add_local_function`]: a binding-local fn's parameter types have + /// readings like any others, and a lookup that missed them would report + /// "no reading" for one that exists. + by_type: std::collections::HashMap, } /// A name a lookup can be performed with. @@ -571,24 +591,6 @@ impl Flat { }) } - /// Every type the API **mentions**, at every nesting depth — as distinct from - /// [`Self::types`], which is every type it **declares**. - /// - /// A parameter, a return, a field, a constant's type, and everything reachable - /// inside those. The same type mentioned in several places yields one - /// [`TypeRef`] per mention, each with its own spelling and origin; a consumer - /// that wants one per type indexes them and picks, and element order makes - /// that pick deterministic. - /// - /// This is how a later stage gets the frontend's reading of a type it holds - /// only as syntax, without lowering it a second time. - pub fn type_refs(&self) -> impl Iterator { - self.elements - .iter() - .flat_map(element_type_refs) - .flat_map(TypeRef::walk) - } - /// The `struct` declared under this name, or `None` for any other shape. /// /// A tuple struct is an [`Extern`] rather than a `Struct`, so this answers @@ -634,6 +636,37 @@ impl Flat { }) } + /// This module's reading of `ty`, if the flat API mentions that type. + /// + /// `None` means no captured item and no binding-local fn writes this type — + /// it is one the binding invented, and there is nothing for the frontend to + /// have decided about it. + /// + /// The argument is normalized the way [`TypeKey`](crate::core::TypeKey) does + /// before lookup, so an adapter-authored spelling finds the same entry a + /// captured one does. + pub fn type_ref(&self, ty: &syn::Type) -> Option<&TypeRef> { + self.by_type + .get(&crate::api::core::types_util::canonical_spelling(ty)) + } + + /// Index every type the element at `pos` writes. Idempotent per key: the + /// first mention in element order wins. + fn index_types_of(&mut self, pos: usize) { + let refs: Vec = element_type_refs(&self.elements[pos]) + .into_iter() + .flat_map(TypeRef::walk) + .cloned() + .collect(); + for ty in refs { + self.by_type + .entry(crate::api::core::types_util::canonical_spelling( + &ty.origin.syntax, + )) + .or_insert(ty); + } + } + /// Every item the language could not express, with its diagnosis. /// /// Present in the model so a consumer can inspect what a source crate marked @@ -693,6 +726,7 @@ impl Flat { }); self.by_name.insert(f.name.to_string(), self.elements.len()); self.elements.push(Element::Function(f)); + self.index_types_of(self.elements.len() - 1); } /// The declaration a reference denotes. diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs index 6991a3a2..699ae4a0 100644 --- a/prebindgen/src/api/core/registry.rs +++ b/prebindgen/src/api/core/registry.rs @@ -102,11 +102,9 @@ impl TypeKey { /// Build a key directly from a `syn::Type` (normalizing a clone; the /// input is not modified). pub fn from_type(ty: &syn::Type) -> Self { - let mut t = ty.clone(); - crate::api::core::types_util::normalize_type( - &mut t, - &crate::api::core::types_util::Normalization::prelude(), - ); + // Off the shared reduction, so this key and the model's type index + // cannot drift apart about what a type is called. + let t = crate::api::core::types_util::canonical_type(ty); Self { canon: t.to_token_stream().to_string().into(), ty: std::rc::Rc::new(t), @@ -157,7 +155,11 @@ impl TypeSubject { /// Where the source wrote this type, or `None` when no source did. pub fn location(&self) -> Option<&SourceLocation> { match self { - TypeSubject::Source(t) => Some(&t.origin.location), + // Having a reading and having a reportable position are different + // facts: a binding-local fn's types are lowered — so they have + // readings — against no file at all. Reporting `:0:0` would invent a + // position; `None` says what is true. + TypeSubject::Source(t) => Some(&*t.origin.location).filter(|l| l.has_position()), TypeSubject::Adapter(_) => None, } } @@ -299,15 +301,6 @@ pub struct Registry { pub input_types: HashMap>, pub output_types: HashMap>, - /// The frontend's reading of every type the flat API mentions, keyed the way - /// the tables are, so a cell gets its [`TypeSubject::Source`] by lookup - /// instead of by lowering the type a second time. - /// - /// Built once from [`Self::flat`] before anything is scanned. A type - /// mentioned in several places keeps the first mention in **element order** — - /// a property of the model, not of the order items were fed in. - type_refs: HashMap, - /// Resolved constructor-expansion plans, keyed by `(function, parameter)`. /// Filled by [`crate::api::core::expand::apply`] before resolution; read /// by language adapters at the parameter-emission site. Empty unless the @@ -359,7 +352,6 @@ impl Registry { flat: crate::api::core::flat::Flat::default(), input_types: Default::default(), output_types: Default::default(), - type_refs: HashMap::new(), expansion_plans: HashMap::new(), unfold_plans: HashMap::new(), error_plans: HashMap::new(), @@ -836,17 +828,6 @@ impl Registry { } let mut registry = Registry::empty(); - // Every type the model mentions, indexed the way the type tables are. - // Read up front, from the whole model: which mention of a repeated type - // wins is then decided by element order rather than by when a scan - // happened to reach it. - for ty in flat.type_refs() { - registry - .type_refs - .entry(TypeKey::from_type(&ty.origin.syntax)) - .or_insert_with(|| ty.clone()); - } - registry.flat = flat; Ok(registry) } @@ -1391,7 +1372,7 @@ impl Registry { /// adapter-authored type otherwise. fn ensure_entry(&mut self, dir: Direction, ty: &syn::Type, root: bool) { let key = TypeKey::from_type(ty); - let subject = match self.type_refs.get(&key) { + let subject = match self.flat.type_ref(ty) { Some(t) => TypeSubject::Source(t.clone()), None => TypeSubject::Adapter(key.to_type()), }; diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index ddada5fe..79794b60 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -1272,3 +1272,166 @@ fn an_alias_flows_through_both_type_diagnostics() { .expect("an alias is a captured item; neither site may fail"); } } + +/// A type only a **binding-local** fn writes still has a frontend reading, and +/// its cell must say so. +/// +/// The ordering that made this wrong: the type index used to be built in +/// `from_flat`, while local fns are inserted later by `resolve`. So their +/// parameter types missed the index and their cells came out `Adapter` — "no +/// reading" — even though `lower_signature` had produced `TypeRef`s for them. +/// `Flat` owns the index now and `add_local_function` feeds it. +#[test] +fn a_type_only_a_local_fn_writes_still_has_a_reading() { + use crate::api::core::flat::TypeKind; + + /// Resolves anything to itself, so declaring the local fn does not also + /// require an adapter that can convert its types. + struct AnyConverterExt(StubExt); + impl AnyConverterExt { + fn converter(ty: &syn::Type) -> Option> { + Some(ConverterImpl { + destination: ty.clone(), + function: syn::parse_quote!( + fn __id() {} + ), + pre_stages: vec![], + subs: vec![], + niches: Niches::empty(), + metadata: (), + }) + } + } + impl Prebindgen for AnyConverterExt { + type Metadata = (); + fn declared_functions(&self) -> HashSet { + self.0.declared_functions() + } + fn local_functions(&self) -> Vec<(syn::ItemFn, String)> { + self.0.local_functions() + } + fn on_function(&self, f: &syn::ItemFn, r: &Registry<()>) -> TokenStream { + self.0.on_function(f, r) + } + fn on_struct(&self, st: &syn::ItemStruct, r: &Registry<()>) -> TokenStream { + self.0.on_struct(st, r) + } + fn on_enum(&self, e: &syn::ItemEnum, r: &Registry<()>) -> TokenStream { + self.0.on_enum(e, r) + } + fn on_input_type(&self, t: &syn::Type, _r: &Registry<()>) -> Option> { + Self::converter(t) + } + fn on_output_type(&self, t: &syn::Type, _r: &Registry<()>) -> Option> { + Self::converter(t) + } + } + + // `Option` appears nowhere in the captured stream. + let reg: Registry<()> = + Registry::from_items(vec![fn_item("fn captured(x: u64) -> u64 { x }")]).unwrap(); + assert!( + reg.flat() + .type_ref(&syn::parse_quote!(Option)) + .is_none(), + "fixture precondition: the captured stream never writes this type" + ); + + let ext = AnyConverterExt(StubExt { + local_fns: vec![( + syn::parse_str("fn helper(v: Option) -> u64 { 0 }").unwrap(), + "helpers".into(), + )], + functions: ["helper"] + .iter() + .map(|s| syn::parse_str(s).unwrap()) + .collect(), + ..Default::default() + }); + let gen = reg.resolve(ext).expect("resolve"); + let reg = gen.registry(); + + // The model now holds the reading … + let read = reg + .flat() + .type_ref(&syn::parse_quote!(Option)) + .expect("a local fn's parameter type is in the model"); + assert!(matches!(read.kind, TypeKind::Optional(_))); + + // … and the cell scanned from that parameter carries it, rather than + // claiming the type is one the binding invented. + let cell = ®.input_types[&TypeKey::parse("Option").expect("test type")]; + assert!( + matches!(cell.subject, TypeSubject::Source(_)), + "the frontend read this type; the cell must not call it adapter-authored" + ); + assert!(matches!(cell.subject.kind(), Some(TypeKind::Optional(_)))); +} + +/// A type with no source position must not get an invented one. +/// +/// Three facts have to stay apart: a type can have a **frontend reading** +/// (`TypeSubject::Source`), a **reportable position**, or neither. A +/// binding-local fn's parameter types have the first and not the second — +/// `lower_signature` lowers them against `SourceLocation::default()`, since +/// `Origin` needs a location and a `sig!(..)` has no file. +/// +/// Indexing those types (this PR) flipped their cells from `Adapter` to +/// `Source`, and `location()` returned the default unconditionally, so the +/// diagnostic read `:0:0: error:` — a position that looks real. The same fault +/// already showed for any hand-built stream, whose captured items also carry +/// default locations; both are fixed by asking whether the location has a +/// position at all. +#[test] +fn an_unresolved_type_without_a_position_reports_none() { + let reg: Registry<()> = + Registry::from_items(vec![fn_item("fn captured(x: u64) -> u64 { x }")]).unwrap(); + let ext = StubExt { + local_fns: vec![( + syn::parse_str("fn helper(v: Option) -> u64 { 0 }").unwrap(), + "helpers".into(), + )], + functions: ["helper"] + .iter() + .map(|s| syn::parse_str(s).unwrap()) + .collect(), + ..Default::default() + }; + // `StubExt` supplies no converters, so every scanned type is unresolved: + // `Option` reached only through the local fn, `u64` through both. + let err = reg.resolve(ext).expect_err("StubExt resolves nothing"); + let msg = err.to_string(); + + assert!( + !msg.contains(":0:0:"), + "no file and no line means no position to print:\n{msg}" + ); + assert!( + msg.contains("error: unresolved prebindgen input type `Option < u64 >`"), + "the local-only type is still reported, just without a position:\n{msg}" + ); + + // A captured item that DOES have a position still reports it. + let located: Registry<()> = Registry::from_items(vec![( + syn::parse_quote!( + pub fn f(x: u64) -> u64 { + x + } + ), + SourceLocation { + file: "src/lib.rs".into(), + line: 12, + column: 3, + crate_name: Some("myflat".into()), + }, + )]) + .unwrap(); + let mut ext = StubExt::default(); + ext.functions.insert(syn::parse_str("f").unwrap()); + let err = located.resolve(ext).expect_err("StubExt resolves nothing"); + assert!( + err.to_string().contains("src/lib.rs:12:3: error:"), + "a real position must still be reported:\n{}", + err + ); +} diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index 6a28eaf2..5692bfeb 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -168,6 +168,29 @@ fn constructor_key(path: &syn::Path) -> String { out } +/// A type reduced to the spelling everything keys on: prelude-normalized, so +/// `std::option::Option` and `Option` are one entry. +/// +/// The **single** definition of that reduction. Two things key on it — the +/// model's type index ([`Flat::type_ref`](crate::core::flat::Flat::type_ref)) +/// and [`TypeKey`](crate::core::TypeKey) — and they have to agree, so neither +/// spells it out itself. +/// +/// Deliberately `prelude()` rather than a source-module-aware normalization: a +/// key must mean the same thing before and after ingestion knows what the source +/// modules are. +pub fn canonical_type(ty: &syn::Type) -> syn::Type { + let mut t = ty.clone(); + normalize_type(&mut t, &Normalization::prelude()); + t +} + +/// [`canonical_type`] as tokens — the string form both indexes use as their key. +pub fn canonical_spelling(ty: &syn::Type) -> String { + use quote::ToTokens; + canonical_type(ty).to_token_stream().to_string() +} + pub fn normalize_type(ty: &mut syn::Type, against: &Normalization) { use syn::visit_mut::VisitMut; struct Normalizer<'a> { diff --git a/prebindgen/src/api/record.rs b/prebindgen/src/api/record.rs index 7e2f6582..bddf5298 100644 --- a/prebindgen/src/api/record.rs +++ b/prebindgen/src/api/record.rs @@ -47,6 +47,20 @@ impl std::fmt::Display for SourceLocation { } impl SourceLocation { + /// Whether this names an actual place in an actual file. + /// + /// Not everything with a `SourceLocation` has one. A signature a build + /// script wrote (`sig!(..)`), or a hand-built item stream in a test, is + /// lowered against [`SourceLocation::default`] because [`Origin`] requires + /// *a* location — but there is no file and no line, and a diagnostic that + /// renders it anyway emits `:0:0:`, which reads as a real position and is + /// worse than saying nothing. + /// + /// [`Origin`]: crate::core::flat::Origin + pub fn has_position(&self) -> bool { + !self.file.is_empty() + } + pub fn from_span(span: &proc_macro2::Span) -> Self { if_rust_version::if_rust_version! { >= 1.88 { // Convert proc_macro2::Span to proc_macro::Span to access file() method From 0746b8e1a9075f922b9a129b600dd1d21deefdcb Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Fri, 31 Jul 2026 11:55:24 +0200 Subject: [PATCH 10/52] Bring the stage map up to what the program actually did MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The map had drifted from the code on almost every concrete claim, and it is the document the umbrella PR mirrors — so the umbrella was wrong too. **Names and shapes.** `core::language` → `core::flat`, and the design section described structs that no longer exist: `Type { kind, syntax }` is `TypeRef { kind, origin }`; `Param` carries an `Origin`; `Variant` is a sum with `alternatives` while the C-style `Enum` is a separate entity with `values` — the old single `Variant { tag, discriminant, fields }` conflated both. The where-does-each-fact-live table pointed at fields that were renamed or split. **L0's own checklist was wrong about L0**: it listed a `Passthrough` variant that #227 deleted during that stage. Restated at the level that survived, with the variant list left to L0.5 where it is accurate. **L0.5 claimed two things that later changed**: `MaybeUninit` became `RefMode::Out`, not `TypeKind::Uninit` — an out-parameter is a property of the borrow, not a wrapper type — and `Cow<'_, [u8]>` is no longer open, since #236 made it transparent like `Box`. `Duration` remains genuinely open. **L1.5 is new**, and recording it is the point of this commit: #239–#246 were not a planned stage, they fell out of reviewing L1, and the map should show where the program went rather than where it was aimed. Seven registry fields deleted, the type table carrying the frontend's reading, `Guard`, `Name`, aliases counting as declarations, and the type index moving to its owner. **The numbers are re-measured, and one of them is unflattering**: the ledger is still **202**. L1.5 deleted 113 map reads but took only two classifiers off the ledger. Saying "still 202, the ledger has not started falling" is the honest report; L2 is where it does. Per-area counts corrected (`api/core` 71, `jnigen` 106, `unfold` 16, `registry` 11). **L5's first bullet is already done** — L1.5 deleted the public item maps outright. **The review protocol is rewritten.** It said byte-identical artifacts were a gate and "a diff is a bug". That is backwards, and I had been applying it: in #243 it argued me out of a correct fix because the fix would have emitted one extra `cargo:warning=`. `regen-check.sh` is instrumentation — it says what moved, not whether the change was allowed. Output that moves without changing semantics or performance is fine, and no architecture decision may be reshaped to keep bytes matching. The protocol now also states how to run the check so it means anything: clean `examples/` first, then `cargo clean` the two crates, because it only regenerates what cargo rebuilds. Co-Authored-By: Claude Opus 5 --- docs/language-integration.md | 208 +++++++++++++++++++++++++---------- 1 file changed, 148 insertions(+), 60 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 719387e1..a72e287d 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -1,7 +1,7 @@ # Parse once, consume elements everywhere — integration map -Umbrella for making every component of prebindgen consume -[`core::language`]'s `Element`s instead of parsing captured Rust itself. +Umbrella for making every component of prebindgen consume [`core::flat`]'s +`Element`s instead of parsing captured Rust itself. [#211](https://github.com/milyin/prebindgen/issues/211) remains the authority on the invariants and the frontend/adapter boundary. This document does not restate @@ -13,23 +13,32 @@ the umbrella PR body — never the other way round. ## The design ``` -Source(s) ──items──> Language ──Elements──> Registry ──> adapters - raw records parse + indexes classify off `kind` - (syn::Item) validate elements spell off `syntax` +Source(s) ──items──> Flat ──Elements──> Registry ──> adapters + raw records parse + projects classify off `kind` + (syn::Item) resolve the model spell off `origin` ``` An `Element` is two things at once, and the pairing is the whole point: -* a **closed classification** — `TypeKind`, `StructFields`, the variant list — - that says what the source *means*, in terms every destination language shares; -* the **exact syntax** each part was built from, sliced down to the parameter, - field, variant and type. +* a **closed classification** — `TypeKind`, the field list, which of the two enum + shapes an item is — that says what the source *means*, in terms every + destination language shares; +* one `Origin`, carrying the **exact syntax** the node was built from and the + source it arrived in. Every node has one, at every level — item, parameter, + field, alternative, type, array extent. ```rust -pub struct Type { pub kind: TypeKind, pub syntax: syn::Type } -pub struct Param { pub name: syn::Ident, pub ty: Type, pub syntax: syn::PatType } -pub struct Variant { pub tag: i32, pub discriminant: Option, pub fields: Vec, - pub syntax: syn::Variant } +pub struct Origin { pub syntax: S, pub location: Rc } +pub struct TypeRef { pub kind: TypeKind, pub origin: Origin } +pub struct Param { pub name: syn::Ident, pub ty: TypeRef, pub origin: Origin } +``` + +The two enum shapes are separate entities, because they are numbered differently +and consumed as different constructs: + +```rust +pub struct Variant { pub name: syn::Ident, pub alternatives: Vec, .. } // a sum +pub struct Enum { pub name: syn::Ident, pub values: Vec, .. } // C-style ``` ### Why the syntax rides along @@ -49,24 +58,26 @@ classification stays small and genuinely neutral: | Fact | Where it lives | Who reads it | |---|---|---| -| `B()` vs `B` | `Variant::syntax` (via `Variant::spell`) | generated Rust only | -| `= 0x07` vs `= 7` | `Variant::syntax.discriminant` | a C mirror re-emits it | -| the number 7 | `Variant::discriminant` | Kotlin `NAME(7)`, `jint` decode | -| `Foo<'a, T>` | `Type::syntax` | generated Rust only | -| "it is a `Foo` with one type argument" | `TypeKind::Named` | every adapter | -| `[u8; TAG_LEN]` — spelling / number / const identity | `Type::syntax` / `ArrayExtent::value` / `ExtentSource::Const` | C header / Kotlin / both | +| `B()` vs `B` | `Alternative::origin.syntax` (via `spell::fields`) | generated Rust only | +| `= 0x07` vs `= 7` | `EnumValue::origin.syntax.discriminant` | a C mirror re-emits it | +| the number 7 | `EnumValue::discriminant` | Kotlin `NAME(7)`, `jint` decode | +| which alternative of a sum | `Alternative::index` | a sum has no Rust number to borrow | +| `Foo<'a, T>` | `TypeRef::origin.syntax` | generated Rust only | +| "it is a `Foo`" | `TypeKind::Named` | every adapter | +| `[u8; TAG_LEN]` — spelling / number / const identity | `TypeRef::origin.syntax` / `ArrayExtent::value` / `ExtentSource::Const` | C header / Kotlin / both | +| where an item came from | `Origin::location` — **absent for a synthesized one** | diagnostics | ### The rule > **Classify off `kind`, spell off `syntax`.** > -> Matching a `syn::Type` or `syn::Expr` variant outside `core::language` is a -> classifier, and #211 says classification lives there alone. Passing a `syntax` -> slice into `quote!` is spelling, and spelling the source is exactly what -> generated Rust must do. +> Matching a `syn::Type` or `syn::Expr` variant outside `core::flat` is a +> classifier, and #211 says classification lives there alone. Passing an +> `Origin`'s syntax into `quote!` is spelling, and spelling the source is exactly +> what generated Rust must do. This is mechanically measured, and needed no new mechanism: -`core::language::boundary` (ported from +`core::flat::boundary` (ported from [#224](https://github.com/milyin/prebindgen/pull/224)) counts *variant mentions* of watched syn enums per file, so `quote!(#slice)` is invisible to it while `matches!(ty, syn::Type::Reference(_))` is counted. The committed ledger is the @@ -74,15 +85,22 @@ scoreboard for this whole program. ## Size of the problem -Seeded by L0 at **202 classification sites** outside `core::language`, plus -**113** reads of the registry's `syn`-keyed item maps: +Seeded by L0 at **202 classification sites** outside the frontend. The second +population it was seeded alongside — **113** reads of the registry's `syn`-keyed +item maps — is **gone**: L1.5 deleted those maps, so every one of those reads now +goes through the model. What remains is the ledger. -| Area | Ledger sites | Registry map reads | Stage | -|---|---:|---:|---| -| `api/core` (`types_util` 40, `unfold` 15, `registry` 13, `expand` 4) | 72 | 39 | L2 | -| `api/lang/cbindgen` | 25 | 25 | L3 | -| `api/lang/jnigen` | 105 | 49 | L4 | -| **total** | **202** | **113** | | +| Area | Ledger sites | Stage | +|---|---:|---| +| `api/core` (`types_util` 40, `unfold` 16, `registry` 11, `expand` 4) | 71 | L2 | +| `api/lang/cbindgen` | 25 | L3 | +| `api/lang/jnigen` | 106 | L4 | +| **total** | **202** | | + +Still 202 in total, and that is the honest number: L1.5 moved reads off the +deleted maps but took only two classifiers off the ledger (−2 in `registry`, +`unfold` +1 elsewhere). **The ledger has not started falling yet** — L2 is where +it does. Not every site must go: some inspect types the adapter itself *synthesized* — wire types, converter signatures — which is legitimately the adapter's business. @@ -94,21 +112,25 @@ moves it. | Stage | Owns | State | |---|---|---| -| L0 | `Language` + `Element` + the ledger | **done** — [#227](https://github.com/milyin/prebindgen/pull/227) | +| L0 | The parser, `Element`, and the ledger | **done** — [#227](https://github.com/milyin/prebindgen/pull/227) | | L0.5 | `Flat`: the model, indexed and resolved | **done** — this branch | -| L1 | `Registry` consumes elements | **done** — this branch | +| L1 | `Registry` consumes elements | **done** — [#238](https://github.com/milyin/prebindgen/pull/238) | +| L1.5 | The model is the only index | **done** — #239–#246 | | L2 | `api/core` stops classifying source syntax | not started | | L3 | `Cbindgen` consumes elements | not started | -| L4 | `JniGen` consumes elements *(the long pole — 105 sites)* | not started | +| L4 | `JniGen` consumes elements *(the long pole — 106 sites)* | not started | | L5 | Close the seam: the public contract stops being `syn` | not started | ### L0 — the parser — **done** (#227) -- [x] `Language::parse` over any `(syn::Item, SourceLocation)` stream — the seam +- [x] One parse over any `(syn::Item, SourceLocation)` stream — the seam `Registry::from_items` occupies, so multi-source composition is unchanged -- [x] `Element` = `Function | Struct | Enum | Const | Unsupported | Passthrough`, - every element and component carrying its syntax slice -- [x] `Type { kind, syntax }`; lowering total over the accepted grammar +- [x] `Element` per modelled kind plus `Unsupported`, every element and component + carrying the syntax it was built from. (There is **no** verbatim-passthrough + variant: the proc-macro refuses to mark a `use`/`mod`/`macro_rules!`, so + nothing reached one. The exact variant list is L0.5's, below.) +- [x] A type reference is a classification plus its syntax; lowering total over + the accepted grammar - [x] The array-length subgrammar and `ArrayExtent`, ported from #212 - [x] Enum tag / discriminant numbering, ported from #226, with `checked_add` - [x] Round-trip tests: syntax slices are the source's tokens, including the @@ -143,17 +165,23 @@ a model, and takes two bullets off L1 in the process. `Element::Unsupported` with `ItemError::UnresolvedType` — so a dangling name is reported here, by name, instead of surfacing downstream as an unresolved *converter* from whichever adapter looked first -- [x] `MaybeUninit` becomes `TypeKind::Uninit`: a boundary concept the adapter - was classifying, and the one foreign generic no alias can name +- [x] `&mut MaybeUninit` becomes `RefMode::Out` — an out-parameter is a + property of the **borrow**, not a wrapper type, and it is a boundary concept + every destination language has (C's `T *out`) - [x] The example flat APIs are closed, and covertest-kotlin's build script asserts they stay closed across both its sources - [x] **Did not move**: every generated artifact byte-identical -**Still open**: `zenoh-flat` and its two consumers are separate repos. Their 28 -unmarked types (26 zenoh aliases, plus `Duration` and `Cow<'_, [u8]>`) need the -same treatment before they parse. `Cow<'_, [u8]>` has no alias spelling — generic -and lifetime-bearing — so `zbytes_to_bytes` needs either the `MaybeUninit` -treatment or a signature change. +`Cow<'_, T>` needed neither an alias nor a grammar addition in the end: it is +transparent, exactly like `Box`, so it lowers to whatever `T` is +([#236](https://github.com/milyin/prebindgen/pull/236)). Both adapters already +treated it as `Vec`, which is what made the transparency the honest reading +rather than a convenience. + +**Still open**: `zenoh-flat` and its two consumers are separate repos. Their +unmarked types — the 26 zenoh aliases, plus `Duration`, which is not in the +prelude and so needs a marked alias like any other foreign type — need the same +treatment before they parse. ### L1 — `Registry` consumes elements — **done** @@ -187,15 +215,63 @@ asserting shapes the subgrammar dropped in #212. **Still open**: `zenoh-flat`'s 26 unmarked aliases. Until they are marked, `zenoh-flat-c` and `zenoh-flat-jni` do not generate. +### L1.5 — the model is the only index — **done** + +L1 made the registry a *projection* of the model, but it still kept its own copies. +A projection that copies is two stores that can disagree, so this stage deleted the +copies. Not planned as a stage; it fell out of reviewing L1 and is recorded here +because the map should show where the program actually went. + +- [x] **The seven fields go** ([#243](https://github.com/milyin/prebindgen/pull/243)): + `functions`, `structs`, `enums`, `consts`, `guards`, `item_origins`, + `source_modules`. `Flat` grows `struct_type` / `enum_item` / + `source_modules`, and the registry answers `origin_module`, + `default_module`, `named_item_idents` off the model. The + `SourceLocation` half of every deleted map entry was provably **dead** — all + 44 `.get()` sites bound it to `_` +- [x] **Binding-local fns join the model**, lowered through the same grammar + (`Flat::lower_signature`) and admitted by `add_local_function` — otherwise + "one index" would be a lie, since a `sig!(..)` never passed through the parser +- [x] **The type table carries the reading** + ([#239](https://github.com/milyin/prebindgen/pull/239)): a cell is + `TypeCell { subject, root, entry }` where `TypeSubject` is either the + frontend's `TypeRef` or an adapter-authored type. `required` stopped being + stored — it was one name over three storages — and is derived by `resolve` +- [x] **`const _` is a `Guard`, not a `Constant`** + ([#240](https://github.com/milyin/prebindgen/pull/240)): an anonymous const + has no address, so it is not API. Four sentinel `ident == "_"` checks had + already gone dead without anyone noticing — the failure mode a sentinel invites +- [x] **A lookup takes the name the caller holds** + ([#244](https://github.com/milyin/prebindgen/pull/244)): the sealed `Name` + trait, because `Ident` hashes via `to_string()` and has no `Borrow` — + the allocation can be *moved*, never removed +- [x] **An alias is a declaration of its name** + ([#245](https://github.com/milyin/prebindgen/pull/245)): the two type + diagnostics had excluded `Extern` as an artefact of asking the old + `structs`/`enums` maps, which had nowhere to put one +- [x] **`Flat` owns the type index** + ([#246](https://github.com/milyin/prebindgen/pull/246)): the last index + living outside its owner. `from_flat` collapses to *check expressibility, + store the model*. Canonicalization becomes one definition + (`types_util::canonical_type`) that both the index and `TypeKey` derive from +- [x] **A reading and a reportable position are different facts**: a synthesized + signature has readings but no file, so `SourceLocation::has_position` gates + what diagnostics print. Fixed a pre-existing `:0:0:` for hand-built streams + as well + +**What is left in `Registry` is now genuinely its own**: the two type tables +(adapter answers plus roots) and the five adapter-declared plan maps. + ### L2 — `api/core` stops classifying source syntax - [ ] `types_util` — 40 sites, the largest single file. `normalize_type`, `immediate_pattern_children`, `match_pattern`, the `is_*` predicates - [ ] `registry::immediate_subtype_positions` — near-duplicate of `immediate_pattern_children`, and the two already diverge on `Type::Path` -- [ ] `unfold` (15) and `expand` (4) read element types -- [ ] `TypeKey` derivable from a `Type` so a lookup stops routing through a - spelling +- [ ] `unfold` (16) and `expand` (4) read element types +- [ ] `TypeKey` derivable from a `TypeRef` so a lookup stops routing through a + spelling. L1.5 got the first half — `TypeKey` and the model's type index + now share one canonicalization (`types_util::canonical_type`) - [ ] Ledger down by the migrated count; every entry that *stays* is justified in the PR as adapter-synthesized @@ -214,7 +290,7 @@ The long pole. Split by area, each PR independently green. - [ ] `emit/names` (17), `jni/builder` (13), `jni/trait_impl` (11), `emit/wrapper` (11), `emit/flat_input` (10), `render` (8), `selector` (7), - and the rest + `iface` (5), and the rest - [ ] `classify.rs` — a whole classifier with **zero** watched sites, so the ledger cannot see it: it must be migrated on its own merit - [ ] `prim_array_of` reads `ArrayExtent` instead of re-matching `Type::Array` @@ -225,8 +301,9 @@ The long pole. Split by area, each PR independently green. The public contract stops being `syn`, which is what stops the population from growing back. -- [ ] `Registry`'s public item maps stop being the adapter-facing contract — - relates to [#92](https://github.com/milyin/prebindgen/issues/92) +- [x] `Registry`'s public item maps stop being the adapter-facing contract — + done early by L1.5, which deleted them outright; relates to + [#92](https://github.com/milyin/prebindgen/issues/92) - [ ] `Prebindgen::post_process_item(&mut syn::Item)` — the hook that let qualification live in an adapter in the first place - [ ] `ConverterImpl::function` / `TypeEntry::function` as `syn::ItemFn`; @@ -243,7 +320,9 @@ growing back. #211's, restated for this design: -- One documented entry point from captured records to elements — `Language::parse`. +- One documented entry point from captured records to elements — + `Flat::builder().items(..).build()`, which `Registry::from_items` also routes + through. - Both `Cbindgen` and `JniGen` take every **source** fact from an element. - No component re-derives a source fact by matching captured syntax; the ledger has reached the irreducible set, and every remaining entry is documented as @@ -263,17 +342,26 @@ dropped is the syn-free model itself — `SourceType::to_syn`, source's own slice does that job without a modelling cost. The `source-frontend` branch stays in place as the reference. Nothing depends on -it, and it is not a base for anything here: every stage of this program targets -`main`. +it, and it is not a base for anything here: every stage of this program lands on +`language-integration`, which merges to `main` when the program does. ## Review protocol Each stage PR states its own exit: -- **Must not move** — byte-identical artifacts, enforced by - `examples/regen-check.sh`. A diff is a bug. -- **Reviewed diff** — expected to change, cause stated up front. A diff outside - that cause is a bug. +- **Reported** — what `examples/regen-check.sh` did, always. The check is + **instrumentation, not a constraint**: it says what moved, not whether the + change was allowed. Byte-identical is the strongest evidence a refactor did + nothing unintended and is worth claiming when it holds — but generated output + that moves without changing semantics or performance is fully acceptable, and + no architecture decision may be reshaped to keep bytes matching. +- **Explained** — if output moved, why the change is semantically and + performance neutral. A movement outside that explanation is a bug. - **Asserted** — the invariant the stage adds, and the ledger delta it claims. -[`core::language`]: ../prebindgen/src/api/core/language/mod.rs +Run the check the way that makes it mean something: `git clean -fd examples/` +first (an earlier `--all-features` run leaves artifacts the check reads as drift), +then `cargo clean -p example-cbindgen -p example-flat` (it only regenerates what +cargo decides to rebuild, so a cached run passes without checking anything). + +[`core::flat`]: ../prebindgen/src/api/core/flat/mod.rs From d845c8f36c8bac92c9108d8365b59aa6937b5db5 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 00:37:27 +0200 Subject: [PATCH 11/52] Delete the pattern engine; the model already names the one shape it matched (#248) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Delete the pattern engine; the model already names the one shape it matched The registry could compose converters for any parametrized type: a four-rank wildcard table, a general unification engine, `Foo<_, _>` patterns at any depth. The universality carried no traffic. **The table had one entry, in the whole crate:** ```rust // builder.rs — the only insert into either table let pattern: syn::Type = syn::parse_quote!(Result<_, _>); jni.output_wrappers[2].insert(key, ..); ``` `input_wrappers` was **never** inserted into, so `match_user_input` always returned `None`; the rank-1 lookups heading `input_wrapper_shape` / `output_wrapper_shape` were always-`None` prologues to their real hardcoded logic; and no public API could register a pattern. The code's own comment said it: *"The rank tables are internal — this is their only entry."* And `Result` is `TypeKind::Fallible` in the model. A unification engine expressed one fact the frontend states outright. Gone: `match_pattern`, `unify`, `immediate_pattern_children`, `wildcard_count`, `lifetime_eq`, `token_eq`, `substitute_wildcards`, `ordered_patterns`, `ordered_input_patterns`, `ordered_output_patterns`, `match_user_input`, `match_user_output`, `WrapperFn`, and both rank tables. `lookup_input` / `lookup_output` lose their `pat`/`args` parameters — with the tables gone they answer only for `convert!`, which was always their only live path. **The `ConverterImpl` tail is extracted, not rewritten.** Terminal-vs-composed detection, exception binding and metadata assembly are the subtle part, so `build_output_converter` holds them verbatim and both survivors call it: the `convert!` path with `arg0: None`, the `Result` peel with `Some(ok)`. That mapping is exact — the old `rank == 0` tested precisely "no peeled inner". **Measured, not assumed:** the syntactic fallback in `fallible_parts` **never fires** — zero occurrences across covertest-kotlin and perftest-kotlin, because #246 indexes a binding-local fn's types, so even `sig!((..) -> Result)` has a reading. It is kept rather than made a hard error, since an out-of-tree consumer may compose a `Result` the model never sees, and it costs nothing: `result_parts` already existed with six other callers. **No new test.** The plan called for one pinning the peel; the existing suite already does, verified by sabotage — removing the peel fails **8** tests across `snapshots`, `flatten`, `sealed` and `cross_artifact`. Adding a ninth would be decoration. **Ledger 202 → 167** (`types_util` 40 → 14, `jnigen/builder` 13 → 4) — a drop of 35, and the first real fall in this program: L1.5 moved 113 map reads but took only two classifiers off. Generation byte-identical, covertest 48 sections, 590 + 518 tests. Co-Authored-By: Claude Opus 5 * Stop documenting the dispatch that was deleted Review's point: the PR's purpose is architectural deletion, so leaving the old model in adjacent documentation makes the surviving code harder to read. **Four broken intra-doc links**, all confirmed by `cargo doc --document-private-items`: `[`match_pattern`]` in `types_util`, `[`WrapperFn`]` in `builder`, and `[`Self::input_wrappers`]` / `[`Self::output_wrappers`]` in `mod`. Each is rewritten to describe what is there now rather than repointed — the lifetime rule stands on its own reason, `lookup_input` answers for `convert!`, and terminal dispatch is opaque → enum → `convert!` → primitive → struct. **Prose that still described a table with no writers**: "the user-wrapper table (`match_user_*`, any depth, specificity-ordered)", "the rank-0 user table", "the rank-1 user table" ×2, "the unified user-registered wrapper table", "before the wrapper tables", and a comment explaining how to override `Result<_, _>` by registering a more specific rank-1 pattern — an instruction for an API that no longer exists. **The ledger's blind-spot list** named `match_pattern` as a classifier the check could not see. That gap is now closed rather than open, and the header says so instead of listing it. Surviving "rank-0" mentions are left alone deliberately: in the adapters they read as "terminal, not composed", which is still true and is a property of the converter rather than of the deleted table. **On the removed public API** — `types_util::match_pattern` and `wildcard_count` were `pub`. They are absent from the released `0.4.1`, so this only affects someone tracking `language-integration` directly; noted here since the crate's 0.5 policy is a new surface with no back-compat shims. Docs only. 590 tests, generation byte-identical, MSRV clippy clean. Co-Authored-By: Claude Opus 5 * The caller states its declarations; the registry stops asking `DeclaredItems` was a private 13-field struct assembled by `DeclaredItems::from_adapter`, which called **twelve getters** back into the adapter — backed by twelve trait methods — from *inside* `resolve`. That round-trip was ceremony. `JniGen` already holds this state; the getters projected it out one method at a time and `from_adapter` copied it into a struct. The struct **is** the registry's construction input. Nothing was learned by asking for it piecemeal. So the struct is now `pub struct Declarations` with builder methods, the twelve trait methods are one: fn declarations(&self) -> Declarations; and `from_adapter` is gone — `adapter.declarations().check()?` replaces it, with `check` keeping the two conflict rules (a name both declared and ignored) that `from_adapter` enforced. **Why this matters beyond the line count.** Assembling declarations *inside* `resolve` is what made "configuring" and "using" the same call, which is what lets a converter be handed a half-built registry, which is why `None` from `on_input_type` is ambiguous between *defer* and *cannot* — and that ambiguity is the only reason the fixed-point loop exists. Stating declarations before resolution is the prerequisite for computing a resolution order at all. Two measurements say the rest is derivable, so S2b can compute that order: * the five *declaration* methods that still take `&Registry` — `prerequisites`, `deconstructors`, `value_struct_decons`, `sum_decons`, `extra_required_types` — read only `flat()` and `all_source_modules()`, never a converter; * `unfold.rs` and `expand.rs`, which compute every decomposition plan, make zero reads of `input_entry` / `output_entry` / `type_table`. The twelve getters move to inherent `pub(crate)` methods on each adapter — they are the adapter's own business now, gathered into one value at one point rather than pulled twelve callbacks deep. Behaviour is untouched by construction: same data, opposite direction. Generation byte-identical, 590 + 518 tests, covertest 48 sections, MSRV clippy clean. Co-Authored-By: Claude Opus 5 * Say what the registry is for Its module doc was a list of fields — *"Registry holds: item maps, guards, type tables, sidecars"* — and a stale one: the item maps were deleted in #243. Nowhere did it state a purpose. That is why the API drifted into names like `require` and `plans`, which cannot be read without already knowing the answer: require *what*, plan *what*? Replaced with what it is: > **Which type conversions a binding needs, and whether it has them all.** and the four things that make that concrete: **The boundary is the wire.** A binding puts a wrapper on each side — generated Rust the destination language can call, and destination code shaped to match. The wrapper's *body* speaks source Rust, its *signature* speaks wire (`jlong`, `*const T`). The translation between them is a conversion. There is a diagram, because the three-way relationship is the thing everything else hangs off. **A conversion is a chain, not a function** — `destination`, a wire-facing `function`, and `pre_stages`. That is *how* composition works: `Option`'s chain embeds `Handle`'s. And a composite need not cross whole: `Option` may be a `T` with a niche, a `(bool, T)` pair, or leaves delivered separately — the adapter's choice, which the registry records so both sides can be written to match. **Conversions are directional.** Two tables, not one. `&str` inbound decodes a `jstring`, outbound allocates one, and one direction may be convertible while the other is not. A callback flips it — `impl Fn(Sample)` is an input whose argument crosses outbound. **It derives the set, then checks completeness — and never writes a conversion.** A binding names a surface; far more types must convert than were named, and computing that closure is the work. Completeness is a meaningful check precisely because the set is derived here rather than handed over. But only the adapter knows what a `jlong` is, so the registry asks for each and fails naming what could not be supplied. Plus an in/out table: model, crossings, decompositions, conversion builder → a conversion per type in the closure, or a failure naming the gaps. Docs only. 590 tests, generation byte-identical, MSRV clippy clean, doc-link warnings unchanged at 17 (measured against this branch's parent, not a different one). Co-Authored-By: Claude Opus 5 * Say how a registry is used, not just what it is for #250 stated the purpose. This states the shape that follows from it: configure it, hand over the answers, read it — and nothing in core calls back into the generator. A `next_request`/`supply` pull loop is not an alternative to a callback trait; it is the same protocol with the arrow flipped. What removes the protocol entirely is the sort: `immediate_edges` is structural, so the demand can be handed over inner-first as a plain list, and a generator building `Option` already holds `Handle`. Each crossing is then offered exactly once, which is also what makes a generator's `None` mean `cannot` rather than `not yet`. Records the two consequences worth knowing up front: a `None` is not a failure (reachability from the exports decides), and a self-referential type has no order, so `crossings` breaks the cycle at its entry. Docs only. 590 tests, regen-check byte-identical, MSRV clippy clean, doc warnings unchanged at 49 measured against this branch's parent. * Move the skip report out of the registry First code step of #251. The registry answers "which conversions does this binding need, and does it have them all"; which items a binding skipped bears on neither. Four inputs were read only to print `cargo:warning=` lines — `ignored_functions`, `ignored_types`, `ignored_consts` and `ignored_name_predicates` — so they leave with the five println! loops, into `core::diagnostics`, and a generator calls `warn_unclaimed` itself. Two things fall out. `Declarations::check()` and its two ScanError variants existed only to reject "declared AND ignored", which is now unrepresentable. And the report is built as lines and then printed, so it is asserted on directly instead of scraped off stdout — seven tests that could not exist before, one of which caught a dropped ignore-suppression while writing it. `consts: Option` STAYS: the plan called it a warning switch, but `write.rs` uses `None` to mean "re-emit every const verbatim", which is what cbindgen relies on. Its doc now says which half needs the sentinel. Interim: generators call this from `validate`, the earliest hook they own that sees the model, running exactly where the registry printed before. It moves to `generate` in phase E. 593 tests (590 - 4 + 7), warning output byte-identical on covertest-kotlin and example-cbindgen, regen-check clean, MSRV clippy clean, doc warnings unchanged at 49. -250 lines. * One way to build a registry: Registry::new(flat) `RegistryBuilder` was a verbatim duplicate of `FlatBuilder` — the same source/source_named/items/build, differing only in appending `from_items` — and the registry's own doc admitted it ("the same shape Flat reads prebindgen data with"). Reading captured output is Flat's job, so a build script now says where items come from once, at the layer that owns the question: let flat = Flat::builder().source(FLAT_OUT_DIR).build()?; Registry::new(flat)?.resolve(adapter)?.write_rust(out)?; `Registry::{empty, from_items, builder}` and `RegistryBuilder` are gone; `from_flat` becomes `new`, no longer disambiguating against a sibling. The NotExpressible check stays here: an item the flat language cannot express is a hard error whatever a binding declares. One extra named type per build.rs against 54 lines of duplicate builder and two redundant constructors. Test fixtures get `test_util::reg_from_items` rather than repeating the two steps in ~40 places. 593 tests, warning output byte-identical, regen-check clean (the two untracked example_flat_aarch64_unstable.* files reproduce identically on the parent after the same clean — that is #219, not this), MSRV clippy clean, doc warnings unchanged at 49. * Correct the input: elements alone cannot name every crossing The shape landed in 2428db4 said the configure step is `export` + `decompose`, on the reasoning that types are reachable by walking a declared element's signature. Measured, and it is half true. Dropping the declaration-as-root for declared types leaves regen-check byte-identical — so for every type with a captured body, deriving per usage really is enough, and it is the more correct rule (an output-only type stops being demanded as an input). It fails for a type with NO captured item behind it: `ptr_class!(zenoh::KeyExpr<'static>)` on a re-exported foreign type appears in no signature this model can walk. Nothing derives it, so the declaration is the only statement that it crosses at all — two tests pinned exactly that and caught the claim. So the input needs `cross(type)` beside `export(name)`: the narrow escape hatch for the no-element case, not the common path. Still four inputs against Declarations' twelve setters. Docs only. 593 tests, doc warnings unchanged at 49. * Push declarations in; the registry stops asking Step 1 of three. `resolve` used to CALL the generator to find out what to build — `declarations()`, `local_functions()`, `extra_required_types()`, three of the twenty Prebindgen hooks. That is the callback the module doc forbids, so it is inverted: the generator states its binding, and the registry records. jni.declare_into(&mut registry)?; // generator pushes jni.resolve(registry)? // pairs the two; registry never asks `Registry` gains export / export_const / export_type / cross / reference / local_function, and `Declarations` plus its twelve setters are deleted. The generator drives `resolve` because it is what knows both halves — which is also the shape `generate(..)` takes when emission moves there (phase E). `cross` is directional. Three of the old inputs were one-sided (`required_output_types` output-only, `extra_required_types` per-direction) and one was implicitly both; stating direction at the point of declaration is what stops an output-only crossing from silently lacking its input twin. accessor / method_receiver / crosses_only_in_pieces ride along with a comment: they are properties of a decomposition, and move onto it in step 2. 593 tests, warning output byte-identical, regen-check byte-identical, MSRV clippy clean, doc warnings unchanged at 49. * Five decomposition callbacks become one stated value Step 2 of three. `expansions`, `deconstructors`, `value_struct_decons`, `sum_decons` and `leaf_vec_fold_elements` were five separate calls the registry made back into the generator from inside `resolve`. All five are implemented by one adapter, none by the other, and — measured while planning this — not one of them ever reads more than `registry.flat()`. So they are stated up front instead of asked for: registry.decompose(Decompositions { .. }); `boundary_only_types` moves onto it as `replaces`, where the fact comes from: a type crosses only in pieces BECAUSE something decomposes it, so listing it separately was two statements of one thing. The five fields are still the five declaration families. Unifying the plan IRs behind them is #223, and collapsing them here would only move that seam while pretending it was closed — what this settles is when they are stated and by whom. Prebindgen is down from 20 hooks to 12; the 8 gone are every "what should I build" question. The 12 left are emission (phase E) plus the three conversion hooks step 3 replaces. 593 tests, warning output byte-identical, regen-check byte-identical, MSRV clippy clean, doc warnings 49 -> 36 (deleted hooks took their links). * Hand over the demand; delete the fixed-point loop Step 3 of three, and the one the other two were clearing the way for. `on_input_type` / `on_output_type` / `dispatch_fn_input` were the last questions core asked the generator, and the fixed-point loop existed only because the order those were asked in was arbitrary. Both are gone: let order = registry.crossings(); // sorted, inner types first for c in &order { ... } // the generator's own loop registry.supply(built)?; // graded once `crossings` sorts by `immediate_edges`, which is structural, so no generator is consulted to derive it. Each crossing is then offered exactly once, and `None` means CANNOT rather than NOT YET — the ambiguity #249 named as the cause of converters reading a half-built registry. Two dependencies the structure cannot show, both found by tests rather than by reasoning: * a callback argument delivered as plan leaves needs those leaves' conversions, and a leaf is named by a plan, not by the argument's syntax. Derived in `plan_edges`. * a `convert!` chains through a helper's parameter type, which nothing about the target type mentions. The generator states it: `depends`. The old loop papered over both by retrying. Making the order explicit is what turns them from invisible into stated. `Conversions` is the seam: `Building` is the partial view a generator builds against, `Registry` the total one everything else reads, and a helper serving both takes `&impl Conversions`. 39 signatures moved. Cycles have no topological order, so `crossings` breaks one at its entry — the single case where "every inner first" is not literally true. No example has a recursive type, so this adds a test instead of trusting byte-identity. Prebindgen: 20 hooks -> 9, all emission. 594 tests, warning output byte-identical, regen-check byte-identical, MSRV clippy clean. * Take expansion_plans back off the Conversions trait Self-review of ec0e1df. The plan accessors went on the trait to stop the generic substitution spreading into emission code, and I flagged the result as looser than conversion-building needs. Measuring which callers are actually generic: unfold_plans / error_plans / decon_plans / callback_arg_plan(s) 6 generic callers — the callback + iface_spec path, reachable while a conversion is being built. These have to be on the trait. expansion_plans 0 generic callers. All five sites (fn_plan, render, report, overloads, wrapper) hold a concrete &Registry and always will: parameter folds are read at emission, never while converting. So it comes off, and those five read the field directly again. One less thing `Building` shows a generator than it has any use for. 594 tests, warning output byte-identical, regen-check byte-identical, MSRV clippy clean. * Close the registry's fields; split it into a module Two changes, both about what the registry shows. **Fields are crate-internal.** `input_types` / `output_types` and the five plan maps were `pub`. Outside the crate a table is now reached through `Conversions::conversion` and `crossings` — which is what makes direction a parameter rather than half of a field name, and what stops anyone observing a cell before `supply` has graded it. `expansion_plans` gets an inherent accessor (it is emission-only, so it stays off the `Conversions` trait, per 9fdbb9e); the other maps already had one. `pub(crate)` rather than private because `expand` and `unfold` fill them — they are core's own state, just not the world's. **One 1989-line file becomes eleven, none over 420.** Grouped by what they answer, not by type: mod Registry, Declared, Decompositions, wiring key/cell TypeKey; TypeSubject/TypeCell/TypeEntry/Direction declare configure — every method records, none derives model questions about the model scan derive the crossing set order hand the demand over, grade the answers run prepare/finish/apply plans view Conversions + Building error what can go wrong walk structural type-graph helpers Inherent impls span modules, so `impl Registry` splits with them. Two things the split surfaced rather than caused: * `TypeKeyParseError` and `DuplicateNameError` are reachable from the public API — `TypeKey::parse` returns one, `ScanError::DuplicateName` carries the other — and were never re-exported. Now they are, along with `NotExpressibleEntry`. * the boundary ledger moves 11 classification sites from `registry.rs` to `scan.rs` (2) + `walk.rs` (9). Total unchanged at 167: relocation, no new classifier. 594 tests, warning output byte-identical, regen-check clean, MSRV clippy clean, doc warnings 44 -> 43. * Retire the docs that describe deleted machinery Closing the fields turned a stale doc into a broken link, which is how I noticed the prose had not kept up with three commits of deletion. Swept core and lib.rs for every reference to something that no longer exists: * `Registry::input_types` / `output_types` — now crate-internal, so the module doc pointed readers at fields they cannot reach. It explains `Direction` and `Crossing` instead, which is the answer to the question that paragraph was actually asking. * `on_input_type` / `on_output_type` / `dispatch_fn_input` — the `Prebindgen` module doc still opened by describing them as the trait's main job. The trait has one job left: per-item emission. It says so, and says where conversion went. * `core`'s "phase-oriented pipeline" list and lib.rs's "# Flow" both still walked through `Registry::resolve` and the fixed-point resolver. Both now describe crossings/supply, and lib.rs no longer advertises `on_input_type_rank_0..3`, which has not existed for far longer than this branch. * one comment in `resolve.rs` explaining an ordering constraint in terms of the loop that enforced it. Only deliberate mentions survive — the `Prebindgen` doc naming what is gone so a reader coming from an older version knows where it went. Docs only. 594 tests, MSRV clippy clean, doc warnings 43 -> 42. * Retire TypeCell and TypeSubject from the public API Auditing what a third-party generator can actually reach — the point of this whole branch — turned up five exports no generator uses. Three are right: `TypeKeyParseError`, `DuplicateNameError` and `NotExpressibleEntry` are unreachable by accident but nameable on purpose, since `TypeKey::parse` returns one and `ScanError` carries the others. `TypeCell` and `TypeSubject` are not. Closing the type tables in 6af68bb left nothing public that returns or accepts either — they became API a caller could name and never obtain. `pub(crate)`. Which then showed what was only alive because it was public: * `TypeSubject::syntax` — read by nothing at all. * `TypeSubject::kind` — read only by tests, pinning that a source reading survives into a cell. Kept, `#[cfg(test)]`, so the lib build stops pretending it has a caller. * `TypeSubject::Adapter(syn::Type)` — the payload was never read back, only matched as `Adapter(_)`. Now a unit variant, and `test_util::cell` loses the key argument it only had to build one. None of this was reachable before the fields closed, which is why it sat here: dead code inside a public type looks alive. 594 tests, warning output byte-identical, regen-check clean (no tracked drift), MSRV clippy clean, doc warnings 42 -> 41. * Box TypeSubject::Source — stable clippy, not MSRV Making `Adapter` a unit variant in 7d0f985 left `Source(TypeRef)` as the only variant carrying anything, and `large_enum_variant` compares the largest against the SECOND largest: a 264-byte enum whose runner-up is empty. Boxing takes it to 8, which is the right shape anyway — cells are numerous and most are `Adapter`. The miss is in how I checked, not what I changed: CI's clippy runs on a `[1.85.0, stable]` matrix, and this lint fires only on stable's 1.97 clippy. I verified MSRV and stopped, so a green local run said nothing about the job that failed. * Split building a registry from reading one `Registry` was both: `&mut self` declaring methods and read-only accessors on one type, so "still being described" and "finished, and answerable" were a phase you had to be careful about rather than something the types knew. Now `RegistryBuilder` owns everything mutating and `build()` is the only way to get a `Registry`. Nothing can add a crossing to one, which makes "every crossing has a conversion" a fact about the type. let registry = Registry::builder(flat)? .export(&name) .decompose(decompositions) .convert_with(|crossing, built| gen.convert_crossing(crossing, built))? .build()?; Declarations consume `self`, so they chain. Two ways to hand conversions over, per your request: * `convert_with(f)` — chainable; `f` is called per crossing in dependency order with everything already built. This is a callback, and it is not the thing we removed: the registry does not re-enter generator logic on its own schedule, the walk is finished before the method returns, and the closure is the caller's. It is `crossings` + a `for` loop, written once instead of in every generator. * `crossings()` + `conversions(map)` — for filling the holes yourself. `conversions` accumulates, so the two compose. `prepare`/`supply` are gone; `validate` now takes the `Building` view instead of a whole `Registry`, which is all it ever read. `scanned()` is `#[cfg(test)]`: it is the state between described and answerable, which is exactly what this split exists to keep out of everyone else's hands. 594 tests, warning output byte-identical, regen-check clean (no tracked drift), BOTH clippy toolchains clean, doc warnings 41 -> 34. * Delete Registry::scan_declared — the split was still leaking Self-review of 21c403c: I claimed a strict builder/read-only split, then checked. `Registry` still had one public `&mut self` method, so the claim was not yet true — a caller could scan a finished registry. Zero callers: `RegistryBuilder::derive` subsumed it the moment the builder landed. Its doc was stale too, still describing `adapter.ignored_functions()` and the skip warnings that left for `core::diagnostics` several commits ago. Now `Registry` has NO public mutating method, and the only `&mut self` left is `RegistryBuilder::crossings`, which caches the derivation. 594 tests, warning output byte-identical, regen-check clean, both clippy toolchains clean, doc warnings unchanged at 34. * Update the docs the builder split invalidated Checked the module doc against the API it describes and found its worked example wrong in four ways: `export` does not return `Result`, `cross` takes a direction, `supply` no longer exists, and the whole thing still used `Registry::new`. A worked example that does not compile is worse than none — it is the first thing a generator author copies. Rewrote it around the two types, since that IS the change: a builder is still being described, a registry is finished and answerable. Added the `crossings`/`conversions` alternative, and said plainly why `convert_with` is not the callback we removed — the walk finishes before it returns, the closure is the caller's, and the builder chooses nothing about when it runs. Swept the rest: `Registry::new` in lib.rs's doctest and four module docs, `Registry::prepare` in the core pipeline description, and two references to `Registry::scan_declared` — deleted in b708c7e — in write.rs and kotlin_emit.rs. Docs only. 594 tests, warning output byte-identical, regen-check clean, both clippy toolchains clean, doc warnings 34 -> 33. * Rename the generators to what they are: builders Mechanical, and alone in its commit so the next one is reviewable. Today's `JniGen` and `Cbindgen` are pure declaration holders — everything on them either records what to emit or answers a question about it — so they are `JniGenBuilder` and `CbindgenBuilder`. That frees the short names for the built objects the next commit introduces, matching the convention already here: `Flat::builder()`/`FlatBuilder`, `Registry::builder()`/`RegistryBuilder`. Renamed OUTSIDE string literals only. Three of those strings matter: `"// Auto-generated by JniGen — do not edit by hand."` is written into every generated file, and two `"JniGen::on_function …"` diagnostics are user-facing. Renaming inside them would have moved the goldens and made this commit unreviewable — the header appears in committed output. Prose and doc links follow the code for now; the next commit revisits them, since it is the one that makes `JniGen` mean something again. 594 tests pass untouched, regen-check byte-identical (no tracked drift), warning output byte-identical, both clippy toolchains clean, doc warnings unchanged at 33. * The generator owns the model and the registry A build script had to know three types and a four-step dance to say "generate bindings from this directory": let flat = Flat::builder().source(DIR).build()?; let registry = Registry::builder(flat)?; let gen = jni.resolve(registry)?; gen.write_rust(&rs)?; `Flat` and `Registry` are pipeline internals. Now: let jni = JniGen::builder() .package(..).fun(..) .source(DIR) .build()?; jni.write_rust(&rs)?; jni.write_kotlin(&kt)?; `JniGenBuilder`/`CbindgenBuilder` gain `source` / `source_named` / `items` — the same three feeders `FlatBuilder` has, because they ARE that feeder: the builder holds a `FlatBuilder` and `build()` runs the pipeline the caller used to run by hand. `JniGen` and `Cbindgen` are the built objects, each holding its registry as a field, each publishing its own writers. `Generation` is deleted, and that is the point rather than a side effect: core no longer owns the artifact-bearing type, so a generator decides what its artifacts are and what they are called. `core::write::write_rust` stays a free function both call. `Registry::finish` goes with it — the post-resolve invariant check is now the generator running its own `validate_resolved`, which is the one place that knows what an invariant means here. `build_with(registry)` is the crate-internal seam tests use to feed synthetic items without a directory; `build()` is that over `source`. 594 tests, warning output byte-identical, regen-check byte-identical with no tracked drift, both clippy toolchains clean, doc warnings unchanged at 33. * Name the example variables after what they now hold `let gen = jni.build()` read backwards once `JniGen` became the built type: the thing called `jni` was the builder, and the thing called `gen` was the JniGen. Now `binding` builds and `jni` is what you write from. The examples are how this API is read before it is used, so the names being the wrong way round is worth a commit of its own. Output byte-identical, warnings byte-identical, regen-check clean. * Delete Registry::supply — the read-only claim was false Review catch (#249 review of the combined head). `21c403c` said `supply` was gone and `b708c7e` said no public `&mut self` remained on `Registry`. Both were wrong, and `supply` shipped: a caller could build a complete registry and then replace any conversion in it, with only core's completeness rerun and the generator's `validate_resolved` skipped entirely. Exactly the half-filled mutable protocol this stack claims to have removed. It had no callers — handoff is `RegistryBuilder::{convert_with, conversions, build}` — so it is deleted outright. **Why it survived two commits that checked for it.** I verified with `grep "pub fn .*&mut self"`, which needs both on one line; `supply`'s signature spans four. The check could not see the thing it was for. So the replacement is a test that strips ALL whitespace before matching, making a multi-line signature indistinguishable from a one-line one. I confirmed it fails by reintroducing a `pub fn __regression_probe(&mut self)` — the first version of the test passed with that present (the whitespace collapse left a space after `(`), which is the only reason I found out it was useless. `Registry::crossings` goes `pub(crate)` with it: same residue, no external caller, and the read phase in the module docs never listed it. Swept the docs the review enumerated — `lib.rs`, `core/mod.rs`, `resolve.rs`, `order.rs`, `registry/mod.rs`, `write.rs`, `kotlin_emit.rs`, `declare.rs` — all still describing `Registry::supply`, `Registry::finish`, or linking declaration methods that now live on `RegistryBuilder`. 595 tests (594 + the guard), warning output byte-identical, regen-check clean with no tracked drift, both clippy toolchains clean, doc warnings 33 -> 32. --------- Co-authored-by: Claude Opus 5 --- examples/covertest-helpers/src/lib.rs | 2 +- examples/covertest-kotlin/build.rs | 46 +- .../io/prebindgen/covertest/model.kt | 2 +- .../io/prebindgen/covertest/storage.kt | 2 +- examples/example-cbindgen/build.rs | 17 +- examples/example-flat/src/lib.rs | 8 +- examples/perftest-c/build.rs | 15 +- examples/perftest-flat/src/ext.rs | 2 +- examples/perftest-flat/src/lib.rs | 4 +- examples/perftest-kotlin/build.rs | 22 +- .../io/prebindgen/perftest/storage.kt | 2 +- prebindgen/src/api/core/diagnostics.rs | 165 ++ prebindgen/src/api/core/diagnostics/tests.rs | 166 ++ prebindgen/src/api/core/expand.rs | 2 +- prebindgen/src/api/core/expand/tests.rs | 30 +- prebindgen/src/api/core/flat/boundary.ledger | 18 +- prebindgen/src/api/core/flat/boundary.rs | 9 +- prebindgen/src/api/core/flat/mod.rs | 2 +- prebindgen/src/api/core/gravestone.rs | 2 +- prebindgen/src/api/core/mod.rs | 33 +- prebindgen/src/api/core/prebindgen.rs | 379 +--- prebindgen/src/api/core/registry.rs | 1671 ----------------- prebindgen/src/api/core/registry/cell.rs | 162 ++ prebindgen/src/api/core/registry/declare.rs | 462 +++++ prebindgen/src/api/core/registry/error.rs | 222 +++ prebindgen/src/api/core/registry/key.rs | 111 ++ prebindgen/src/api/core/registry/mod.rs | 364 ++++ prebindgen/src/api/core/registry/model.rs | 94 + prebindgen/src/api/core/registry/order.rs | 115 ++ prebindgen/src/api/core/registry/run.rs | 68 + prebindgen/src/api/core/registry/scan.rs | 388 ++++ prebindgen/src/api/core/registry/tests.rs | 611 +++--- prebindgen/src/api/core/registry/view.rs | 193 ++ prebindgen/src/api/core/registry/walk.rs | 42 + prebindgen/src/api/core/resolve.rs | 138 +- prebindgen/src/api/core/resolve/tests.rs | 21 +- prebindgen/src/api/core/types_util.rs | 195 +- prebindgen/src/api/core/types_util/tests.rs | 107 -- prebindgen/src/api/core/unfold.rs | 4 +- prebindgen/src/api/core/unfold/tests.rs | 82 +- prebindgen/src/api/core/write.rs | 15 +- prebindgen/src/api/core/write/tests.rs | 105 +- prebindgen/src/api/gen/kotlin/expr/tests.rs | 4 +- prebindgen/src/api/gen/kotlin/model.rs | 2 +- prebindgen/src/api/lang/cbindgen/builder.rs | 31 +- prebindgen/src/api/lang/cbindgen/convert.rs | 19 +- prebindgen/src/api/lang/cbindgen/emit.rs | 19 +- prebindgen/src/api/lang/cbindgen/mod.rs | 130 +- prebindgen/src/api/lang/cbindgen/selector.rs | 9 +- .../src/api/lang/cbindgen/tests/aliasing.rs | 5 +- .../cbindgen/tests/boundary_invariants.rs | 4 +- .../src/api/lang/cbindgen/tests/builder.rs | 44 +- .../src/api/lang/cbindgen/tests/callbacks.rs | 18 +- .../src/api/lang/cbindgen/tests/errors.rs | 29 +- .../src/api/lang/cbindgen/tests/inputs.rs | 50 +- .../src/api/lang/cbindgen/tests/lowering.rs | 31 +- prebindgen/src/api/lang/cbindgen/tests/mod.rs | 9 +- .../src/api/lang/cbindgen/tests/returns.rs | 94 +- .../src/api/lang/cbindgen/tests/structs.rs | 40 +- .../api/lang/cbindgen/tests/tagged_unions.rs | 54 +- .../src/api/lang/cbindgen/trait_impl.rs | 337 ++-- prebindgen/src/api/lang/jnigen/jni/builder.rs | 437 ++--- .../src/api/lang/jnigen/jni/classify.rs | 5 +- prebindgen/src/api/lang/jnigen/jni/config.rs | 24 +- prebindgen/src/api/lang/jnigen/jni/decl.rs | 42 +- .../src/api/lang/jnigen/jni/emit/callback.rs | 10 +- .../src/api/lang/jnigen/jni/emit/convert.rs | 16 +- .../src/api/lang/jnigen/jni/emit/delivery.rs | 15 +- .../api/lang/jnigen/jni/emit/flat_input.rs | 21 +- .../src/api/lang/jnigen/jni/emit/names.rs | 6 +- .../api/lang/jnigen/jni/emit/struct_out.rs | 19 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 11 +- .../src/api/lang/jnigen/jni/emit/vec_build.rs | 12 +- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 33 +- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 35 +- prebindgen/src/api/lang/jnigen/jni/fold.rs | 4 +- prebindgen/src/api/lang/jnigen/jni/iface.rs | 101 +- .../api/lang/jnigen/jni/jni_binding_error.rs | 2 +- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 60 +- .../src/api/lang/jnigen/jni/metadata.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/mod.rs | 179 +- .../src/api/lang/jnigen/jni/overloads.rs | 26 +- prebindgen/src/api/lang/jnigen/jni/render.rs | 51 +- prebindgen/src/api/lang/jnigen/jni/report.rs | 19 +- .../src/api/lang/jnigen/jni/selector.rs | 54 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 13 +- prebindgen/src/api/lang/jnigen/jni/symbol.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/symbols.rs | 9 +- .../src/api/lang/jnigen/jni/tests/aliasing.rs | 8 +- .../api/lang/jnigen/jni/tests/callbacks.rs | 39 +- .../src/api/lang/jnigen/jni/tests/config.rs | 158 +- .../src/api/lang/jnigen/jni/tests/consts.rs | 143 +- .../lang/jnigen/jni/tests/cross_artifact.rs | 12 +- .../src/api/lang/jnigen/jni/tests/flatten.rs | 353 ++-- .../src/api/lang/jnigen/jni/tests/mod.rs | 6 +- .../src/api/lang/jnigen/jni/tests/niches.rs | 2 +- .../src/api/lang/jnigen/jni/tests/sealed.rs | 273 +-- .../api/lang/jnigen/jni/tests/snapshots.rs | 60 +- .../src/api/lang/jnigen/jni/tests/symbols.rs | 94 +- .../api/lang/jnigen/jni/tests/value_form.rs | 178 +- .../src/api/lang/jnigen/jni/tests/values.rs | 264 +-- .../src/api/lang/jnigen/jni/trait_impl.rs | 730 +++---- prebindgen/src/api/lang/jnigen/mod.rs | 14 +- prebindgen/src/api/record.rs | 2 +- prebindgen/src/api/source.rs | 2 +- prebindgen/src/api/test_util.rs | 32 +- prebindgen/src/lib.rs | 83 +- 107 files changed, 5677 insertions(+), 5011 deletions(-) create mode 100644 prebindgen/src/api/core/diagnostics.rs create mode 100644 prebindgen/src/api/core/diagnostics/tests.rs delete mode 100644 prebindgen/src/api/core/registry.rs create mode 100644 prebindgen/src/api/core/registry/cell.rs create mode 100644 prebindgen/src/api/core/registry/declare.rs create mode 100644 prebindgen/src/api/core/registry/error.rs create mode 100644 prebindgen/src/api/core/registry/key.rs create mode 100644 prebindgen/src/api/core/registry/mod.rs create mode 100644 prebindgen/src/api/core/registry/model.rs create mode 100644 prebindgen/src/api/core/registry/order.rs create mode 100644 prebindgen/src/api/core/registry/run.rs create mode 100644 prebindgen/src/api/core/registry/scan.rs create mode 100644 prebindgen/src/api/core/registry/view.rs create mode 100644 prebindgen/src/api/core/registry/walk.rs diff --git a/examples/covertest-helpers/src/lib.rs b/examples/covertest-helpers/src/lib.rs index 8931eb00..0e2c850d 100644 --- a/examples/covertest-helpers/src/lib.rs +++ b/examples/covertest-helpers/src/lib.rs @@ -3,7 +3,7 @@ //! //! This crate exists to prove the multi-source model: a binding crate's //! `build.rs` chains SEVERAL prebindgen source streams into one registry -//! (`Registry::from_items(flat.items_all().chain(helpers.items_all()))`) and +//! (`Flat::builder().items(flat.items_all()).items(helpers.items_all())`) and //! the generated Rust qualifies each function with its origin crate. //! covertest-kotlin additionally RENAMES this dependency //! (`cov_helpers = { package = "covertest-helpers", .. }`) and overrides the diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index facf6762..81ccfde4 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -1,21 +1,21 @@ //! Build script generating Kotlin/JNI bindings for `perftest-flat` using -//! prebindgen's [`prebindgen::lang::JniGen`] adapter — exercising **every** -//! JniGen feature so the hand-written `kotlin/.../Test.kt` can assert each one. +//! prebindgen's [`prebindgen::lang::JniGenBuilder`] adapter — exercising **every** +//! JniGenBuilder feature so the hand-written `kotlin/.../Test.kt` can assert each one. //! //! Unlike `examples/perftest-kotlin` (which maps only the lean perf surface in //! the performance-optimal shape), this binding maps the *same* flat library — //! including the coverage-only items in `perftest_flat::ext` — through the full -//! adapter surface. `JniGen` accepts pre-built declaration objects (the +//! adapter surface. `JniGenBuilder` accepts pre-built declaration objects (the //! `prebindgen::lang` decl types, built by the root decl macros) rather than a fluent typestate //! chain — each row below is a `PackageDecl`/`ConvertDecl`/etc. built //! independently and then handed to `jni.package(...)` / `jni.convert(...)`: //! -//! | JniGen feature | Exercised by | +//! | JniGenBuilder feature | Exercised by | //! |--------------------------------------|--------------| //! | default module (first stream origin) | `perftest_flat` | -//! | `JniGen::set_package_prefix` | `io.prebindgen.covertest` | -//! | `JniGen::package` (subpackages) | `model` / `errors` / `analytics` / `storage` | -//! | `JniGen::set_jni_native_init` | `NativeLibrary.ensureLoaded()` | +//! | `JniGenBuilder::set_package_prefix` | `io.prebindgen.covertest` | +//! | `JniGenBuilder::package` (subpackages) | `model` / `errors` / `analytics` / `storage` | +//! | `JniGenBuilder::set_jni_native_init` | `NativeLibrary.ensureLoaded()` | //! | contextual name-mangle closures | package-aware class/function hooks + package/class-aware method hook | //! | `DataClassDecl` | `Payload`; `Annotated` (recursive direct + optional nested fields) | //! | `DataClassDecl::jobject_input()` | `ObjectBoundary` (127 `Long` leaves plus JNI infrastructure exceed the JVM's 255-slot method limit) | @@ -37,7 +37,7 @@ //! | `expand_return!` `.fields(fields!(…))` (#213) | `Report` — boundary DERIVED from the value form instead of restated; covers every per-field rule (spliced `Summary`, inlined `Stamp`, `Option`, a sum with a handle payload, a plain leaf) | //! | `expand_return!` `.fields_self_into(fields!(…))` | `report_into_struct(r: Report)` — the CONSUMING value form: the value is given away and its fields MOVED out, so the clones the borrowing `report_to_struct` pays are not emitted at all | //! | `PackageDecl::fun` / `FunctionDecl::name`| every free function; `.name` renames `millis_add` → `addMillis` | -//! | `Generation::report()` (C7) | `kotlin/REPORT.md` — the resolved surface, committed next to the regen | +//! | `JniGen::report()` (C7) | `kotlin/REPORT.md` — the resolved surface, committed next to the regen | //! | contextual method names | method hook strips `storage`/`stamp` class prefixes; `summary_new`→`.name("of")` still overrides | //! | per-class `.name()` | `Archive` → Kotlin `SummaryVault` (literal, bypasses mangles) | //! | `.interface()` + `.implements(…)` | `Storage`/`Payload` emit an Api interface; `CovResource`/`Timestamped` extend it (#54) | @@ -70,13 +70,13 @@ //! | binding-error channel (`JniErrorHandler`) | wrong-length `[u8; 2]` (fixed-size array length guard) | //! | callback no-throw contract | a throwing `PayloadCallback` (described + cleared per upcall) | //! | `data_class` instance member | `Payload.labelLen()` (receiver crosses as `this` field leaves) | -//! | `JniGen::ignore` (exact) | `string_len` / `storage_put_by_read_and_update` (acknowledged-unbound, no skip warnings) | -//! | `JniGen::ignore` + `matching(…)` | the `storage_get_into_*` group (one name predicate, any item kind) | +//! | `JniGenBuilder::ignore` (exact) | `string_len` / `storage_put_by_read_and_update` (acknowledged-unbound, no skip warnings) | +//! | `JniGenBuilder::ignore` + `matching(…)` | the `storage_get_into_*` group (one name predicate, any item kind) | //! //! One feature is deliberately left at its default and documented rather than //! toggled, because it is mutually exclusive with a richer path this example //! prefers to keep covered: -//! * `JniGen::set_emit_handle_locks` — kept ENABLED (default). Toggling +//! * `JniGenBuilder::set_emit_handle_locks` — kept ENABLED (default). Toggling //! it OFF would remove the `withSortedHandleLocks` codegen this example //! asserts against; a single binding can only be in one lock mode, so we //! keep the locked one. (The toggle is a verification aid, not an @@ -98,9 +98,9 @@ //! "skipping undeclared" build warning while emitting nothing. use prebindgen::{ - constant, convert, core::Registry, data_class, enum_class, expand_param, expand_return, expr, - fields, from, fun, into, lang::JniGen, matching, package, path, ptr_class, sealed_class, sig, - try_from, ty, variant, + constant, convert, data_class, enum_class, expand_param, expand_return, expr, fields, from, + fun, into, lang::JniGen, matching, package, path, ptr_class, sealed_class, sig, try_from, ty, + variant, }; fn strip_flat_class_prefix(class: &str, name: &str) -> String { @@ -118,7 +118,9 @@ fn strip_flat_class_prefix(class: &str, name: &str) -> String { } fn main() { - let jni = JniGen::new() + let binding = JniGen::builder() + .source(perftest_flat::PREBINDGEN_OUT_DIR) + .source_named(cov_helpers::PREBINDGEN_OUT_DIR, "cov_helpers") .set_package_prefix("io.prebindgen.covertest") .set_jni_native_init("io.prebindgen.covertest.NativeLibrary.ensureLoaded()") // Every naming tier used here is configured. The harness hook is a @@ -700,20 +702,14 @@ fn main() { // so the stamp recorded at capture time (`covertest-helpers`) would not // resolve from this crate — `source_named` overrides it with the name this // crate actually uses, per directory. - let registry = Registry::builder() - .source(perftest_flat::PREBINDGEN_OUT_DIR) - .source_named(cov_helpers::PREBINDGEN_OUT_DIR, "cov_helpers") - .build() - .expect("scan prebindgen items"); - let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); // Rust JNI wrappers → src/generated_bindings.rs (committed; included by lib.rs). let rust_dest = std::path::Path::new(&crate_dir) .join("src") .join("generated_bindings.rs"); - let gen = registry.resolve(jni).expect("resolve failed"); - let rust_path = gen.write_rust(&rust_dest).expect("write_rust failed"); + let jni = binding.build().expect("build failed"); + let rust_path = jni.write_rust(&rust_dest).expect("write_rust failed"); println!( "cargo:warning=Generated bindings at: {}", rust_path.display() @@ -725,7 +721,7 @@ fn main() { .join("generated"); // The root is prebindgen-owned: `write_kotlin` replaces marked output, // so no consumer-side cleanup is needed. - for path in gen.write_kotlin(&kotlin_root).expect("write_kotlin failed") { + for path in jni.write_kotlin(&kotlin_root).expect("write_kotlin failed") { println!("cargo:warning=Wrote {}", path.display()); } @@ -735,7 +731,7 @@ fn main() { std::path::Path::new(&crate_dir) .join("kotlin") .join("REPORT.md"), - gen.report(), + jni.report(), ) .expect("write REPORT.md"); } diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt index f061bb8b..f6cfd61b 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt @@ -138,7 +138,7 @@ public sealed interface Marker { * single-payload tuple variant, a multi-field named variant, and a tuple * variant whose payloads include a declared `enum_class`. The binding maps it * to a Kotlin `sealed interface` with the variants nested inside - * (`lang::JniGen` `sealed_class!`). + * (`lang::JniGenBuilder` `sealed_class!`). * * JVM-side surface for the native Rust `Reading` sum: exactly one alternative is live. */ diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/storage.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/storage.kt index a8dceed4..15615f32 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/storage.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/storage.kt @@ -179,7 +179,7 @@ public fun payloadHandlerNew( * element. In C the closure receives a `const payload_t *` (zero-copy); in Kotlin * the borrowed `Payload` is delivered whole to the handler's * `PayloadCallback.run(Payload)` (its fields cross as decoupled leaves and are - * reassembled on the Kotlin side — see `prebindgen::lang::JniGen`). + * reassembled on the Kotlin side — see `prebindgen::lang::JniGenBuilder`). */ public fun storageCallback(s: Storage, handler: PayloadHandler, onError: JniErrorHandler) { if (s.isClosed()) { onError.run("Operation on a closed native handle."); return } diff --git a/examples/example-cbindgen/build.rs b/examples/example-cbindgen/build.rs index 280dd34a..21057a14 100644 --- a/examples/example-cbindgen/build.rs +++ b/examples/example-cbindgen/build.rs @@ -1,7 +1,7 @@ //! Build script generating C bindings for `example-flat` using prebindgen + cbindgen. //! //! This is a language-specific binding crate. It reads the `#[prebindgen]` items -//! captured by `example-flat`, runs them through the `prebindgen::lang::Cbindgen` +//! captured by `example-flat`, runs them through the `prebindgen::lang::CbindgenBuilder` //! adapter to produce a Rust file of `extern "C"` wrappers, then runs cbindgen on //! that file to produce a C header. //! @@ -65,13 +65,14 @@ fn main() { } /// Generate the Rust FFI bindings from `example-flat`'s prebindgen output via the -/// `lang::Cbindgen` adapter, and publish the result to `generated/example_flat.rs`. +/// `lang::CbindgenBuilder` adapter, and publish the result to `generated/example_flat.rs`. fn generate_ffi_bindings() -> PathBuf { let unstable = std::env::var("CARGO_FEATURE_UNSTABLE").is_ok(); // The C / cbindgen adapter. Name-mangling rules turn each Rust type/function // into its C name, so no per-item `.name(...)` is needed. - let mut cbindgen = prebindgen::lang::Cbindgen::new() + let mut cbindgen = prebindgen::lang::Cbindgen::builder() + .source(example_flat::PREBINDGEN_OUT_DIR) .source_module(pq!(example_flat)) // Single universal freer for the `char*` data the layer hands out // (the `String` returns). Opaque handles keep their typed `*_drop`. @@ -212,15 +213,11 @@ fn generate_ffi_bindings() -> PathBuf { } // Reads example-flat's `#[prebindgen]` output straight from its directory. - let registry = prebindgen::core::Registry::builder() - .source(example_flat::PREBINDGEN_OUT_DIR) - .build() - .expect("scan prebindgen items"); // Always written to OUT_DIR under a stable name too, so the commented-out // `include!(OUT_DIR ...)` alternative in `lib.rs` works for any target. - let out_file = registry - .resolve(cbindgen) - .expect("resolve prebindgen items") + let out_file = cbindgen + .build() + .expect("build prebindgen items") .write_rust("example_flat.rs") .expect("write generated bindings"); diff --git a/examples/example-flat/src/lib.rs b/examples/example-flat/src/lib.rs index 8ff3b57d..99bd7db9 100644 --- a/examples/example-flat/src/lib.rs +++ b/examples/example-flat/src/lib.rs @@ -1,7 +1,7 @@ //! Flat, FFI-friendly example library — a miniature in the style of `zenoh-flat`. //! //! Every public function is annotated with `#[prebindgen]`, so `prebindgen` -//! captures this surface and a language adapter (here `prebindgen::lang::Cbindgen`, +//! captures this surface and a language adapter (here `prebindgen::lang::CbindgenBuilder`, //! driven by `example-cbindgen`) generates the FFI layer — no hand-written //! `extern "C"` glue, and **no `#[repr(C)]`** in this crate. //! @@ -66,7 +66,7 @@ pub enum Operation { /// with an **owning** payload (a `String`, which crosses to C as a malloc'd /// `char *`) beside a payload that is itself a declared enum. The C adapter /// lowers the whole thing to a `#[repr(C)]` enum, which cbindgen renders as the -/// idiomatic tag + `union`. (`lang::Cbindgen` `.tagged_union`.) +/// idiomatic tag + `union`. (`lang::CbindgenBuilder` `.tagged_union`.) #[prebindgen] #[derive(Debug, Clone, PartialEq)] pub enum Shape { @@ -445,7 +445,7 @@ pub fn calculator_reset(c: &mut Calculator) { /// A fieldless enum whose **discriminants differ by target architecture**. The two /// definitions are mutually exclusive — the `#[prebindgen(cfg = ...)]` macro emits a /// matching real `#[cfg]`, so each target compiles exactly one and the generated C -/// enum carries that target's values. (`lang::Cbindgen` `.enum_type`.) +/// enum carries that target's values. (`lang::CbindgenBuilder` `.enum_type`.) #[prebindgen("structs", cfg = "target_arch = \"x86_64\"")] #[repr(i32)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -464,7 +464,7 @@ pub enum InsideFoo { /// A by-value data struct whose **field set varies by target architecture and by /// feature**. `#[prebindgen]` records every `cfg`-gated field; the binding crate /// keeps only those matching the build target, so the generated `#[repr(C)] foo_t` -/// differs per target. (`lang::Cbindgen` `.data_struct`.) +/// differs per target. (`lang::CbindgenBuilder` `.data_struct`.) #[prebindgen("structs")] #[derive(Clone, Copy, Debug, Default, PartialEq)] pub struct Foo { diff --git a/examples/perftest-c/build.rs b/examples/perftest-c/build.rs index 93594dda..97a1d9e2 100644 --- a/examples/perftest-c/build.rs +++ b/examples/perftest-c/build.rs @@ -1,7 +1,7 @@ //! Build script generating C bindings for `perftest-flat` using prebindgen + cbindgen. //! //! It reads the `#[prebindgen]` items captured by `perftest-flat`, runs them through -//! the `prebindgen::lang::Cbindgen` adapter to produce a Rust file of `extern "C"` +//! the `prebindgen::lang::CbindgenBuilder` adapter to produce a Rust file of `extern "C"` //! wrappers, then runs cbindgen on that file to produce a C header. //! //! The headline feature exercised here is **`.repr_c_struct(Payload)`**: `Payload` @@ -26,11 +26,12 @@ fn main() { } /// Generate the Rust FFI bindings from `perftest-flat`'s prebindgen output via the -/// `lang::Cbindgen` adapter, and publish the result to `generated/perftest_.rs`. +/// `lang::CbindgenBuilder` adapter, and publish the result to `generated/perftest_.rs`. fn generate_ffi_bindings() -> PathBuf { // The C / cbindgen adapter. Name-mangling rules turn each Rust type/function // into its C name (e.g. `Payload` -> `payload_t`, `String` -> `string_t`). - let mut cbindgen = prebindgen::lang::Cbindgen::new() + let mut cbindgen = prebindgen::lang::Cbindgen::builder() + .source(perftest_flat::PREBINDGEN_OUT_DIR) .source_module(pq!(perftest_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -128,13 +129,9 @@ fn generate_ffi_bindings() -> PathBuf { cbindgen = cbindgen.function(pq!(storage_callback_vec)).panic(); // Reads perftest-flat's `#[prebindgen]` output straight from its directory. - let registry = prebindgen::core::Registry::builder() - .source(perftest_flat::PREBINDGEN_OUT_DIR) + let out_file = cbindgen .build() - .expect("scan prebindgen items"); - let out_file = registry - .resolve(cbindgen) - .expect("resolve prebindgen items") + .expect("build prebindgen items") .write_rust("perftest.rs") .expect("write generated bindings"); diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index 9dcb1de8..0e0ffb30 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -141,7 +141,7 @@ pub fn priority_or(p: Option, fallback: Priority) -> Priority { /// single-payload tuple variant, a multi-field named variant, and a tuple /// variant whose payloads include a declared `enum_class`. The binding maps it /// to a Kotlin `sealed interface` with the variants nested inside -/// (`lang::JniGen` `sealed_class!`). +/// (`lang::JniGenBuilder` `sealed_class!`). #[prebindgen] #[derive(Clone, Debug, PartialEq)] pub enum Reading { diff --git a/examples/perftest-flat/src/lib.rs b/examples/perftest-flat/src/lib.rs index 2911f9dd..22c8a682 100644 --- a/examples/perftest-flat/src/lib.rs +++ b/examples/perftest-flat/src/lib.rs @@ -1,7 +1,7 @@ //! Flat, FFI-friendly example library demonstrating a **zero-copy** data struct. //! //! Every public function is annotated with `#[prebindgen]`, so `prebindgen` -//! captures this surface and a language adapter (here `prebindgen::lang::Cbindgen`, +//! captures this surface and a language adapter (here `prebindgen::lang::CbindgenBuilder`, //! driven by `perftest-c`) generates the FFI layer — no hand-written `extern "C"` //! glue. //! @@ -228,7 +228,7 @@ pub fn payload_handler_new(f: impl Fn(&Payload) + Send + Sync + 'static) -> Payl /// element. In C the closure receives a `const payload_t *` (zero-copy); in Kotlin /// the borrowed `Payload` is delivered whole to the handler's /// `PayloadCallback.run(Payload)` (its fields cross as decoupled leaves and are -/// reassembled on the Kotlin side — see `prebindgen::lang::JniGen`). +/// reassembled on the Kotlin side — see `prebindgen::lang::JniGenBuilder`). #[prebindgen] pub fn storage_callback(s: &Storage, handler: &PayloadHandler) { for payload in &s.payloads { diff --git a/examples/perftest-kotlin/build.rs b/examples/perftest-kotlin/build.rs index 6be8e8d4..2be147c8 100644 --- a/examples/perftest-kotlin/build.rs +++ b/examples/perftest-kotlin/build.rs @@ -1,5 +1,5 @@ //! Build script generating Kotlin/JNI bindings for `perftest-flat` using -//! prebindgen's [`prebindgen::lang::JniGen`] adapter. It produces: +//! prebindgen's [`prebindgen::lang::JniGenBuilder`] adapter. It produces: //! * `src/generated_bindings.rs` — the Rust-side JNI wrappers (included by //! `src/lib.rs`), and //! * `kotlin/generated/**` — the matching typed Kotlin classes. @@ -13,7 +13,7 @@ //! (no Java object is built on the Rust side). //! //! `Payload.label` is `Option>` (an opaque-pointer string field); -//! JniGen maps `Box` → Kotlin `String` and `Option>` → +//! JniGenBuilder maps `Box` → Kotlin `String` and `Option>` → //! `String?` automatically. //! //! The `large_*_input_sum` pair compares the two Kotlin→Rust data-class input @@ -21,10 +21,12 @@ //! `ObjectBoundary64` is recursively flattened, while its structural twin //! `ObjectBoundary64Object` uses `.jobject_input()`. -use prebindgen::{core::Registry, data_class, fun, lang::JniGen, package, ptr_class}; +use prebindgen::{data_class, fun, lang::JniGen, package, ptr_class}; fn main() { - let jni = JniGen::new() + // Reads perftest-flat's `#[prebindgen]` output straight from its directory. + let binding = JniGen::builder() + .source(perftest_flat::PREBINDGEN_OUT_DIR) .set_package_prefix("io.prebindgen.perftest") // Trigger native-library loading from the generated `JNINative` static // init (the single choke point through which every JNI call routes). @@ -113,20 +115,14 @@ fn main() { .fun(fun!(storage_callback_vec)), ); - // Reads perftest-flat's `#[prebindgen]` output straight from its directory. - let registry = Registry::builder() - .source(perftest_flat::PREBINDGEN_OUT_DIR) - .build() - .expect("scan prebindgen items"); - let crate_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); // Rust JNI wrappers → src/generated_bindings.rs (committed; included by lib.rs). let rust_dest = std::path::Path::new(&crate_dir) .join("src") .join("generated_bindings.rs"); - let gen = registry.resolve(jni).expect("resolve failed"); - let rust_path = gen.write_rust(&rust_dest).expect("write_rust failed"); + let jni = binding.build().expect("build failed"); + let rust_path = jni.write_rust(&rust_dest).expect("write_rust failed"); println!( "cargo:warning=Generated bindings at: {}", rust_path.display() @@ -136,7 +132,7 @@ fn main() { let kotlin_root = std::path::Path::new(&crate_dir) .join("kotlin") .join("generated"); - for path in gen.write_kotlin(&kotlin_root).expect("write_kotlin failed") { + for path in jni.write_kotlin(&kotlin_root).expect("write_kotlin failed") { println!("cargo:warning=Wrote {}", path.display()); } } diff --git a/examples/perftest-kotlin/kotlin/generated/io/prebindgen/perftest/storage.kt b/examples/perftest-kotlin/kotlin/generated/io/prebindgen/perftest/storage.kt index 8c7b8f66..244ed386 100644 --- a/examples/perftest-kotlin/kotlin/generated/io/prebindgen/perftest/storage.kt +++ b/examples/perftest-kotlin/kotlin/generated/io/prebindgen/perftest/storage.kt @@ -115,7 +115,7 @@ public fun payloadHandlerNew( * element. In C the closure receives a `const payload_t *` (zero-copy); in Kotlin * the borrowed `Payload` is delivered whole to the handler's * `PayloadCallback.run(Payload)` (its fields cross as decoupled leaves and are - * reassembled on the Kotlin side — see `prebindgen::lang::JniGen`). + * reassembled on the Kotlin side — see `prebindgen::lang::JniGenBuilder`). */ public fun storageCallback(s: Storage, handler: PayloadHandler, onError: JniErrorHandler) { if (s.isClosed()) { onError.run("Operation on a closed native handle."); return } diff --git a/prebindgen/src/api/core/diagnostics.rs b/prebindgen/src/api/core/diagnostics.rs new file mode 100644 index 00000000..4c20f7e6 --- /dev/null +++ b/prebindgen/src/api/core/diagnostics.rs @@ -0,0 +1,165 @@ +//! What a binding did **not** claim, reported to the build log. +//! +//! Which items a binding skipped says nothing about which conversions it needs, +//! so none of this belongs to [`Registry`](crate::api::core::registry::Registry) +//! — that derives a crossing set from what **is** declared, and an ignore has no +//! effect on that set. What ignores are for is suppressing a report: telling +//! "you meant to skip this" apart from "you forgot this". +//! +//! So the ignores live here, with the reporting, and a generator calls +//! [`warn_unclaimed`] itself. + +use std::collections::HashSet; + +use crate::api::core::{ + flat::Flat, + prebindgen::NamePredicate, + registry::TypeKey, + types_util::{bare_path_ident, type_from_ident}, +}; + +/// What a binding claimed, so everything else can be reported. +/// +/// The two populations are separate on purpose. A **declared** item is claimed +/// and emitted; an **ignored** one is claimed and deliberately dropped. Both +/// silence the skip report, but only an ignore that matches nothing is itself +/// worth a warning — a declaration that matches nothing is a hard error the +/// registry raises long before this runs. +#[derive(Default)] +pub struct Claimed { + /// Functions the binding emits, plus the helpers it only references. + pub functions: HashSet, + /// Types the binding emits, plus the ones that cross only through a plan. + pub types: HashSet, + /// Consts the binding emits, or `None` when it has no const mechanism at + /// all — then every const is re-emitted verbatim, so none is ever skipped + /// and reporting one would be a lie. + pub consts: Option>, + pub ignored_functions: HashSet, + pub ignored_types: HashSet, + pub ignored_consts: HashSet, + /// Bulk ignores keyed on a naming family rather than an exact ident. + /// Kind-agnostic: prebindgen names live in one flat namespace. + pub ignored_name_predicates: Vec, +} + +impl Claimed { + /// Whether a bulk-ignore predicate covers this name. + /// + /// A predicate matching nothing is silent by design — it is a filter, not a + /// claim, and its match count varies across feature configurations. + fn predicate_ignored(&self, name: &str) -> bool { + !self.ignored_name_predicates.is_empty() + && self.ignored_name_predicates.iter().any(|p| p(name)) + } +} + +/// Print one `cargo:warning=` line per captured item this binding never +/// claimed, and per ignore entry that matches nothing. +pub fn warn_unclaimed(flat: &Flat, claimed: &Claimed) { + for line in unclaimed_report(flat, claimed) { + println!("cargo:warning={line}"); + } +} + +/// The report itself, as lines — so it can be asserted on rather than scraped +/// off stdout. Sorted within each group, so a build says the same thing twice. +pub(crate) fn unclaimed_report(flat: &Flat, claimed: &Claimed) -> Vec { + let mut out = Vec::new(); + + // Stale ignores: an entry naming nothing is a build.rs that has drifted + // from its source crate. + for ident in sorted(claimed.ignored_functions.iter().map(|i| i.to_string())) { + if flat.function(&ident_of(&ident)).is_none() { + out.push(format!( + "prebindgen: ignored function `{ident}` not found among #[prebindgen] items" + )); + } + } + for key in sorted(claimed.ignored_types.iter().map(|k| k.as_str().to_owned())) { + let named = TypeKey::parse(&key) + .ok() + .and_then(|k| bare_path_ident(&k.to_type())) + .is_some_and(|ident| flat.declared_type(&ident).is_some()); + if !named { + out.push(format!( + "prebindgen: ignored type `{key}` not found among #[prebindgen] items" + )); + } + } + if claimed.consts.is_some() { + for ident in sorted(claimed.ignored_consts.iter().map(|i| i.to_string())) { + if flat.constant(&ident_of(&ident)).is_none() { + out.push(format!( + "prebindgen: ignored const `{ident}` not found among #[prebindgen] items" + )); + } + } + } + + for name in sorted( + flat.functions() + .map(|f| &f.name) + .filter(|k| !claimed.functions.contains(*k) && !claimed.ignored_functions.contains(*k)) + .map(|k| k.to_string()) + .filter(|n| !claimed.predicate_ignored(n)), + ) { + out.push(format!( + "prebindgen: skipping undeclared #[prebindgen] fn `{name}`" + )); + } + + // Struct/enum only — an alias is deliberately absent, because the message + // names a kind an alias is not. + for name in sorted( + struct_enum_idents(flat) + .filter(|i| { + let key = TypeKey::from_type(&type_from_ident(i)); + !claimed.types.contains(&key) && !claimed.ignored_types.contains(&key) + }) + .map(|i| i.to_string()) + .filter(|n| !claimed.predicate_ignored(n)), + ) { + out.push(format!( + "prebindgen: skipping undeclared #[prebindgen] struct/enum `{name}`" + )); + } + + if let Some(declared) = &claimed.consts { + for name in sorted( + flat.constants() + .map(|c| &c.name) + .filter(|k| !declared.contains(*k) && !claimed.ignored_consts.contains(*k)) + .map(|k| k.to_string()) + .filter(|n| !claimed.predicate_ignored(n)), + ) { + out.push(format!( + "prebindgen: skipping undeclared #[prebindgen] const `{name}`" + )); + } + } + + out +} + +/// Every **struct or enum** name — either enum shape, never an alias. +fn struct_enum_idents(flat: &Flat) -> impl Iterator { + use crate::api::core::flat::Type; + flat.types().filter_map(|t| match t { + Type::Struct(_) | Type::Variant(_) | Type::Enum(_) => Some(t.name()), + Type::Extern(_) => None, + }) +} + +fn sorted(it: impl Iterator) -> Vec { + let mut v: Vec = it.collect(); + v.sort(); + v +} + +fn ident_of(name: &str) -> syn::Ident { + syn::Ident::new(name, proc_macro2::Span::call_site()) +} + +#[cfg(test)] +mod tests; diff --git a/prebindgen/src/api/core/diagnostics/tests.rs b/prebindgen/src/api/core/diagnostics/tests.rs new file mode 100644 index 00000000..d9c7d973 --- /dev/null +++ b/prebindgen/src/api/core/diagnostics/tests.rs @@ -0,0 +1,166 @@ +use std::collections::HashSet; + +use super::*; +use crate::api::core::flat::Flat; + +fn flat_with(sources: &[&str]) -> Flat { + let items = sources + .iter() + .map(|src| { + let item: syn::Item = syn::parse_str(src).expect("parse item"); + (item, crate::SourceLocation::default()) + }) + .collect::>(); + Flat::builder().items(items).build().expect("index") +} + +fn ident(n: &str) -> syn::Ident { + syn::parse_str(n).unwrap() +} + +/// The default report: everything captured and nothing claimed is a skip, one +/// line per item, functions before struct/enums. +#[test] +fn every_unclaimed_item_is_reported_once() { + let flat = flat_with(&[ + "pub fn a(x: u64) -> u64 { x }", + "pub fn b(x: u64) -> u64 { x }", + "pub struct S { pub v: u64 }", + "pub enum E { X = 1 }", + ]); + let lines = unclaimed_report(&flat, &Claimed::default()); + assert_eq!( + lines, + vec![ + "prebindgen: skipping undeclared #[prebindgen] fn `a`", + "prebindgen: skipping undeclared #[prebindgen] fn `b`", + "prebindgen: skipping undeclared #[prebindgen] struct/enum `E`", + "prebindgen: skipping undeclared #[prebindgen] struct/enum `S`", + ] + ); +} + +/// A claim silences the skip — whether the binding emits the item or only +/// references it (helpers and boundary-only types are folded into these sets +/// by the generator, precisely so both count as claimed). +#[test] +fn a_claimed_item_is_not_reported() { + let flat = flat_with(&[ + "pub fn a(x: u64) -> u64 { x }", + "pub struct S { pub v: u64 }", + ]); + let claimed = Claimed { + functions: HashSet::from([ident("a")]), + types: HashSet::from([TypeKey::parse("S").unwrap()]), + ..Claimed::default() + }; + assert!(unclaimed_report(&flat, &claimed).is_empty()); +} + +/// An ignore silences the skip exactly like a declaration does — of every kind. +/// The two differ only in what the registry does with them, which here is +/// nothing. +#[test] +fn an_ignored_item_is_not_reported_as_skipped() { + let flat = flat_with(&[ + "pub fn a(x: u64) -> u64 { x }", + "pub struct S { pub v: u64 }", + "pub const K: u64 = 7;", + ]); + let claimed = Claimed { + consts: Some(HashSet::new()), + ignored_functions: HashSet::from([ident("a")]), + ignored_types: HashSet::from([TypeKey::parse("S").unwrap()]), + ignored_consts: HashSet::from([ident("K")]), + ..Claimed::default() + }; + assert!( + unclaimed_report(&flat, &claimed).is_empty(), + "an ignore must suppress the skip line, not just the stale-ignore line" + ); +} + +/// A stale ignore — one naming nothing — is itself worth a line, because it +/// means build.rs has drifted from the source crate. It is only ever a warning: +/// a *declaration* that matches nothing is a hard error the registry raises. +#[test] +fn a_stale_ignore_is_reported() { + let flat = flat_with(&["pub fn a(x: u64) -> u64 { x }"]); + let claimed = Claimed { + functions: HashSet::from([ident("a")]), + ignored_functions: HashSet::from([ident("gone_fn")]), + ignored_types: HashSet::from([TypeKey::parse("Gone").unwrap()]), + ..Claimed::default() + }; + assert_eq!( + unclaimed_report(&flat, &claimed), + vec![ + "prebindgen: ignored function `gone_fn` not found among #[prebindgen] items", + "prebindgen: ignored type `Gone` not found among #[prebindgen] items", + ] + ); +} + +/// An **alias** is a captured item, so ignoring one by name is not stale — +/// `declared_type` counts aliases, unlike the struct/enum skip population. +#[test] +fn ignoring_an_alias_is_not_stale() { + let flat = flat_with(&[ + "pub type Handle = other::Inner;", + "pub fn f(x: u64) -> u64 { x }", + ]); + let claimed = Claimed { + functions: HashSet::from([ident("f")]), + ignored_types: HashSet::from([TypeKey::parse("Handle").unwrap()]), + ..Claimed::default() + }; + assert!(unclaimed_report(&flat, &claimed).is_empty()); + // …and the alias is never itself a "skipping undeclared struct/enum", + // because it is neither. + assert!(unclaimed_report(&flat, &Claimed::default()) + .iter() + .all(|l| !l.contains("Handle"))); +} + +/// An ignore predicate acknowledges matching undeclared items of EVERY kind — +/// fn, struct/enum, const (one flat namespace, so a name filter needs no kind) +/// — and a predicate matching nothing is silent: it is a filter, not a claim. +#[test] +fn an_ignore_predicate_covers_every_kind_and_is_silent_when_unmatched() { + let flat = flat_with(&[ + "pub fn helper_a(x: u64) -> u64 { x }", + "pub fn helper_b(x: u64) -> u64 { x }", + "pub struct HelperThing { pub v: u64 }", + "pub const HELPER_MAX: u64 = 1;", + ]); + let claimed = Claimed { + // Some(..) = this binding HAS a const mechanism, so consts are reported. + consts: Some(HashSet::new()), + ignored_name_predicates: vec![ + std::sync::Arc::new(|n: &str| n.to_lowercase().starts_with("helper")), + // A second, zero-match predicate is fine and says nothing. + std::sync::Arc::new(|n: &str| n.starts_with("nothing_")), + ], + ..Claimed::default() + }; + assert!(unclaimed_report(&flat, &claimed).is_empty()); +} + +/// `consts: None` means the binding has no const mechanism at all: every const +/// is re-emitted verbatim, so none was skipped and reporting one would be a lie. +/// This is the one asymmetry with functions and types. +#[test] +fn no_const_mechanism_reports_no_consts() { + let flat = flat_with(&["pub const K: u64 = 7;"]); + + assert!(unclaimed_report(&flat, &Claimed::default()).is_empty()); + + let declares_consts = Claimed { + consts: Some(HashSet::new()), + ..Claimed::default() + }; + assert_eq!( + unclaimed_report(&flat, &declares_consts), + vec!["prebindgen: skipping undeclared #[prebindgen] const `K`"] + ); +} diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index 55cbdb1a..95a661e0 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -166,7 +166,7 @@ fn validate_declarations(exp: &Expansions) -> Result<(), ExpandError> { /// claimed `#[prebindgen]` fn set — the domain over which `.default()` /// constructors auto-apply. /// -/// Runs inside `write_rust` after `scan_declared` and before `resolve`, so +/// Runs inside the builder's scan, before any conversion is built, so /// leaf converters resolve through the normal rank machinery. pub fn apply( registry: &mut Registry, diff --git a/prebindgen/src/api/core/expand/tests.rs b/prebindgen/src/api/core/expand/tests.rs index fe027c15..09a4fec9 100644 --- a/prebindgen/src/api/core/expand/tests.rs +++ b/prebindgen/src/api/core/expand/tests.rs @@ -1,7 +1,7 @@ use quote::ToTokens; use super::*; -use crate::api::test_util::reg_with; +use crate::api::{core::registry::Registry, test_util::scanned_with as reg_with}; fn src_qualify(id: &syn::Ident) -> syn::Path { syn::parse_quote!(zenoh_flat::#id) @@ -9,7 +9,7 @@ fn src_qualify(id: &syn::Ident) -> syn::Path { #[test] fn single_constructor_plan_and_fold() { - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", "fn z_keyexpr_intersects(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { todo!() }", ]); @@ -51,7 +51,7 @@ fn single_constructor_plan_and_fold() { #[test] fn constructor_plan_and_fold() { - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", "fn z_keyexpr_intersects(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { todo!() }", ]); @@ -117,7 +117,7 @@ fn constructor_plan_and_fold() { #[test] fn optional_byvalue_single_ctor() { // `attachment: Option` with single `z_zbytes_from_vec(Vec)`. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_zbytes_from_vec(bytes: Vec) -> ZZBytes { todo!() }", "fn z_session_delete(s: &ZSession, attachment: Option) -> bool { todo!() }", ]); @@ -170,7 +170,7 @@ fn optional_byvalue_single_ctor() { fn optional_byref_single_ctor() { // `encoding: Option<&ZEncoding>` with single, infallible // `z_encoding_from_string(String) -> ZEncoding`. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_encoding_from_string(s: String) -> ZEncoding { todo!() }", "fn z_session_put(s: &ZSession, encoding: Option<&ZEncoding>) -> bool { todo!() }", ]); @@ -216,7 +216,7 @@ fn optional_byref_multi_arg_ctor() { // `encoding: Option<&ZEncoding>` built from a TWO-arg, infallible // `z_encoding_from_id(i32, Option) -> ZEncoding`: an explicit // `present: bool` flag + two plain (non-`Option`-wrapped) arg leaves. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_encoding_from_id(id: i32, schema: Option) -> ZEncoding { todo!() }", "fn z_session_put(s: &ZSession, encoding: Option<&ZEncoding>) -> bool { todo!() }", ]); @@ -288,7 +288,7 @@ fn optional_combined_selector_encodes_absence() { // absence (`-1` = `None`). The ctor's own `Option` arg is a // PASSTHROUGH leaf (kept as `Option`, not double-wrapped — // `None` is a legitimate value for the taken arm). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_encoding_from_id(id: i32, schema: Option) -> ZEncoding { todo!() }", "fn z_session_put(s: &ZSession, encoding: Option<&ZEncoding>) -> bool { todo!() }", ]); @@ -415,7 +415,7 @@ fn iterable_emit_shape() { fn default_constructor_auto_applies_and_skips() { // A `.default()` ZKeyExpr constructor auto-`construct`s every matching // param of every declared fn — except where `.skip_default_construct`'d. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", "fn z_keyexpr_intersects(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { todo!() }", "fn z_session_undeclare(s: &ZSession, k: ZKeyExpr) -> bool { todo!() }", @@ -459,7 +459,7 @@ fn default_constructor_auto_applies_and_skips() { #[test] fn default_constructor_skips_accessor_and_explicit_construct_errors() { - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", "fn z_keyexpr_intersects(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { todo!() }", "fn z_keyexpr_clone(ke: &ZKeyExpr) -> ZKeyExpr { todo!() }", @@ -488,7 +488,7 @@ fn default_constructor_skips_accessor_and_explicit_construct_errors() { .contains_key(&(ident("z_keyexpr_clone"), ident("ke")))); // An explicit per-fn input flatten on an accessor is a build error. - let mut reg2 = reg_with(&[ + let mut reg2: Registry<()> = reg_with(&[ "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", "fn z_keyexpr_clone(ke: &ZKeyExpr) -> ZKeyExpr { todo!() }", ]); @@ -509,7 +509,7 @@ fn recursive_input_nests_param_constructors() { // by z_reply_sample(sample: ZSample). ZSample's default input expands // the `sample` param into z_sample_new's params, each of which (ZKeyExpr, // ZZBytes) recursively expands per ITS default input. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_sample_new(key_expr: ZKeyExpr, payload: ZZBytes) -> ZSample { todo!() }", "fn z_keyexpr_try_from(s: String) -> ZKeyExpr { todo!() }", "fn z_zbytes_from_vec(b: Vec) -> ZZBytes { todo!() }", @@ -599,7 +599,7 @@ fn recursive_input_nests_param_constructors() { #[test] fn recursive_input_cycle_errors() { // A → B → A constructor cycle is a build error (not an infinite expansion). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn make_a(b: B) -> A { todo!() }", "fn make_b(a: A) -> B { todo!() }", "fn consume_a(a: A) -> bool { todo!() }", @@ -637,7 +637,7 @@ fn recursive_input_cycle_errors() { #[test] fn unknown_constructor_errors() { use crate::api::core::types_util::ident; - let mut reg = + let mut reg: Registry<()> = reg_with(&["fn z_keyexpr_intersects(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { todo!() }"]); let mut exp = Expansions::default(); exp.expands.push(ExpandDecl { @@ -666,7 +666,7 @@ fn unknown_constructor_errors() { #[test] fn constructor_target_mismatch_errors() { use crate::api::core::types_util::ident; - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_sample_new(s: String) -> ZSample { todo!() }", "fn z_keyexpr_intersects(a: &ZKeyExpr, b: &ZKeyExpr) -> bool { todo!() }", ]); @@ -694,7 +694,7 @@ fn constructor_target_mismatch_errors() { #[test] fn invalid_declarations_collected() { use crate::api::core::types_util::ident; - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", "fn z_session_get(s: &ZSession, k: &ZKeyExpr) -> bool { todo!() }", ]); diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 988c22fc..34a8796e 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -34,23 +34,27 @@ # * helper delegation — `jnigen/jni/classify.rs` is a whole classifier with # zero watched sites; # * syn enums outside WATCHED: Item, Fields, FnArg, ReturnType, -# GenericArgument, Pat, ...; -# * `types_util::match_pattern` unification against `parse_quote!(_)` -# patterns, which adds a shape rule with no watched site at all. +# GenericArgument, Pat, ... +# +# One listed gap is closed rather than still open: `types_util::match_pattern` +# unified against `parse_quote!(_)` patterns, adding shape rules with no watched +# site. It is deleted — the wildcard tables it served held a single entry, +# `Result<_, _>`, which the model already names `TypeKind::Fallible`. # # A check that silently under-reports is worse than no check, which is why the # gaps are listed here rather than implied away. 4 api/core/expand.rs -11 api/core/registry.rs -40 api/core/types_util.rs +2 api/core/registry/scan.rs +9 api/core/registry/walk.rs +14 api/core/types_util.rs 16 api/core/unfold.rs 8 api/lang/cbindgen/builder.rs 1 api/lang/cbindgen/convert.rs 5 api/lang/cbindgen/emit.rs 5 api/lang/cbindgen/mod.rs 6 api/lang/cbindgen/trait_impl.rs -13 api/lang/jnigen/jni/builder.rs +4 api/lang/jnigen/jni/builder.rs 1 api/lang/jnigen/jni/emit/callback.rs 4 api/lang/jnigen/jni/emit/convert.rs 2 api/lang/jnigen/jni/emit/delivery.rs @@ -70,4 +74,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 202 +# total: 167 diff --git a/prebindgen/src/api/core/flat/boundary.rs b/prebindgen/src/api/core/flat/boundary.rs index b0c9d1ae..10231ebb 100644 --- a/prebindgen/src/api/core/flat/boundary.rs +++ b/prebindgen/src/api/core/flat/boundary.rs @@ -121,9 +121,12 @@ const HEADER: &str = "\ # * helper delegation — `jnigen/jni/classify.rs` is a whole classifier with # zero watched sites; # * syn enums outside WATCHED: Item, Fields, FnArg, ReturnType, -# GenericArgument, Pat, ...; -# * `types_util::match_pattern` unification against `parse_quote!(_)` -# patterns, which adds a shape rule with no watched site at all. +# GenericArgument, Pat, ... +# +# One listed gap is closed rather than still open: `types_util::match_pattern` +# unified against `parse_quote!(_)` patterns, adding shape rules with no watched +# site. It is deleted — the wildcard tables it served held a single entry, +# `Result<_, _>`, which the model already names `TypeKind::Fallible`. # # A check that silently under-reports is worse than no check, which is why the # gaps are listed here rather than implied away. diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index a905a9ce..ac8f70b9 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -233,7 +233,7 @@ use crate::SourceLocation; /// reach into, one set of source modules to normalize paths against, and every /// type reference resolving against every declaration. None can be /// decided per input, so every input is in hand before any of it is classified. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct FlatBuilder { items: Vec<(syn::Item, SourceLocation)>, } diff --git a/prebindgen/src/api/core/gravestone.rs b/prebindgen/src/api/core/gravestone.rs index 7998b1e2..0e8afefd 100644 --- a/prebindgen/src/api/core/gravestone.rs +++ b/prebindgen/src/api/core/gravestone.rs @@ -3,7 +3,7 @@ //! //! Unlike the rest of `core`, these are *runtime* traits: they are implemented on //! the opaque counterpart of an inline-by-value Rust type and called from the -//! `extern "C"` converters that [`crate::lang::Cbindgen`] emits for a +//! `extern "C"` converters that [`crate::lang::CbindgenBuilder`] emits for a //! `value_opaque` declaration. //! //! An inline-opaque type is passed across the C ABI *by value* (no `Box`): the diff --git a/prebindgen/src/api/core/mod.rs b/prebindgen/src/api/core/mod.rs index 9b975338..d66cbb4e 100644 --- a/prebindgen/src/api/core/mod.rs +++ b/prebindgen/src/api/core/mod.rs @@ -1,24 +1,23 @@ //! Core: language-agnostic primitives for the Registry-based pipeline. //! -//! The new API pipeline is intentionally phase-oriented: +//! The pipeline is phase-oriented, and a generator drives it: //! -//! 1. [`registry::Registry::from_items`] indexes `(syn::Item, SourceLocation)` -//! records into one flat namespace. This phase is index-only: it does not -//! inspect function signatures or mark any type required. -//! 2. [`registry::Registry::scan_declared`] asks the configured -//! [`prebindgen::Prebindgen`] adapter which functions and types it claims, -//! then scans only those items into input/output type requirements. -//! 3. Adapter-provided constructor and deconstructor declarations are resolved -//! into expansion/unfold plans and register their leaf requirements. -//! 4. The fixed-point resolver asks the adapter for input/output converters -//! until no unresolved type advances, then propagates `ConverterImpl::subs` -//! from required roots. -//! 5. [`registry::Registry::write_rust`] emits adapter prerequisites, -//! converters, per-item wrapper Rust, and verbatim anonymous consts. +//! 1. [`flat::Flat::builder`] parses `(syn::Item, SourceLocation)` records into +//! one flat namespace; [`registry::Registry::builder`] starts describing a +//! binding over the model. +//! 2. The generator states its binding — which elements it exports, which types +//! cross, how composites decompose — and the builder derives the crossing +//! set from it. +//! 3. [`registry::Registry::crossings`] hands that set over inner-first; the +//! generator builds a conversion for each and returns them all through +//! `RegistryBuilder::convert_with`, and `build` checks the set is complete. +//! 4. The emitter writes prerequisites, converters, per-item wrapper Rust, and +//! verbatim anonymous consts. //! //! Secondary artifacts such as C headers or Kotlin sources are produced by the //! language adapter after the Rust registry is resolved. +pub mod diagnostics; pub mod domain; pub mod expand; pub mod flat; @@ -33,13 +32,15 @@ pub mod unfold; pub(crate) mod write; pub use self::{ + diagnostics::{warn_unclaimed, Claimed}, domain::{DomainScalar, RepresentationDomain, ScalarValue}, flat::{Element, Flat}, gravestone::{Gravestone, Transmute}, niches::{NicheSlot, Niches}, prebindgen::{const_path_alias, ConverterImpl, Prebindgen, Stage}, registry::{ - Direction, Generation, Registry, RegistryBuilder, ScanError, TypeCell, TypeEntry, TypeKey, - TypeSubject, WriteRustError, + Building, Conversions, Crossing, Decompositions, Direction, DuplicateNameError, + NotExpressibleEntry, Registry, ScanError, TypeEntry, TypeKey, TypeKeyParseError, + WriteRustError, }, }; diff --git a/prebindgen/src/api/core/prebindgen.rs b/prebindgen/src/api/core/prebindgen.rs index 84effb99..6853ad79 100644 --- a/prebindgen/src/api/core/prebindgen.rs +++ b/prebindgen/src/api/core/prebindgen.rs @@ -1,30 +1,23 @@ -//! `Prebindgen` — the single extension point for the new pipeline. +//! `Prebindgen` — what a generator still hands the emitter. //! //! One method per `#[prebindgen]` item kind (`on_function`, `on_struct`, -//! `on_enum`, `on_const`) returning the wrapper Rust tokens to emit, plus a -//! pair of structural converter methods split by direction: +//! `on_enum`, `on_const`) returning the wrapper Rust tokens to emit, plus the +//! items they depend on (`prerequisites`), a cross-cutting rewrite +//! (`post_process_item`) and two invariant checks. //! -//! * Input (wire → rust): `on_input_type` -//! * Output (rust → wire): `on_output_type` +//! **Conversion is not here.** A generator builds those itself, against the +//! demand `RegistryBuilder::crossings` hands it, and gives them back through +//! `RegistryBuilder::convert_with` — so there is no `on_input_type`, no deferral, and no +//! fixed-point loop retrying until it converges. //! -//! Each converter method returns `Some(ConverterImpl)` if the adapter handles -//! the type, or `None` to defer. Deferred types are retried by the fixed-point -//! resolver and ultimately reported as "unresolved required type" errors if no -//! converter can fill the cell. -//! -//! `ConverterImpl::function` is the **complete** Rust function for the -//! converter — signature, body, attributes, lifetimes. The adapter owns -//! 100% of the shape. Other code that wants to call this converter reads -//! the name from `function.sig.ident`; the wire form from `destination`. - -use std::collections::HashSet; +//! [`ConverterImpl::function`] is the **complete** Rust function for a +//! converter — signature, body, attributes, lifetimes. The generator owns 100% +//! of the shape. Callers read the name from `function.sig.ident` and the wire +//! form from `destination`. use proc_macro2::TokenStream; -use crate::api::core::{ - niches::Niches, - registry::{Direction, Registry, TypeKey}, -}; +use crate::api::core::{niches::Niches, registry::Registry}; /// A shared predicate over an item name, as used by /// [`Prebindgen::ignored_name_predicates`] (bulk ignores keyed on a naming @@ -109,8 +102,8 @@ pub struct ConverterImpl { /// `input_entry`/`output_entry` the adapter looked up to build a wrapper /// (`Option` → `[X]`, `Result` → `[T, E]`, `&T` → `[&T]`). Empty /// for a terminal converter (scalar, opaque handle, string) and for - /// `dispatch_fn_input` (callback args are cross-direction — their - /// required-ness flows through `Registry::immediate_edges`, not here). The + /// a callback's own converter (callback args are cross-direction — their + /// required-ness flows through the registry's type-graph edges, not here). The /// resolver copies these into `TypeEntry::subs`, which `propagate_required` /// walks to mark reachable types required. pub subs: Vec, @@ -138,14 +131,20 @@ pub fn const_path_alias(c: &syn::ItemConst, source_module: &syn::Path) -> TokenS /// the language-agnostic [`Registry`] how that language represents Rust types /// on the wire and what wrapper code to emit. /// -/// The trait has no language-specific concepts of its own. Two jobs: -/// * **Type resolution.** The resolver asks `on_input_type` / `on_output_type` -/// for the wire form of each required type and gets back a [`ConverterImpl`] -/// (a generated converter fn + its wire type); these fill -/// `Registry::input_types` / `output_types`. -/// * **Per-item emission.** The file emitter calls `on_function` / `on_struct` -/// / `on_enum` / `on_const` to produce the per-item wrapper code for the -/// destination language. +/// The trait has no language-specific concepts of its own, and — since the +/// registry stopped asking it questions — one job left: **per-item emission**. +/// The file emitter calls `on_function` / `on_struct` / `on_enum` / `on_const` +/// to produce the per-item wrapper code, plus `prerequisites` and +/// `post_process_item` around them and the two `validate` hooks for +/// adapter invariants. +/// +/// What used to be here and is not any more: which items to build, how +/// composites decompose, and the wire form of each type. A generator states the +/// first two into the builder (`RegistryBuilder::export`, +/// `RegistryBuilder::decompose`) +/// and answers the third by filling `RegistryBuilder::crossings` — so nothing in +/// core calls back to ask. Moving emission out too is what would delete this +/// trait entirely (prebindgen#251 phase E). /// /// Anything language-specific the rest of the pipeline must carry — a JNI /// adapter's Kotlin class names and exception info, a C adapter's header @@ -174,270 +173,8 @@ pub trait Prebindgen { Vec::new() } - /// Constructor-expansion declarations for this adapter, or `None` if it - /// doesn't support expansion. Consulted by `write_rust` after scanning and - /// before resolution: each `.expand` is resolved into a - /// [`crate::api::core::expand::FoldPlan`] on the registry and its leaf - /// types are registered as required inputs. - /// - /// Returned by value so the adapter may assemble it on demand from its - /// raw declarations (keeping its builder free of stored derived state); - /// it is consulted exactly once per `write_rust`. - /// - /// Default: `None`. - fn expansions(&self) -> Option { - None - } - - /// Output-expansion (deconstructor / converter) declarations for this - /// adapter, or `None` if it doesn't support output expansion. Consulted by - /// `write_rust` after `expansions` and before resolution: each - /// `.deconstruct_output` / `.convert_output` is resolved into a - /// [`crate::api::core::unfold::UnfoldPlan`] on the registry and its leaf - /// types are registered as required outputs. - /// - /// Returned by value, same as [`Self::expansions`]. The registry is - /// available because a declaration may name a **value form** (an accessor - /// returning "this type's fields in one struct") whose fields have to be - /// read off the indexed struct to become records. - /// - /// Default: `None`. - fn deconstructors( - &self, - registry: &Registry, - ) -> Option { - let _ = registry; - None - } - - /// Synthesized by-value `data_class` decompositions for this adapter. Each - /// names a value struct and its field-access leaves (the adapter knows the - /// per-field encoding — projections, enums, nested classes — so it builds - /// the leaves; the registry is available so field converters resolve). - /// Consulted by `write_rust` right after [`Self::deconstructors`]: each is - /// wired by [`crate::api::core::unfold::apply_value_structs`] into a - /// fixed-builder [`crate::api::core::unfold::UnfoldPlan`] for every function - /// that returns / callbacks the struct, so it crosses the boundary as - /// decoupled leaves (reassembled on the foreign side) instead of a Java - /// object built on the Rust side. - /// - /// Default: empty. - fn value_struct_decons( - &self, - _registry: &Registry, - ) -> Vec { - Vec::new() - } - - /// Synthesized **sum** decompositions for this adapter — the - /// selector-carrying sibling of [`Self::value_struct_decons`]. Each names a - /// data-carrying enum, its synthesized tag leaf and one leaf group per - /// alternative. Consulted by `write_rust` right after - /// [`Self::value_struct_decons`]: each is wired by - /// [`crate::api::core::unfold::apply_sum_returns`] into a fixed-builder - /// [`crate::api::core::unfold::UnfoldPlan`] for every function whose own - /// return (or callback argument) IS the sum, so the value crosses as a tag - /// plus tag-gated groups and the foreign side picks the live alternative. - /// - /// Default: empty. - fn sum_decons( - &self, - _registry: &Registry, - ) -> Vec { - Vec::new() - } - - /// Element types the adapter nominates for a **whole-element leaf fold**: a - /// `Vec` / `Option>` return (or `impl Fn(&[T])` callback arg) whose - /// element `T` is a single boundary leaf (e.g. a String, a scalar, an - /// opaque handle) the foreign side can reassemble from one wire value. The - /// single-leaf analog of [`Self::value_struct_decons`]: consulted right after - /// it and wired by [`crate::api::core::unfold::apply_leaf_vec_folds`] so the - /// collection crosses as decoupled raw leaves folded into a foreign-built list - /// instead of a `java.util.ArrayList` built on the Rust side. Multi-field - /// `data_class` elements are excluded (they go through - /// [`Self::value_struct_decons`]). - /// - /// Default: empty. - fn leaf_vec_fold_elements(&self, _registry: &Registry) -> Vec { - Vec::new() - } - // ── Declaration queries ──────────────────────────────────────── - /// Idents of `#[prebindgen]` functions the adapter claims for emission. - /// Anything not in this set is left in the registry's `functions` - /// map but never scanned for type requirements and never emitted — - /// the build prints a `cargo:warning=` line per skip. - /// - /// Default: empty (strict allowlist; an adapter with no declarations - /// emits nothing for functions). - fn declared_functions(&self) -> HashSet { - HashSet::new() - } - - /// Subset of [`Self::declared_functions`] declared as **read accessors**: - /// the parameter composer (constructor expansion) is never applied to them, - /// and a decomposer record may only reference one. Adapters without the - /// concept return empty (then no fn is treated as an accessor). - /// - /// Default: empty. - fn accessor_functions(&self) -> HashSet { - HashSet::new() - } - - /// **Binding-local functions** to synthesize into the registry before - /// scanning: `(item, origin module path)` pairs built from - /// adapter-declared signatures (there is no `#[prebindgen]` item behind - /// them — the fn lives in the binding crate and the generated code calls - /// it qualified by `origin`). The item's body is never emitted; only its - /// signature is read. A synthesized ident colliding with a real - /// `#[prebindgen]` item is a hard resolve error. Adapters without the - /// concept return empty. - /// - /// Default: empty. - fn local_functions(&self) -> Vec<(syn::ItemFn, String)> { - Vec::new() - } - - /// `#[prebindgen]` functions declared as **methods** of a class, mapping the - /// fn ident to its class's canonical [`TypeKey`]. A method's first parameter - /// of that class type is the receiver and is excluded from input-flattening - /// (it is bound to `this`); the remaining parameters flatten normally. - /// Adapters without the concept return empty. - /// - /// Default: empty. - fn method_receivers(&self) -> std::collections::HashMap { - std::collections::HashMap::new() - } - - /// Idents of `#[prebindgen]` functions the adapter explicitly knows about but - /// intentionally does not emit. These suppress the registry's - /// "skipping undeclared" warning while still leaving the items out of the - /// scan and write pipelines. - /// - /// Default: empty. - fn ignored_functions(&self) -> HashSet { - HashSet::new() - } - - /// Bulk form of the `ignored_*` sets: predicates over the item NAME — - /// every *undeclared* `#[prebindgen]` item (function, struct/enum, or - /// const) whose name matches any predicate is an acknowledged skip (no - /// "skipping undeclared" warning). Kind-agnostic by design: prebindgen - /// items live in one flat namespace, so a name filter needs no kind. A - /// declared item matching a predicate is unaffected (declaration wins), - /// and a predicate matching nothing is silent — it is a filter, not a - /// claim, so unlike an exact-name ignore there is no "not found" - /// warning. - /// - /// Default: empty. - fn ignored_name_predicates(&self) -> Vec { - Vec::new() - } - - /// Idents of `#[prebindgen]` **helper** functions: called from the - /// adapter's generated converter bodies rather than exported. No - /// extern/wrapper is emitted for them and the "skipping undeclared" - /// warning is suppressed; the specific types a helper makes the adapter - /// depend on are registered via [`Self::extra_required_types`] (a full - /// signature scan would over-require — e.g. an output conversion fn's - /// `&T` parameter has no input-direction meaning). - /// - /// Default: empty. - fn helper_functions(&self) -> HashSet { - HashSet::new() - } - - /// Extra converter requirements the adapter derives from its own decls - /// **with registry access** (e.g. a `convert!` conversion fn's - /// other-side type, in the conversion's direction, read from the fn's - /// registry signature). Consulted by `write_rust` after the adapter's - /// plans are applied and before resolution. - /// - /// Default: none. - fn extra_required_types( - &self, - _registry: &Registry, - ) -> Vec<(Direction, syn::Type)> { - Vec::new() - } - - /// Idents of `#[prebindgen]` consts the adapter claims for emission. - /// - /// * `None` (default) — the adapter has **no const declaration - /// mechanism**: every indexed const is re-emitted into the generated - /// Rust via [`Self::on_const`] (a path-alias when - /// [`Self::source_module`] is available, verbatim otherwise), none - /// drives type resolution, and no skip warnings are printed. - /// * `Some(set)` — declared-only, symmetric with functions: a declared - /// const's type is scanned as a required **output** type, only - /// declared consts reach [`Self::on_const`], and undeclared ones get - /// a `cargo:warning=` skip line (suppressed via - /// [`Self::ignored_consts`]). - fn declared_consts(&self) -> Option> { - None - } - - /// Idents of `#[prebindgen]` consts the adapter explicitly knows about - /// but intentionally does not emit — suppresses the "skipping - /// undeclared" warning. Only meaningful when [`Self::declared_consts`] - /// returns `Some`. - /// - /// Default: empty. - fn ignored_consts(&self) -> HashSet { - HashSet::new() - } - - /// Extra types the adapter requires in the **output** direction beyond - /// what scanning the declared items discovers — for adapter-synthesized - /// values that have no `#[prebindgen]` item to scan (e.g. the declared - /// value type of a binding-defined expression constant). - /// - /// Default: none. - fn required_output_types(&self) -> Vec { - Vec::new() - } - - /// Canonical keys of types (structs / enums) the adapter claims for - /// emission. Matched against `Registry::structs` and `Registry::enums` - /// by bare-ident lookup. Anything not in this set is left in the - /// registry but never scanned for body type requirements and never - /// emitted — the build prints a `cargo:warning=` line per skip. - /// - /// Default: empty (strict allowlist). - fn declared_types(&self) -> HashSet { - HashSet::new() - } - - /// Canonical keys of types the adapter explicitly knows about but - /// intentionally does not emit. These suppress the registry's - /// "skipping undeclared" warning while still leaving the items out of the - /// scan and write pipelines. - /// - /// Default: empty. - fn ignored_types(&self) -> HashSet { - HashSet::new() - } - - /// Canonical keys of the adapter's **boundary-only** (rust-side-only) - /// types: types the adapter converts exclusively through its - /// expansion/deconstruction plans — built from ingredients on input, - /// decomposed into fields on output — so the value itself never crosses - /// the boundary and has no destination-language representation. - /// - /// `write_rust` treats them as acknowledged (no "skipping undeclared" - /// warning) and, after the adapter's plans are applied, drops their - /// direct converter requirements in both directions - /// ([`crate::api::core::registry::Registry`]'s `unrequire_input` / - /// `unrequire_output`) — a direct converter for such a type is genuinely - /// not needed and typically cannot resolve. - /// - /// Default: empty. - fn boundary_only_types(&self) -> HashSet { - HashSet::new() - } - /// Final post-processing pass applied to every emitted item right /// before write. Default: no-op. /// @@ -451,7 +188,7 @@ pub trait Prebindgen { /// Adapter-invariant checks that need registry **signatures** — the /// earliest they can run (decl objects are built before any source is - /// read). Called by `Registry::resolve` right after the declaration + /// read). Called by `RegistryBuilder::validate_with` right after the declaration /// scan (so a missing fn has already hard-errored; validate sees only /// indexed items) and before plan application. An `Err` aborts the /// resolve as `ScanError::AdapterInvariant` with the message verbatim @@ -459,7 +196,10 @@ pub trait Prebindgen { /// receiver parameter of the class type. /// /// Default: no checks. - fn validate(&self, _registry: &Registry) -> Result<(), String> { + fn validate( + &self, + _binding: &crate::api::core::registry::Building<'_, Self::Metadata>, + ) -> Result<(), String> { Ok(()) } @@ -516,55 +256,4 @@ pub trait Prebindgen { None => c.to_token_stream(), } } - - // ── Structural type resolution (the converter-resolution surface) ── - - /// Resolve the **input** (wire → rust) converter for `ty`. The adapter - /// inspects `ty`'s outermost structure itself (peeling with - /// `core::types_util` helpers) and returns either a *terminal* converter - /// (`ConverterImpl::subs` empty) or a *wrapper* that looked up inner - /// converters via [`Registry::input_entry`] (listing those inners in - /// `subs`). Return `None` to **defer** — when an inner isn't resolved yet - /// the resolver retries on a later fixed-point iteration. - fn on_input_type( - &self, - ty: &syn::Type, - registry: &Registry, - ) -> Option>; - - /// Resolve the **output** (rust → wire) converter for `ty`. The dual of - /// [`Self::on_input_type`]; same terminal-vs-wrapper / `subs` / defer - /// contract, looking up inners via [`Registry::output_entry`]. - fn on_output_type( - &self, - ty: &syn::Type, - registry: &Registry, - ) -> Option>; - - /// Build the wrapper converter for an - /// `impl Fn(args...) + Send + Sync + 'static` parameter, given the - /// already-extracted arg types in declaration order. The resolver calls - /// this only after [`Self::on_input_type`] returns `None`, so wrappers that - /// need custom callback dispatch can intercept earlier and skip this path. - /// - /// `args` are the rust-side argument types as they appear in the source - /// signature. Note that callback args flow inverse to the callback - /// parameter itself: the callback parameter is *input*, but its args are - /// produced by the rust side and consumed by the foreign side, so they are - /// *output* direction for converter resolution. The framework handles this - /// direction-flip at registration time (`register_type_inner` in - /// `core::registry`), so implementations of this method should look up - /// already-registered *output* converters for each arg type. The returned - /// `ConverterImpl::subs` should be empty — the callback-arg required-ness - /// flows through that direction-flipped `immediate_edges`, not `subs`. - /// - /// Default: `None`. Adapters that support `impl Fn` callbacks override this. - fn dispatch_fn_input( - &self, - args: &[syn::Type], - registry: &Registry, - ) -> Option> { - let _ = (args, registry); - None - } } diff --git a/prebindgen/src/api/core/registry.rs b/prebindgen/src/api/core/registry.rs deleted file mode 100644 index 699ae4a0..00000000 --- a/prebindgen/src/api/core/registry.rs +++ /dev/null @@ -1,1671 +0,0 @@ -//! Single owner of everything parsed from the prebindgen source stream. -//! -//! [`Registry`] holds: -//! * Item maps (`functions`, `structs`, `enums`, `consts`) indexed by ident. -//! Duplicate names across kinds OR within a kind are an error — prebindgen -//! items live in one flat namespace. -//! * `guards` — anonymous consts, emitted verbatim. Not API: having no name, -//! they cannot be declared, so they are neither gated nor addressable. -//! * `input_types` / `output_types` — direction-specific type tables. Each -//! scanned type maps to either a resolved [`TypeEntry`] or an unresolved cell -//! that the fixed-point resolver can retry. -//! * Expansion/deconstruction sidecars — adapter declarations are resolved into -//! plans before type resolution, then consumed at wrapper-emission sites. - -use std::{ - collections::{HashMap, HashSet}, - fmt, - marker::PhantomData, -}; - -use quote::ToTokens; - -use crate::{ - api::core::{ - niches::Niches, - prebindgen::{Prebindgen, Stage}, - types_util::bare_path_ident, - }, - SourceLocation, -}; - -/// Canonical type-shape key: identity is the token string of the -/// **normalized** type ([`crate::api::core::types_util::normalize_type`] — -/// group/paren unwrap, `crate::`/`self::` and std-prelude path reduction; -/// the complete equivalence rule set is documented there). The normalized -/// parsed form is kept alongside the string, so [`Self::to_type`] is an -/// infallible clone — no core invariant depends on serialize-then-reparse -/// round trips (issue #95). -#[derive(Clone)] -pub struct TypeKey { - /// Canonical token string — the identity `Eq`/`Hash` compare. - canon: std::rc::Rc, - /// The normalized parsed form the string was rendered from. - ty: std::rc::Rc, -} - -impl PartialEq for TypeKey { - fn eq(&self, other: &Self) -> bool { - self.canon == other.canon - } -} -impl Eq for TypeKey {} -impl std::hash::Hash for TypeKey { - fn hash(&self, state: &mut H) { - self.canon.hash(state) - } -} -impl PartialOrd for TypeKey { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} -impl Ord for TypeKey { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - self.canon.cmp(&other.canon) - } -} -// Keep the historical single-field tuple rendering (`TypeKey("Vec < u8 >")`) -// — error text and test expectations format keys through it. -impl fmt::Debug for TypeKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_tuple("TypeKey").field(&&*self.canon).finish() - } -} - -/// Structured failure of [`TypeKey::parse`]: the offending input plus the -/// underlying `syn` parse error. -#[derive(Debug)] -pub struct TypeKeyParseError { - pub input: String, - pub error: syn::Error, -} - -impl fmt::Display for TypeKeyParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "invalid type `{}`: {}", self.input, self.error) - } -} - -impl std::error::Error for TypeKeyParseError {} - -impl TypeKey { - /// Build a key by parsing the input as a type and normalizing. - pub fn parse(s: &str) -> Result { - let ty: syn::Type = syn::parse_str(s).map_err(|error| TypeKeyParseError { - input: s.to_string(), - error, - })?; - Ok(Self::from_type(&ty)) - } - - /// Build a key directly from a `syn::Type` (normalizing a clone; the - /// input is not modified). - pub fn from_type(ty: &syn::Type) -> Self { - // Off the shared reduction, so this key and the model's type index - // cannot drift apart about what a type is called. - let t = crate::api::core::types_util::canonical_type(ty); - Self { - canon: t.to_token_stream().to_string().into(), - ty: std::rc::Rc::new(t), - } - } - - /// Build a key for a bare item ident — infallible by construction (an - /// ident IS a single-segment path type; nothing to parse or normalize). - pub fn from_ident(ident: &syn::Ident) -> Self { - Self::from_type(&crate::api::core::types_util::type_from_ident(ident)) - } - - /// The canonical string form. - pub fn as_str(&self) -> &str { - &self.canon - } - - /// The normalized parsed form. Infallible — a clone of the stored type, - /// never a reparse. - pub fn to_type(&self) -> syn::Type { - (*self.ty).clone() - } -} - -impl fmt::Display for TypeKey { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.canon) - } -} - -/// What a type-table key names. -/// -/// Two populations, and saying which is which is what keeps one origin per cell: -/// a type the flat API contains **is** a [`TypeRef`](crate::api::core::flat::TypeRef), reused whole, so its -/// classification and its source location are already there. -#[derive(Clone, Debug)] -pub enum TypeSubject { - /// A type the flat API contains — the frontend's own reading, unmodified. - Source(crate::api::core::flat::TypeRef), - /// A type only the binding authored: a declared wire type with no - /// `#[prebindgen]` item behind it, an [`unfold`](crate::api::core::unfold) - /// leaf. It has no reading and no source location — a fact about it, rather - /// than information that went missing. - Adapter(syn::Type), -} - -impl TypeSubject { - /// Where the source wrote this type, or `None` when no source did. - pub fn location(&self) -> Option<&SourceLocation> { - match self { - // Having a reading and having a reportable position are different - // facts: a binding-local fn's types are lowered — so they have - // readings — against no file at all. Reporting `:0:0` would invent a - // position; `None` says what is true. - TypeSubject::Source(t) => Some(&*t.origin.location).filter(|l| l.has_position()), - TypeSubject::Adapter(_) => None, - } - } - - /// The frontend's classification, or `None` for an adapter-authored type. - pub fn kind(&self) -> Option<&crate::api::core::flat::TypeKind> { - match self { - TypeSubject::Source(t) => Some(&t.kind), - TypeSubject::Adapter(_) => None, - } - } - - /// The type as Rust must spell it, either way. - pub fn syntax(&self) -> &syn::Type { - match self { - TypeSubject::Source(t) => &t.origin.syntax, - TypeSubject::Adapter(ty) => ty, - } - } -} - -/// One type-table cell: what the key names, and the adapter's answer for it. -pub struct TypeCell { - /// The type itself, as the frontend reads it when it can. - pub subject: TypeSubject, - /// The binding asks for this cell **directly** — a declared fn's signature, a - /// declared type, an `unfold` leaf — as opposed to reaching it through some - /// converter's [`TypeEntry::subs`]. - /// - /// A scan fact. Whether a converter is *needed* here is reachability from - /// these roots, which [`crate::api::core::resolve`] derives rather than - /// stores: the scan deliberately over-approximates the table (every nested - /// position, every struct in both directions), so the roots are what say - /// which of it has to work. - pub root: bool, - /// The adapter's converter, once resolved. - pub entry: Option>, -} - -/// Per-cell registry entry. -#[derive(Clone)] -pub struct TypeEntry { - /// Wire/destination type — the form the value takes on the wire as - /// chosen by the adapter (e.g. an `i64` handle for a JNI adapter, or - /// a `*const T` raw pointer for a C adapter). Other converters that - /// ask "what's the wire form of this rust type?" read this. - pub destination: syn::Type, - /// Complete generated function for the **wire-facing** stage of the - /// converter (signature, body, attributes, lifetimes). The adapter - /// owns the shape. Callers compute this stage's name via - /// `function.sig.ident`. - pub function: syn::ItemFn, - /// **Rust-side** stages that compose with [`Self::function`] to form - /// the full chain — copied verbatim from the resolving - /// [`crate::api::core::prebindgen::ConverterImpl::pre_stages`]. See - /// that field's docs for the chain-order semantics. - pub pre_stages: Vec>, - /// Inner types whose function delegates to their converters. Empty for - /// terminal converters; populated by wrapper converters. Used by the - /// post-resolution propagation pass. - pub subs: Vec, - /// Wire bit-patterns this converter never produces / always rejects. - /// Wrappers (`Option<_>`, sum-typed enums) carve from this set for - /// their own discriminants. See [`Niches`] for the cascade model. - pub niches: Niches, - /// Adapter-specific extras carried in by the - /// [`crate::api::core::prebindgen::ConverterImpl`] that filled this - /// slot. Emitter code reads this directly — the registry is the - /// single source of truth for cross-language facts (C header names, - /// JVM class names, etc.). Defaults to `()` for adapters that don't - /// need any. - pub metadata: M, -} - -impl TypeEntry { - /// Identifier of the wire-facing converter function. - pub fn converter_ident(&self) -> &syn::Ident { - &self.function.sig.ident - } - - /// Wire/destination type carried by this converter on success. - pub fn wire_type(&self) -> &syn::Type { - &self.destination - } - - /// Rust-side stages in input execution order, after the wire-facing - /// converter has decoded the wire value. - pub fn input_stage_order(&self) -> impl Iterator)> { - self.pre_stages.iter().enumerate().rev() - } - - /// Rust-side stages in output execution order, before the wire-facing - /// converter encodes the final wire value. - pub fn output_stage_order(&self) -> impl Iterator)> { - self.pre_stages.iter().enumerate() - } - - /// Immediate converter dependencies recorded by the adapter when this entry - /// resolved. - pub fn dependency_keys(&self) -> &[TypeKey] { - &self.subs - } -} - -/// Direction of a converter pair. -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] -pub enum Direction { - /// Wire → Rust. - Input, - /// Rust → Wire. - Output, -} - -impl Direction { - pub fn flip(self) -> Self { - match self { - Direction::Input => Direction::Output, - Direction::Output => Direction::Input, - } - } -} - -/// Single owner of everything parsed from the prebindgen source stream. -/// -/// The metadata parameter `M` is the language adapter's per-converter -/// extra type, supplied via -/// [`crate::api::core::prebindgen::Prebindgen::Metadata`]. Each -/// [`TypeEntry`] carries one `M` copied in by the resolver from the -/// [`crate::api::core::prebindgen::ConverterImpl`] that produced it. -/// Adapters that don't carry extras leave `M = ()`. -pub struct Registry { - /// The parsed model these maps project. Held rather than discarded, so a - /// later stage can ask it what a name means through the registry it already - /// has — see [`Self::flat`]. - flat: crate::api::core::flat::Flat, - /// Type tables, one per direction. Each scanned type gets a [`TypeCell`] - /// holding what the key names, whether the binding asks for it directly, and - /// the resolved [`TypeEntry`] once the structural resolver fills it. - pub input_types: HashMap>, - pub output_types: HashMap>, - - /// Resolved constructor-expansion plans, keyed by `(function, parameter)`. - /// Filled by [`crate::api::core::expand::apply`] before resolution; read - /// by language adapters at the parameter-emission site. Empty unless the - /// adapter declared expansions. - pub expansion_plans: HashMap<(syn::Ident, syn::Ident), crate::api::core::expand::FoldPlan>, - - /// Resolved output-expansion plans, keyed by function ident. Filled by - /// [`crate::api::core::unfold::apply`] before resolution; read by language - /// adapters at the return-emission site. Empty unless the adapter declared - /// deconstructors. - pub unfold_plans: HashMap, - - /// Resolved **error**-position expansion plans, keyed by function ident: the - /// decomposition of a fallible fn's `Result<_, E>` domain error `E` (from - /// `.convert_error` / `.deconstruct_error`). Separate from - /// [`Self::unfold_plans`] — a fn may have both an output and an error plan. - pub error_plans: HashMap, - - /// Default decomposition of a **callback argument** type — the `T` of a - /// declared fn's `impl Fn(T, …)` parameter — keyed by the bare arg type - /// (type-level, fn-independent). Filled by - /// [`crate::api::core::unfold::apply`] from the type's default - /// deconstructor (`by_ref = false`: the trampoline owns the value); read by - /// language adapters when emitting the callback trampoline. A type without - /// a default deconstructor has no entry and is delivered whole. - pub callback_arg_plans: HashMap, - - /// The declaration-default decomposition per deconstructor declaration - /// ([`crate::api::core::unfold::DeconId`]) — resolved once with - /// normalized inputs, independent of using functions and processing - /// order. The single source language adapters derive declaration-keyed - /// signature artifacts (e.g. generated callback interfaces) from, so - /// every function selecting the same declaration sees one signature by - /// construction. - pub decon_plans: - HashMap, -} - -impl Registry { - /// An empty registry: no model, no items, no types. - /// - /// **Not public.** A `Registry` is a projection of a [`Flat`], and one built - /// this way projects nothing — [`Self::flat`] would hand a later stage an - /// empty model that claims to be this registry's source. Outside this crate - /// the entry points are [`Self::from_items`], [`Self::from_flat`] and - /// [`Self::builder`], each of which has a model behind it. - pub(crate) fn empty() -> Self { - Self { - flat: crate::api::core::flat::Flat::default(), - input_types: Default::default(), - output_types: Default::default(), - expansion_plans: HashMap::new(), - unfold_plans: HashMap::new(), - error_plans: HashMap::new(), - callback_arg_plans: HashMap::new(), - decon_plans: HashMap::new(), - } - } -} - -impl From for ScanError { - fn from(e: crate::api::core::flat::ParseError) -> Self { - match e { - crate::api::core::flat::ParseError::DuplicateName(d) => { - ScanError::DuplicateName(Box::new(DuplicateNameError { - name: d.name, - first: d.first, - second: d.second, - first_crate: d.first_crate, - second_crate: d.second_crate, - })) - } - } - } -} - -/// One item of a [`ScanError::NotExpressible`] report. -#[derive(Debug)] -pub struct NotExpressibleEntry { - /// The item's name, or `None` for an item kind that has none. - pub name: Option, - /// Rendered [`ItemError`](crate::core::flat::ItemError) — the frontend's own - /// message, so one authority produces it. - pub reason: String, - pub location: SourceLocation, -} - -/// Payload of [`ScanError::DuplicateName`], boxed to keep the error enum -/// small (`clippy::result_large_err`). -#[derive(Debug)] -pub struct DuplicateNameError { - pub name: syn::Ident, - pub first: SourceLocation, - pub second: SourceLocation, - /// Origin crates of the colliding items, when known (multi-source - /// ingestion via [`Registry::from_items`]) — the `SourceLocation` - /// file paths are crate-relative, so with several sources they alone - /// may not identify the colliding crates. - pub first_crate: Option, - pub second_crate: Option, -} - -/// Errors surfaced by the scan phase. -#[derive(Debug)] -pub enum ScanError { - DuplicateName(Box), - ConflictingFunctionIntent { - name: syn::Ident, - }, - ConflictingTypeIntent { - key: TypeKey, - }, - /// Items the flat language cannot express, all of them at once. - /// - /// The message for each comes from - /// [`ItemError`](crate::core::flat::ItemError), so one authority produces it. - /// This replaces the per-item guards the registry used to duplicate — a `self` - /// receiver, a non-ident parameter pattern, a disallowed `impl Trait` — which - /// the frontend now catches with a richer diagnosis (it names the parameter). - NotExpressible { - entries: Vec, - }, - /// An adapter-invariant check failed — see [`Prebindgen::validate`]. - /// The message is adapter-authored and printed verbatim. - AdapterInvariant { - message: String, - }, - /// Explicitly declared items (functions, helper functions, constants) - /// that match no indexed `#[prebindgen]` item. A declaration is a - /// statement of intent — its target being absent is always a bug (a - /// typo in build.rs, or the item was renamed/removed in the source - /// crate), so this is a hard error, unlike the soft warnings for stale - /// *ignore* entries. All missing names are collected before failing. - DeclaredNotFound { - entries: Vec<(&'static str, String)>, - }, - /// Declared type keys that qualify a source item with its crate path - /// (`ptr_class!(myflat::Foo)` where `myflat` is a chained source crate). - /// Source items live in one flat namespace and are keyed by their bare - /// name — the qualified spelling can never match a captured signature, - /// so it is a hard error with a fix-it instead of a silent miss (issue - /// #95). All offenders are collected before failing. - QualifiedDeclaredTypes { - /// `(qualified spelling, bare fix-it name)` pairs. - entries: Vec<(String, String)>, - }, -} - -impl fmt::Display for ScanError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ScanError::DuplicateName(e) => { - let in_crate = |c: &Option| match c { - Some(c) => format!(" in crate `{c}`"), - None => String::new(), - }; - write!( - f, - "duplicate prebindgen name `{}`: first{} at {}, second{} at {} — prebindgen \ - items live in one flat namespace across all sources; rename one of them", - e.name, - in_crate(&e.first_crate), - e.first, - in_crate(&e.second_crate), - e.second - ) - } - ScanError::ConflictingFunctionIntent { name } => { - write!(f, "function `{}` cannot be both declared and ignored", name) - } - ScanError::ConflictingTypeIntent { key } => { - write!(f, "type `{}` cannot be both declared and ignored", key) - } - ScanError::NotExpressible { entries } => { - write!( - f, - "{} `#[prebindgen]` item(s) the flat language cannot express:", - entries.len() - )?; - for e in entries { - // The crate, because a captured path is crate-relative: with - // several sources, two offenders both read `src/lib.rs:..` - // and the location alone says nothing about which one to fix. - // Same reason the duplicate-name diagnostic carries it. - let in_crate = match &e.location.crate_name { - Some(c) => format!(" in crate `{c}`"), - None => String::new(), - }; - match &e.name { - Some(name) => { - write!(f, "\n {}{in_crate}: {name} {}", e.location, e.reason)? - } - None => write!(f, "\n {}{in_crate}: {}", e.location, e.reason)?, - } - } - Ok(()) - } - ScanError::AdapterInvariant { message } => write!(f, "{}", message), - ScanError::DeclaredNotFound { entries } => { - writeln!( - f, - "{} declared item(s) not found among #[prebindgen] items:", - entries.len() - )?; - for (kind, name) in entries { - writeln!(f, " - {kind} `{name}`")?; - } - write!( - f, - "a declaration names an item that does not exist — typo in build.rs, \ - or renamed/removed in the source crate?" - ) - } - ScanError::QualifiedDeclaredTypes { entries } => { - writeln!( - f, - "{} declared type(s) qualify a source item with its crate path:", - entries.len() - )?; - for (spelled, bare) in entries { - writeln!(f, " - `{spelled}` — declare it as `{bare}`")?; - } - write!( - f, - "source items live in one flat namespace keyed by their bare name; \ - a crate-qualified spelling never matches captured signatures" - ) - } - } - } -} - -impl std::error::Error for ScanError {} - -/// Combined error surfaced by [`Registry::resolve`] / [`Generation::write_rust`]. -#[derive(Debug)] -pub enum WriteRustError { - Scan(ScanError), - Expand(crate::api::core::expand::ExpandError), - Unfold(crate::api::core::unfold::UnfoldError), - Resolve(crate::api::core::resolve::ResolveError), - Write(crate::api::core::write::WriteError), -} - -impl fmt::Display for WriteRustError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - WriteRustError::Scan(e) => write!(f, "{}", e), - WriteRustError::Expand(e) => write!(f, "{}", e), - WriteRustError::Unfold(e) => write!(f, "{}", e), - WriteRustError::Resolve(e) => write!(f, "{}", e), - WriteRustError::Write(e) => write!(f, "{}", e), - } - } -} - -impl std::error::Error for WriteRustError {} - -impl From for WriteRustError { - fn from(e: ScanError) -> Self { - WriteRustError::Scan(e) - } -} - -impl From for WriteRustError { - fn from(e: crate::api::core::expand::ExpandError) -> Self { - WriteRustError::Expand(e) - } -} - -impl From for WriteRustError { - fn from(e: crate::api::core::unfold::UnfoldError) -> Self { - WriteRustError::Unfold(e) - } -} - -impl From for WriteRustError { - fn from(e: crate::api::core::resolve::ResolveError) -> Self { - WriteRustError::Resolve(e) - } -} - -impl From for WriteRustError { - fn from(e: crate::api::core::write::WriteError) -> Self { - WriteRustError::Write(e) - } -} - -/// Adapter declaration intent normalized once per pipeline run. -struct DeclaredItems { - functions: HashSet, - ignored_functions: HashSet, - /// Bulk-ignore predicates over item names — every matching *undeclared* - /// item (fn, struct/enum, const) is an acknowledged skip (no warning). - /// Kind-agnostic: prebindgen names live in one flat namespace. A - /// declared item matching a predicate is unaffected: declaration wins. - /// See [`Prebindgen::ignored_name_predicates`]. - ignored_name_predicates: Vec, - /// Signature-scanned but not emitted — see [`Prebindgen::helper_functions`]. - helper_functions: HashSet, - accessors: HashSet, - method_receivers: HashMap, - types: HashSet, - ignored_types: HashSet, - /// Types converted exclusively through the adapter's plans (built from - /// ingredients / decomposed into fields); acknowledged for warning - /// purposes and un-required after plans — see - /// [`Prebindgen::boundary_only_types`]. - boundary_only_types: HashSet, - /// `None` = the adapter has no const declaration mechanism (all consts - /// re-emitted verbatim, no scan, no warnings) — see - /// [`Prebindgen::declared_consts`]. - consts: Option>, - ignored_consts: HashSet, - /// Adapter-required extra output types (no `#[prebindgen]` item to - /// scan — e.g. expression-constant value types); see - /// [`Prebindgen::required_output_types`]. - required_output_types: Vec, -} - -impl DeclaredItems { - fn from_adapter(adapter: &E) -> Result - where - E: Prebindgen, - { - let declared = Self { - functions: adapter.declared_functions(), - ignored_functions: adapter.ignored_functions(), - ignored_name_predicates: adapter.ignored_name_predicates(), - helper_functions: adapter.helper_functions(), - accessors: adapter.accessor_functions(), - method_receivers: adapter.method_receivers(), - types: adapter.declared_types(), - ignored_types: adapter.ignored_types(), - boundary_only_types: adapter.boundary_only_types(), - consts: adapter.declared_consts(), - ignored_consts: adapter.ignored_consts(), - required_output_types: adapter.required_output_types(), - }; - - if let Some(name) = declared - .functions - .intersection(&declared.ignored_functions) - .cloned() - .min_by_key(|ident| ident.to_string()) - { - return Err(ScanError::ConflictingFunctionIntent { name }); - } - if let Some(key) = declared - .types - .intersection(&declared.ignored_types) - .cloned() - .min_by_key(|key| key.as_str().to_owned()) - { - return Err(ScanError::ConflictingTypeIntent { key }); - } - - Ok(declared) - } -} - -/// Collects what a [`Registry`] will index, then hands over the registry. -/// -/// The same shape [`Flat`](crate::core::flat) reads prebindgen data with — name a -/// directory, or feed a stream, then `build()` — so there is one way to say where -/// captured items come from, whoever consumes them. -/// -/// Feeders accumulate, so mix them freely. -pub struct RegistryBuilder { - items: Vec<(syn::Item, SourceLocation)>, - /// `M` is fixed by the adapter a caller eventually `resolve`s with, and is - /// carried through so `Registry::builder()` needs no turbofish: it threads - /// from here to [`Self::build`] and is inferred at the `resolve` call. - _metadata: PhantomData, -} - -impl RegistryBuilder { - /// Every `#[prebindgen]` item captured in `dir` — pass - /// `::PREBINDGEN_OUT_DIR`. - /// - /// Panics the way [`Source::new`](crate::Source::new) does if `dir` is not - /// readable prebindgen output: a build script has nothing to recover with. - pub fn source>(self, dir: P) -> Self { - let source = crate::Source::new(dir); - self.items(source.items_all()) - } - - /// The same, for a dependency this crate **renames** in `Cargo.toml`. - /// - /// The origin recorded at capture time is the dependency's real package name, - /// which will not resolve from a crate that refers to it by another name. - /// `crate_name` is the name *this* crate uses. - /// - /// Per directory, deliberately: a registry-level override could only fix one - /// module, and a registry may layer several sources. - pub fn source_named>( - self, - dir: P, - crate_name: impl Into, - ) -> Self { - let source = crate::Source::builder(dir).crate_name(crate_name).build(); - self.items(source.items_all()) - } - - /// Add a captured item stream — a group selection, an otherwise-configured - /// [`Source`](crate::Source), or synthetic items in a test. - pub fn items(mut self, items: I) -> Self - where - I: IntoIterator, - { - self.items.extend(items); - self - } - - /// Index everything collected so far. - /// - /// Sugar over [`Registry::from_items`], which stays the primitive: one place - /// decides what a stream of items means. - pub fn build(self) -> Result, ScanError> { - Registry::from_items(self.items) - } -} - -impl Registry { - /// Start collecting what to index. - /// - /// The way a build script reads prebindgen output: name the directory and get - /// a registry, with no [`Source`](crate::Source) in between. - /// - /// ``` - /// # prebindgen::Source::init_doctest_simulate(); - /// use prebindgen::core::Registry; - /// - /// // Annotated only because nothing here resolves: in a build script `M` is - /// // fixed by the adapter passed to `resolve`, so no call site names it. - /// let registry: Registry<()> = Registry::builder().source("source_ffi").build()?; - /// assert!(registry.flat().function("test_function").is_some()); - /// # Ok::<_, prebindgen::core::ScanError>(()) - /// ``` - /// - /// Several directories compose, including one this crate renames: - /// - /// ```ignore - /// let registry = Registry::builder() - /// .source(flat_crate::PREBINDGEN_OUT_DIR) - /// .source_named(helpers::PREBINDGEN_OUT_DIR, "helpers") - /// .build()?; - /// ``` - pub fn builder() -> RegistryBuilder { - RegistryBuilder { - items: Vec::new(), - _metadata: PhantomData, - } - } - - /// Construct a `Registry` by indexing a stream of source items. - /// - /// The primitive: [`Self::builder`] is sugar over this, and is what a build - /// script reading a directory should reach for. Callers feed any - /// `(syn::Item, SourceLocation)` iterator — typically `source.items_all()`, - /// `source.items_except_groups(...)`, or a hand-rolled filter chain — so - /// item-level selection happens upstream of the registry rather than inside - /// it. Streams from several sources combine with plain iterator composition: - /// - /// ```ignore - /// let registry = Registry::from_items( - /// flat.items_all().chain(helpers.items_all()), - /// )?; - /// ``` - /// - /// Each item's **origin crate** rides its [`SourceLocation`] (stamped - /// by [`Source`](crate::Source) when parsing records): named items get - /// their origin recorded for qualified references in generated code - /// (`flat_crate::…` vs `helper_crate::…`), and the first origin seen - /// becomes the default module ([`Self::default_module`]). When a - /// dependency is renamed in Cargo.toml, override the stamp at the - /// source: `Source::builder(dir).crate_name("myflat")` — being - /// per-source, it composes across chained streams (a registry-level - /// override could only fix one module). - /// - /// This step only populates the item maps (`functions`, `structs`, - /// `enums`, `consts`, `guards`). Signature/body scanning that - /// drives type-resolution requirements happens later, in - /// [`Self::scan_declared`], and is gated on what the language adapter - /// has explicitly declared. An **API** item that is never declared remains - /// in the registry but never drives type resolution and never emits. - /// - /// `guards` is the exception, and it is not one of the API maps: an - /// anonymous const has no name to declare, so it is outside the gate - /// entirely and always emits. - pub fn from_items(items: I) -> Result - where - I: IntoIterator, - { - let flat = crate::api::core::flat::Flat::builder() - .items(items) - .build()?; - Self::from_flat(flat) - } - - /// Index a parsed [`Flat`](crate::api::core::flat::Flat) model. - /// - /// The registry is a **projection** of the model, not a second reading of the - /// source: `Flat` decided what every item means, and this arranges those - /// decisions into the maps adapters read. The model itself is kept - /// ([`Self::flat`]) so later stages can ask it questions rather than - /// re-deriving them. - /// - /// **Fails on anything the language cannot express** — a `self` receiver, an - /// `async fn`, a generic binder, a type form outside the grammar, or a - /// reference to a type the flat API does not declare. All of them at once, so - /// a source crate that needs migrating sees one list instead of one rebuild - /// per item. - pub fn from_flat(flat: crate::api::core::flat::Flat) -> Result { - let entries: Vec = flat - .unsupported() - .map(|u| NotExpressibleEntry { - name: u.name.clone(), - reason: u.error.to_string(), - location: (*u.origin.location).clone(), - }) - .collect(); - if !entries.is_empty() { - return Err(ScanError::NotExpressible { entries }); - } - - let mut registry = Registry::empty(); - registry.flat = flat; - Ok(registry) - } - - /// The parsed model this registry projects. - pub fn flat(&self) -> &crate::api::core::flat::Flat { - &self.flat - } - - /// Every **named** item the model holds — functions, structs, either enum - /// shape, consts — regardless of whether the stream carried an origin stamp. - /// - /// Lives here so an adapter that needs "anything the source crate defines" - /// does not enumerate element kinds itself: a new kind is taught here once - /// instead of drifting in each adapter. An **alias is deliberately absent** - /// — see the arm below — and callers are expected to pair this with - /// `origin_module(..).unwrap_or_else(default_module)`. - pub fn named_item_idents(&self) -> impl Iterator { - use crate::api::core::flat::{Element, Type}; - self.flat.elements().filter_map(|e| match e { - // An `Extern` names a type without declaring a body, and is - // deliberately absent: its caller decides which names to qualify in - // generated Rust, and qualifying an alias would move that output. - Element::Type(Type::Extern(_)) => None, - Element::Function(_) | Element::Type(_) | Element::Constant(_) => e.name(), - Element::Guard(_) | Element::Unsupported(_) => None, - }) - } - - /// Every **struct or enum** name — either enum shape, never an alias. - /// - /// Named for its population rather than as the iterator form of - /// [`Self::declares_type`], which it is **not**: that predicate counts every - /// declared type, aliases included. This one feeds *"skipping undeclared - /// `#[prebindgen]` struct/enum"*, which names a kind an alias is not — so the - /// two answer differently on purpose, and the names now say so. - fn struct_enum_idents(&self) -> impl Iterator { - use crate::api::core::flat::Type; - self.flat.types().filter_map(|t| match t { - Type::Struct(_) | Type::Variant(_) | Type::Enum(_) => Some(t.name()), - Type::Extern(_) => None, - }) - } - - /// Whether the source declares a type under this name — **including an - /// alias**. - /// - /// The question both type-diagnostic sites ask, shared so they cannot drift. - /// An alias counts because `#[prebindgen] pub type Handle = ..` *is* a - /// declaration of that name: it can be declared bare by an adapter (landing - /// in the no-indexed-body branch above, which is what - /// `ptr_class(ZKeyExpr<'static>)` relies on), so a diagnostic that says - /// "no such captured item" would be false. - /// - /// Distinct from [`Self::struct_enum_idents`], which excludes aliases - /// because it feeds a *"skipping undeclared struct/enum"* warning — a - /// different question, about what an adapter left unclaimed. - fn declares_type(&self, ident: &syn::Ident) -> bool { - self.flat.declared_type(ident).is_some() - } - - /// The origin crate's **module path** for an item, read off the element's - /// own [`SourceLocation`] stamp, or `None` when unknown — callers then fall - /// back to [`Self::default_module`]. - pub fn origin_module(&self, ident: &syn::Ident) -> Option { - // Off the element's own location, which covers both populations: a - // captured item stamped at capture time, and a binding-local fn stamped - // by `add_local_function`. - let crate_name = self.flat.element(&ident)?.location().crate_name.as_ref()?; - let module = crate_name.replace('-', "_"); - syn::parse_str(&module).ok() - } - - /// The default module for references with no recorded origin: the - /// first-seen item origin. `None` for an origin-less item-level - /// registry (adapters then fall back to `crate`). To change a module - /// name, override it at the source — a stream's origin stamps - /// (`Source::builder(dir).crate_name("myflat")`) — never here: a - /// registry-level override could only fix ONE module, which is - /// incomplete with chained multi-source streams. - pub fn default_module(&self) -> Option { - self.flat - .source_modules() - .first() - .and_then(|m| syn::parse_str(m).ok()) - } - - /// Module paths of every ingested source, ingestion order — e.g. for a - /// glob import that must see all sources' items. - pub fn all_source_modules(&self) -> Vec { - self.flat - .source_modules() - .iter() - .filter_map(|m| syn::parse_str(m).ok()) - .collect() - } - - /// Scan the signature/body of every item declared by the adapter. - /// - /// * For each ident in `adapter.declared_functions()` ∩ indexed functions, - /// call `scan_fn_signature` so parameter and return types - /// are registered as required. - /// * For each `TypeKey` in `adapter.declared_types()`, mark the key as - /// required in both directions; if the key resolves to an indexed - /// struct/enum, also scan its body so field types are registered - /// (still `required: false` — propagation later promotes them - /// through `subs`). - /// * Idents / types returned by `adapter.ignored_functions()` / - /// `adapter.ignored_types()` are treated as intentional skips: they are - /// neither scanned nor emitted, but they do suppress the "skipping - /// undeclared" warnings. - /// - /// Declared items that don't match any indexed body get a build - /// warning (likely a typo in the build script). Indexed items that - /// were neither declared nor ignored also get a `cargo:warning=` skip - /// line so the user sees the remaining unexpected skips per build. - pub fn scan_declared(&mut self, ext: &E) -> Result<(), ScanError> - where - E: Prebindgen, - { - let declared = DeclaredItems::from_adapter(ext)?; - self.scan_declared_items(&declared) - } - - fn scan_declared_items(&mut self, declared: &DeclaredItems) -> Result<(), ScanError> { - // Source-qualified declared types are a hard error (issue #95). The - // key's own normalization already reduced `crate::`/`self::` and std - // prelude spellings, so a remaining multi-segment declared path - // either qualifies a SOURCE item with its crate name (can never - // match — the flat namespace keys are bare) or names a genuinely - // foreign type (supported verbatim; warned about below only when it - // shadows a captured item's name — the likely-mistake heuristic). - let mut qualified: Vec<(String, String)> = Vec::new(); - let mut probed: HashSet<&TypeKey> = HashSet::new(); - for key in declared - .types - .iter() - .chain(declared.ignored_types.iter()) - .chain(declared.boundary_only_types.iter()) - { - if !probed.insert(key) { - continue; - } - let ty = key.to_type(); - // Peel one reference level; the qualified head only appears on - // path types. - let inner = match &ty { - syn::Type::Reference(r) => &*r.elem, - other => other, - }; - let syn::Type::Path(tp) = inner else { continue }; - if tp.qself.is_some() || tp.path.segments.len() < 2 { - continue; - } - let head = tp - .path - .segments - .first() - .expect("len checked") - .ident - .to_string(); - let last = tp.path.segments.last().expect("len checked"); - if self.flat.source_modules().contains(&head) { - qualified.push((key.to_string(), last.to_token_stream().to_string())); - } else if self.declares_type(&last.ident) { - println!( - "cargo:warning=prebindgen: declared type `{}` is path-qualified, but a \ - captured #[prebindgen] item `{}` exists — if you meant the source item, \ - declare it by its bare name", - key, last.ident - ); - } - } - if !qualified.is_empty() { - qualified.sort(); - return Err(ScanError::QualifiedDeclaredTypes { entries: qualified }); - } - - // Declared-but-missing items are collected across all three loops and - // reported together as one hard error (see - // [`ScanError::DeclaredNotFound`]); stale *ignore* entries below only - // warn. - let mut missing: Vec<(&'static str, String)> = Vec::new(); - - // Scan declared functions. - for ident in &declared.functions { - if let Some(item_fn) = self.flat.function(&ident).map(|f| f.origin.syntax.clone()) { - self.scan_fn_signature(&item_fn)?; - } else { - missing.push(("function", ident.to_string())); - } - } - - for ident in &declared.ignored_functions { - if self.flat.function(&ident).is_none() { - println!( - "cargo:warning=prebindgen: ignored function `{}` not found among #[prebindgen] items", - ident - ); - } - } - - // Helper functions: never emitted, no blanket signature scan (the - // adapter registers the specific requirements via - // `extra_required_types`) — but they are referenced by name from - // adapter declarations, so a missing one is a hard error. - for ident in &declared.helper_functions { - if self.flat.function(&ident).is_none() { - missing.push(("helper function", ident.to_string())); - } - } - - // Scan declared consts (only when the adapter has a const - // declaration mechanism): a const is a nullary source of its type, - // so the type is required in the output direction only. - if let Some(decl_consts) = &declared.consts { - for ident in decl_consts { - if let Some(item_const) = - self.flat.constant(&ident).map(|c| c.origin.syntax.clone()) - { - self.ensure_entry(Direction::Output, &item_const.ty, true); - } else { - missing.push(("constant", ident.to_string())); - } - } - for ident in &declared.ignored_consts { - if self.flat.constant(&ident).is_none() { - println!( - "cargo:warning=prebindgen: ignored const `{}` not found among #[prebindgen] items", - ident - ); - } - } - } - - if !missing.is_empty() { - missing.sort(); - return Err(ScanError::DeclaredNotFound { entries: missing }); - } - - // Adapter-required extra output types — synthesized values with no - // `#[prebindgen]` item behind them (e.g. expression constants). - for ty in &declared.required_output_types { - self.ensure_entry(Direction::Output, ty, true); - } - - // Scan declared types. - for key in &declared.types { - let ty = key.to_type(); - let mut matched = false; - if let Some(ident) = bare_path_ident(&ty) { - if let Some(s) = self - .flat - .struct_type(&ident) - .map(|s| s.origin.syntax.clone()) - { - self.scan_struct(&s)?; - self.ensure_entry(Direction::Input, &ty, true); - self.ensure_entry(Direction::Output, &ty, true); - matched = true; - } else if let Some(e) = self.flat.enum_item(&ident).cloned() { - self.scan_enum(&e)?; - self.ensure_entry(Direction::Input, &ty, true); - self.ensure_entry(Direction::Output, &ty, true); - matched = true; - } - } - if !matched { - // Declared type without an indexed body (e.g. - // `ptr_class(ZKeyExpr<'static>)` on a re-exported - // foreign type). Still mark required so the resolver - // tries to produce a converter for it. - self.ensure_entry(Direction::Input, &ty, true); - self.ensure_entry(Direction::Output, &ty, true); - } - } - - for key in &declared.ignored_types { - let ty = key.to_type(); - let matched = bare_path_ident(&ty).is_some_and(|ident| self.declares_type(&ident)); - if !matched { - println!( - "cargo:warning=prebindgen: ignored type `{}` not found among #[prebindgen] items", - key.as_str() - ); - } - } - - // Warn about indexed items that the adapter never claimed. An - // ignore *predicate* acknowledges every matching item in bulk — - // kind-agnostic, since prebindgen names live in one flat namespace; - // a predicate matching nothing is silent by design (it is a filter, - // not a claim — match counts vary across feature configurations). - let pred_ignored = |name: &str| { - !declared.ignored_name_predicates.is_empty() - && declared.ignored_name_predicates.iter().any(|p| p(name)) - }; - let mut skipped_fns: Vec = self - .flat - .functions() - .map(|f| &f.name) - .filter(|k| { - !declared.functions.contains(*k) - && !declared.ignored_functions.contains(*k) - && !declared.helper_functions.contains(*k) - && !pred_ignored(&k.to_string()) - }) - .map(|k| k.to_string()) - .collect(); - skipped_fns.sort(); - for name in &skipped_fns { - println!( - "cargo:warning=prebindgen: skipping undeclared #[prebindgen] fn `{}`", - name - ); - } - - let mut skipped_types: Vec = Vec::new(); - let type_acknowledged = |key: &TypeKey| { - declared.types.contains(key) - || declared.ignored_types.contains(key) - || declared.boundary_only_types.contains(key) - }; - for ident in self.struct_enum_idents() { - let name = ident.to_string(); - let key = TypeKey::from_ident(ident); - if !type_acknowledged(&key) && !pred_ignored(&name) { - skipped_types.push(name); - } - } - skipped_types.sort(); - for name in &skipped_types { - println!( - "cargo:warning=prebindgen: skipping undeclared #[prebindgen] struct/enum `{}`", - name - ); - } - - if let Some(decl_consts) = &declared.consts { - let mut skipped_consts: Vec = self - .flat - .constants() - .map(|c| &c.name) - .filter(|k| { - !decl_consts.contains(*k) - && !declared.ignored_consts.contains(*k) - && !pred_ignored(&k.to_string()) - }) - .map(|k| k.to_string()) - .collect(); - skipped_consts.sort(); - for name in &skipped_consts { - println!( - "cargo:warning=prebindgen: skipping undeclared #[prebindgen] const `{}`", - name - ); - } - } - - Ok(()) - } - - /// Direction-indexed read access to the type-resolution tables. - pub(crate) fn type_table(&self, dir: Direction) -> &HashMap> { - match dir { - Direction::Input => &self.input_types, - Direction::Output => &self.output_types, - } - } - - /// Direction-indexed mutable access to the type-resolution tables. - pub(crate) fn type_table_mut(&mut self, dir: Direction) -> &mut HashMap> { - match dir { - Direction::Input => &mut self.input_types, - Direction::Output => &mut self.output_types, - } - } - - /// Look up the resolved input entry for `ty`, returning `None` if it - /// was never registered or is still unresolved. The returned entry's - /// `function.sig.ident` is the converter's call name; `destination` is - /// its wire form. - pub fn input_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { - let key = TypeKey::from_type(ty); - self.type_table(Direction::Input).get(&key)?.entry.as_ref() - } - - /// Look up the resolved output entry for `ty`. See [`Self::input_entry`]. - pub fn output_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { - let key = TypeKey::from_type(ty); - self.type_table(Direction::Output).get(&key)?.entry.as_ref() - } - - /// Register `ty` (and its nested positions) as a required **input** so - /// the resolver produces a converter for it. Used by - /// [`crate::api::core::expand`] to pull in the leaf types a fold needs. - pub(crate) fn require_input(&mut self, ty: &syn::Type) { - // Leaf/expansion types are concrete (no disallowed `impl Trait`), so - // the recursive registration cannot fail here. - let _ = self.register_type_recursive(Direction::Input, ty, true); - } - - /// Register `ty` (and its nested positions) as a required **output** so the - /// resolver produces a converter for it. The output-side peer of - /// [`Self::require_input`]; used by [`crate::api::core::unfold`] to pull in - /// the leaf types a decomposition delivers. - pub(crate) fn require_output(&mut self, ty: &syn::Type) { - let _ = self.register_type_recursive(Direction::Output, ty, true); - } - - /// Drop `ty` from the required-output scan set. The type's table entry is - /// left intact (so [`crate::api::core::resolve`]'s PASS A still resolves it - /// if it can, and emits it when resolved), but a `None` resolution no longer - /// counts as an unresolved-required error. Used by - /// [`crate::api::core::unfold::apply_leaf_vec_folds`]: when a `Vec` / - /// `Option>` return is delivered element-by-element through a fold, - /// the whole-collection converter is genuinely not needed — and for a - /// `Vec` it cannot resolve at all (a `jlong` wire is not - /// JObject-shaped), so requiring it would wrongly fail resolution. - pub(crate) fn unrequire_output(&mut self, ty: &syn::Type) { - self.clear_root(Direction::Output, ty); - } - - /// Drop `ty` from the required-input scan set — the input-side peer of - /// [`Self::unrequire_output`]. Used by [`Self::apply_adapter_plans`] for - /// the adapter's boundary-only types: a fold plan replaces every direct - /// crossing of the type with its ingredients, so the type's own input - /// converter is genuinely not needed (and for an undeclared type cannot - /// resolve at all). - pub(crate) fn unrequire_input(&mut self, ty: &syn::Type) { - self.clear_root(Direction::Input, ty); - } - - /// Stop treating `ty` as a root. The cell stays, so the resolver still fills - /// it if it can — only the demand that it *must* resolve is dropped. - fn clear_root(&mut self, dir: Direction, ty: &syn::Type) { - let key = TypeKey::from_type(ty); - if let Some(cell) = self.type_table_mut(dir).get_mut(&key) { - cell.root = false; - } - } - - fn scan_fn_signature(&mut self, f: &syn::ItemFn) -> Result<(), ScanError> { - // Mechanical: register every fn-signature type as the user wrote it. - // No semantic transformations (no &T→T strip, no ZResult→T strip, - // no skip for () / ZResult<()>). The adapter handles structural - // wrappers; propagation through `subs` then marks transitive deps - // (e.g. &Foo's `&_` converter returns subs=[Foo], so Foo becomes - // required). - // No receiver or non-ident pattern can reach here: a captured item was - // refused by the frontend and `from_flat` failed before indexing it, and - // a binding-local fn was checked against the same grammar - // (`Flat::lower_signature`) when `resolve` synthesized it. - for input in &f.sig.inputs { - match input { - syn::FnArg::Receiver(_) => continue, - syn::FnArg::Typed(pt) => { - self.register_type_recursive(Direction::Input, &pt.ty, true)?; - } - } - } - let ret_ty: syn::Type = match &f.sig.output { - syn::ReturnType::Default => syn::parse_quote!(()), - syn::ReturnType::Type(_, ty) => (**ty).clone(), - }; - self.register_type_recursive(Direction::Output, &ret_ty, true)?; - Ok(()) - } - - fn scan_struct(&mut self, s: &syn::ItemStruct) -> Result<(), ScanError> { - // The struct itself can appear in either direction. - let ty: syn::Type = crate::api::core::types_util::type_from_ident(&s.ident); - self.ensure_entry(Direction::Input, &ty, false); - self.ensure_entry(Direction::Output, &ty, false); - - if let syn::Fields::Named(named) = &s.fields { - for field in &named.named { - self.register_type_recursive(Direction::Input, &field.ty, false)?; - self.register_type_recursive(Direction::Output, &field.ty, false)?; - } - } - Ok(()) - } - - fn scan_enum(&mut self, e: &syn::ItemEnum) -> Result<(), ScanError> { - let ty: syn::Type = crate::api::core::types_util::type_from_ident(&e.ident); - self.ensure_entry(Direction::Input, &ty, false); - self.ensure_entry(Direction::Output, &ty, false); - - for variant in &e.variants { - for field in &variant.fields { - self.register_type_recursive(Direction::Input, &field.ty, false)?; - self.register_type_recursive(Direction::Output, &field.ty, false)?; - } - } - Ok(()) - } - - /// Register `ty` as a cell in the given direction, then recurse into every - /// nested position. `root` applies only to `ty` itself — a nested position is - /// never something the binding asked for directly. - fn register_type_recursive( - &mut self, - dir: Direction, - ty: &syn::Type, - root: bool, - ) -> Result<(), ScanError> { - let mut visited: HashSet = HashSet::new(); - self.register_type_inner(dir, ty, root, &mut visited) - } - - fn register_type_inner( - &mut self, - dir: Direction, - ty: &syn::Type, - is_top: bool, - visited: &mut HashSet, - ) -> Result<(), ScanError> { - // A disallowed `impl Trait` cannot reach here: every fn whose signature - // reaches this point passed the frontend's grammar — captured items at - // ingestion, binding-local ones at synthesis — and it names the - // parameter the bad type sits on. - - let key = TypeKey::from_type(ty); - if !visited.insert(key.clone()) { - return Ok(()); // cycle guard - } - - self.ensure_entry(dir, ty, is_top); - - for (child_dir, sub) in self.immediate_edges(dir, ty) { - self.register_type_inner(child_dir, &sub, false, visited)?; - } - Ok(()) - } - - /// Create the cell for `ty` in `dir` if it has none, and mark it a root when - /// the binding asked for it directly. - /// - /// The one place a cell is born, which is what lets the subject be decided - /// once: the model's reading if the flat API mentions this type, an - /// adapter-authored type otherwise. - fn ensure_entry(&mut self, dir: Direction, ty: &syn::Type, root: bool) { - let key = TypeKey::from_type(ty); - let subject = match self.flat.type_ref(ty) { - Some(t) => TypeSubject::Source(t.clone()), - None => TypeSubject::Adapter(key.to_type()), - }; - let cell = self - .type_table_mut(dir) - .entry(key) - .or_insert_with(|| TypeCell { - subject, - root: false, - entry: None, - }); - cell.root |= root; - } - - /// Enumerate the immediate type-graph edges out of `(dir, ty)`: - /// generic args / Fn args / tuple elements / ref/array/slice/ptr targets, - /// plus — if `ty` is the bare ident of an indexed struct or enum — the - /// field types of that struct/enum. - /// - /// `impl Fn(args)` arg types flow with `dir.flip()`; everything else - /// inherits `dir`. Used by both `register_type_inner` (during scan) and - /// the unresolved-descendants BFS in `resolve` (for diagnostics). - pub(crate) fn immediate_edges( - &self, - dir: Direction, - ty: &syn::Type, - ) -> Vec<(Direction, syn::Type)> { - let mut out: Vec<(Direction, syn::Type)> = Vec::new(); - let (positions, child_dir) = if let Some(args) = extract_fn_trait_args(ty) { - (args, dir.flip()) - } else { - (immediate_subtype_positions(ty), dir) - }; - for sub in positions { - out.push((child_dir, sub)); - } - // A declared type's own fields, read off the element rather than off its - // `syn::Fields`: a positional field is an ordinary `Field` there, so the - // named-only asymmetry the syntax walk had does not arise. An `Enum` has - // no fields and an `Extern` declares none, which is what makes both - // contribute nothing here. - if let Some(name) = bare_path_ident(ty) { - use crate::api::core::flat::{Field, Type}; - let fields: Vec<&Field> = match self.flat.declared_type(&name) { - Some(Type::Struct(s)) => s.fields.iter().collect(), - Some(Type::Variant(v)) => v - .alternatives - .iter() - .flat_map(|a| a.fields.iter()) - .collect(), - Some(Type::Enum(_) | Type::Extern(_)) | None => Vec::new(), - }; - for field in fields { - out.push((dir, field.ty.origin.syntax.clone())); - } - } - out - } - - /// Resolve the binding: scan the adapter's declarations, apply its - /// plans, and run type resolution — consuming both the registry and the - /// adapter into a [`Generation`], whose `write_*` methods are pure, - /// order-free emissions. This is the single public entry point for - /// language-specific binding generation; language-agnostic because - /// `adapter` is any [`crate::api::core::prebindgen::Prebindgen`] impl - /// whose `Metadata` matches this registry's `M` parameter. - /// - /// ```ignore - /// let gen = Registry::from_items(source.items_all())?.resolve(jni)?; - /// gen.write_rust(&rust_dest)?; - /// gen.write_kotlin(&kotlin_root)?; // JNI adapter's second artifact - /// ``` - pub fn resolve(mut self, adapter: E) -> Result, WriteRustError> - where - E: Prebindgen, - M: Clone + Default, - { - // Synthesis pre-pass: adapter-declared BINDING-LOCAL fns become - // ordinary registry entries (signature read from the synthesized - // item, calls qualified by the recorded origin), so every downstream - // stage treats them exactly like `#[prebindgen]` fns. - for (item_fn, origin) in adapter.local_functions() { - let ident = item_fn.sig.ident.clone(); - // The one input that does not come through `Flat`: a `sig!(..)` is - // written by hand in a build script, so the grammar has to be checked - // here or nowhere. Silently dropping a `self` receiver or a pattern - // parameter would surface as an arity mismatch out of rustc on - // generated code, which is the wrong end of the pipeline to learn - // about a build.rs typo. - let lowered = match self.flat.lower_signature(&item_fn) { - Ok(f) => f, - Err(error) => { - return Err(ScanError::AdapterInvariant { - message: format!("binding-local fn `{ident}`: {error}"), - } - .into()) - } - }; - if self.flat.element(&ident).is_some() { - return Err(ScanError::AdapterInvariant { - message: format!( - "binding-local fn `{ident}` collides with a `#[prebindgen]` item — \ - the generated call would resolve the wrong fn; rename the \ - binding-local fn" - ), - } - .into()); - } - // Into the model, so every downstream stage finds it exactly where it - // finds a captured fn — there is one index, and this is it. - self.flat.add_local_function(lowered, origin); - } - let declared = DeclaredItems::from_adapter(&adapter)?; - self.scan_declared_items(&declared)?; - adapter - .validate(&self) - .map_err(|message| ScanError::AdapterInvariant { message })?; - self.apply_adapter_plans(&adapter, &declared)?; - crate::api::core::resolve::resolve(&mut self, &adapter)?; - // Post-resolve validation runs ONCE here, so a `Generation` is valid - // by construction and the `write_*` emitters are genuinely pure - // (previously each writer re-ran this, validating twice per build). - // Sibling of the pre-resolve `validate` above — same adapter-invariant - // channel. An invalid binding fails `resolve`; no `Generation` is - // produced, so nothing can be written. - adapter - .validate_resolved(&self) - .map_err(|message| ScanError::AdapterInvariant { message })?; - Ok(Generation { - registry: self, - adapter, - }) - } - - fn apply_adapter_plans( - &mut self, - ext: &E, - declared: &DeclaredItems, - ) -> Result<(), WriteRustError> - where - E: Prebindgen, - { - // The set of declared fns drives `.default()` auto-apply: a defaulted - // constructor/deconstructor is synthesized for every matching declared - // fn. `accessors` is the `.fun_accessor` subset: excluded from - // constructor composition and the only fns a decomposer record may - // reference. - if let Some(exp) = ext.expansions() { - crate::api::core::expand::apply( - self, - &exp, - &declared.functions, - &declared.accessors, - &declared.method_receivers, - )?; - } - if let Some(dec) = ext.deconstructors(self) { - crate::api::core::unfold::apply(self, &dec, &declared.functions, &declared.accessors)?; - } - // Synthesized by-value `data_class` decompositions: build the leaves - // (immutable borrow), then wire them into fixed-builder plans. - let value_decons = ext.value_struct_decons(self); - if !value_decons.is_empty() { - crate::api::core::unfold::apply_value_structs(self, value_decons, &declared.functions)?; - } - // Synthesized sum decompositions: the same fixed-builder wiring for a - // value whose alternatives are chosen at runtime (tag + one leaf group - // per variant) rather than being a fixed product. - let sum_decons = ext.sum_decons(self); - if !sum_decons.is_empty() { - crate::api::core::unfold::apply_sum_returns(self, sum_decons, &declared.functions)?; - } - // Single-leaf `Vec`/`&[T]` whole-element folds — the dual of the - // `data_class` folds above, for String / scalar / handle elements - // (so the list is built on the foreign side, not via a Rust ArrayList). - let leaf_elements = ext.leaf_vec_fold_elements(self); - if !leaf_elements.is_empty() { - crate::api::core::unfold::apply_leaf_vec_folds( - self, - leaf_elements, - &declared.functions, - )?; - } - // Adapter-derived extra requirements (registry-aware — e.g. the - // other-side types of `convert!` conversion fns, per direction). - for (dir, ty) in ext.extra_required_types(self) { - match dir { - Direction::Input => self.require_input(&ty), - Direction::Output => self.require_output(&ty), - } - } - // Boundary-only types: every crossing is now covered by a plan (fold - // in, unfold out / error channel), so the scan-time direct converter - // requirement is stale — and typically unresolvable, since the type - // has no destination-language representation. Drop it both ways; the - // entry stays in the table, so a converter is still produced if one - // happens to resolve. - for key in &declared.boundary_only_types { - let ty = key.to_type(); - self.unrequire_input(&ty); - self.unrequire_output(&ty); - } - Ok(()) - } -} - -// ────────────────────────────────────────────────────────────────────── -// Helpers -// ────────────────────────────────────────────────────────────────────── - -/// Immediate child type positions of `ty` (one level deep). -pub fn immediate_subtype_positions(ty: &syn::Type) -> Vec { - match ty { - syn::Type::Path(p) => { - if let Some(last) = p.path.segments.last() { - if let syn::PathArguments::AngleBracketed(ab) = &last.arguments { - return ab - .args - .iter() - .filter_map(|a| { - if let syn::GenericArgument::Type(t) = a { - Some(t.clone()) - } else { - None - } - }) - .collect(); - } - } - vec![] - } - syn::Type::Reference(r) => vec![(*r.elem).clone()], - syn::Type::Tuple(t) => t.elems.iter().cloned().collect(), - syn::Type::Array(a) => vec![(*a.elem).clone()], - syn::Type::Slice(s) => vec![(*s.elem).clone()], - syn::Type::Ptr(p) => vec![(*p.elem).clone()], - syn::Type::Group(g) => immediate_subtype_positions(&g.elem), - syn::Type::Paren(p) => immediate_subtype_positions(&p.elem), - syn::Type::ImplTrait(_) => extract_fn_trait_args(ty).unwrap_or_default(), - _ => vec![], - } -} - -/// The callback grammar, which the source language owns — re-exported here for -/// the existing call sites until they consume elements (stages L2–L4 of #229). -pub use crate::api::core::flat::extract_fn_trait_args; - -/// A **resolved** binding generation: the [`Registry`] after -/// [`Registry::resolve`] ran the adapter's scan, plans, and type -/// resolution, bound together with the adapter that produced it. Both -/// halves of a generation run are methods here — [`Self::write_rust`] and -/// any adapter-specific artifact (e.g. `write_kotlin` for the JNI -/// adapter) — so the resolve-before-write ordering is enforced by -/// construction, and the writes themselves are pure reads that may run in -/// any order. -pub struct Generation { - registry: Registry, - adapter: E, -} - -// Opaque — exists so `Result::expect_err` works in tests. -impl fmt::Debug for Generation { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("Generation(..)") - } -} - -impl Generation { - /// Write the generated Rust bindings file. `out_path` may be relative - /// (resolved against `OUT_DIR`) or absolute; returns the path actually - /// written. Pure emission — the registry was fully resolved by - /// [`Registry::resolve`]. - pub fn write_rust( - &self, - out_path: impl AsRef, - ) -> Result { - Ok(crate::api::core::write::write_rust( - &self.registry, - &self.adapter, - out_path, - )?) - } - - /// The resolved registry (converter tables, plans, item maps). - pub fn registry(&self) -> &Registry { - &self.registry - } - - /// The adapter this generation was resolved with. - pub fn adapter(&self) -> &E { - &self.adapter - } -} - -#[cfg(test)] -mod tests; diff --git a/prebindgen/src/api/core/registry/cell.rs b/prebindgen/src/api/core/registry/cell.rs new file mode 100644 index 00000000..3de3d14d --- /dev/null +++ b/prebindgen/src/api/core/registry/cell.rs @@ -0,0 +1,162 @@ +//! What a crossing IS: the type it names, the conversion for it, and which way +//! it goes. + +use super::*; + +/// What a type-table key names. +/// +/// Two populations, and saying which is which is what keeps one origin per cell: +/// a type the flat API contains **is** a [`TypeRef`](crate::api::core::flat::TypeRef), reused whole, so its +/// classification and its source location are already there. +#[derive(Clone, Debug)] +pub(crate) enum TypeSubject { + /// A type the flat API contains — the frontend's own reading, unmodified. + Source(Box), + /// A type only the binding authored: a declared wire type with no + /// `#[prebindgen]` item behind it, an [`unfold`](crate::api::core::unfold) + /// leaf. It has no reading and no source location — a fact about it, rather + /// than information that went missing. + Adapter, +} + +impl TypeSubject { + /// Where the source wrote this type, or `None` when no source did. + pub fn location(&self) -> Option<&SourceLocation> { + match self { + // Having a reading and having a reportable position are different + // facts: a binding-local fn's types are lowered — so they have + // readings — against no file at all. Reporting `:0:0` would invent a + // position; `None` says what is true. + TypeSubject::Source(t) => Some(&*t.origin.location).filter(|l| l.has_position()), + TypeSubject::Adapter => None, + } + } + + /// The frontend's classification, or `None` for an adapter-authored type. + /// + /// Test-only: the cells carry it so a test can pin that a source reading + /// survives into the table, but no production path re-reads it. + #[cfg(test)] + pub fn kind(&self) -> Option<&crate::api::core::flat::TypeKind> { + match self { + TypeSubject::Source(t) => Some(&t.kind), + TypeSubject::Adapter => None, + } + } +} + +/// One type-table cell: what the key names, and the adapter's answer for it. +pub(crate) struct TypeCell { + /// The type itself, as the frontend reads it when it can. + pub subject: TypeSubject, + /// The binding asks for this cell **directly** — a declared fn's signature, a + /// declared type, an `unfold` leaf — as opposed to reaching it through some + /// converter's [`TypeEntry::subs`]. + /// + /// A scan fact. Whether a converter is *needed* here is reachability from + /// these roots, which [`crate::api::core::resolve`] derives rather than + /// stores: the scan deliberately over-approximates the table (every nested + /// position, every struct in both directions), so the roots are what say + /// which of it has to work. + pub root: bool, + /// The adapter's converter, once resolved. + pub entry: Option>, +} + +/// Per-cell registry entry. +#[derive(Clone)] +pub struct TypeEntry { + /// Wire/destination type — the form the value takes on the wire as + /// chosen by the adapter (e.g. an `i64` handle for a JNI adapter, or + /// a `*const T` raw pointer for a C adapter). Other converters that + /// ask "what's the wire form of this rust type?" read this. + pub destination: syn::Type, + /// Complete generated function for the **wire-facing** stage of the + /// converter (signature, body, attributes, lifetimes). The adapter + /// owns the shape. Callers compute this stage's name via + /// `function.sig.ident`. + pub function: syn::ItemFn, + /// **Rust-side** stages that compose with [`Self::function`] to form + /// the full chain — copied verbatim from the resolving + /// [`crate::api::core::prebindgen::ConverterImpl::pre_stages`]. See + /// that field's docs for the chain-order semantics. + pub pre_stages: Vec>, + /// Inner types whose function delegates to their converters. Empty for + /// terminal converters; populated by wrapper converters. Used by the + /// post-resolution propagation pass. + pub subs: Vec, + /// Wire bit-patterns this converter never produces / always rejects. + /// Wrappers (`Option<_>`, sum-typed enums) carve from this set for + /// their own discriminants. See [`Niches`] for the cascade model. + pub niches: Niches, + /// Adapter-specific extras carried in by the + /// [`crate::api::core::prebindgen::ConverterImpl`] that filled this + /// slot. Emitter code reads this directly — the registry is the + /// single source of truth for cross-language facts (C header names, + /// JVM class names, etc.). Defaults to `()` for adapters that don't + /// need any. + pub metadata: M, +} + +impl TypeEntry { + /// The resolved form of what a generator built. + /// + /// The only difference is `subs`: a generator names its inners as types, + /// and the table keys them. + pub fn from_converter(c: crate::api::core::prebindgen::ConverterImpl) -> Self { + Self { + destination: c.destination, + function: c.function, + pre_stages: c.pre_stages, + subs: c.subs.iter().map(TypeKey::from_type).collect(), + niches: c.niches, + metadata: c.metadata, + } + } + + /// Identifier of the wire-facing converter function. + pub fn converter_ident(&self) -> &syn::Ident { + &self.function.sig.ident + } + + /// Wire/destination type carried by this converter on success. + pub fn wire_type(&self) -> &syn::Type { + &self.destination + } + + /// Rust-side stages in input execution order, after the wire-facing + /// converter has decoded the wire value. + pub fn input_stage_order(&self) -> impl Iterator)> { + self.pre_stages.iter().enumerate().rev() + } + + /// Rust-side stages in output execution order, before the wire-facing + /// converter encodes the final wire value. + pub fn output_stage_order(&self) -> impl Iterator)> { + self.pre_stages.iter().enumerate() + } + + /// Immediate converter dependencies recorded by the adapter when this entry + /// resolved. + pub fn dependency_keys(&self) -> &[TypeKey] { + &self.subs + } +} + +/// Direction of a converter pair. +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +pub enum Direction { + /// Wire → Rust. + Input, + /// Rust → Wire. + Output, +} + +impl Direction { + pub fn flip(self) -> Self { + match self { + Direction::Input => Direction::Output, + Direction::Output => Direction::Input, + } + } +} diff --git a/prebindgen/src/api/core/registry/declare.rs b/prebindgen/src/api/core/registry/declare.rs new file mode 100644 index 00000000..54dc3c48 --- /dev/null +++ b/prebindgen/src/api/core/registry/declare.rs @@ -0,0 +1,462 @@ +//! Build a registry: say what the binding contains, then close it. +//! +//! Every declaring method here records; none derives. The builder is a passive +//! recorder precisely so it never has to call back into a generator to find out +//! what it is meant to produce — and it is a *separate type* from [`Registry`] +//! so that "still being described" and "finished, and answerable" cannot be +//! confused for one another. + +use std::collections::{HashMap, HashSet}; + +use super::*; + +/// A registry under construction. +/// +/// Chain the declarations, hand over the conversions, then [`build`](Self::build): +/// +/// ```ignore +/// let registry = Registry::builder(flat)? +/// .export(&name) +/// .decompose(decompositions) +/// .convert_with(|crossing, built| my_gen.convert(crossing, built))? +/// .build()?; +/// ``` +/// +/// The result is read-only. Nothing can add a crossing to a `Registry`, which +/// is what makes "every crossing has a conversion" a fact about the type rather +/// than a phase you have to be careful about. +pub struct RegistryBuilder { + registry: Registry, + /// Conversions handed over so far, applied at [`Self::build`]. + built: HashMap>, + /// The scan runs once, on demand: it needs every declaration, and + /// [`Self::crossings`] / [`Self::convert_with`] / [`Self::build`] each need + /// it to have run. `Some` holds the derived demand, in order. + order: Option>, +} + +impl Registry { + /// Start describing a binding over this model. + /// + /// A `Flat` is what a registry projects, and reading captured prebindgen + /// output into one is [`FlatBuilder`](crate::core::flat::FlatBuilder)'s job + /// — so a build script says where items come from at the layer that owns + /// the question, and there is one such layer rather than two: + /// + /// ``` + /// # prebindgen::Source::init_doctest_simulate(); + /// use prebindgen::core::{Flat, Registry}; + /// + /// let flat = Flat::builder().source("source_ffi").build()?; + /// // Annotated only because nothing here resolves: in a build script `M` is + /// // fixed by the adapter passed to `resolve`, so no call site names it. + /// let registry: Registry<()> = Registry::builder(flat)?.build()?; + /// assert!(registry.flat().function("test_function").is_some()); + /// # Ok::<_, Box>(()) + /// ``` + /// + /// Several sources compose there too, including one this crate renames: + /// + /// ```ignore + /// let flat = Flat::builder() + /// .source(flat_crate::PREBINDGEN_OUT_DIR) + /// .source_named(helpers::PREBINDGEN_OUT_DIR, "helpers") + /// .build()?; + /// ``` + /// + /// **Fails on anything the language cannot express** — a `self` receiver, an + /// `async fn`, a generic binder, a type form outside the grammar, or a + /// reference to a type the flat API does not declare. All of them at once, so + /// a source crate that needs migrating sees one list instead of one rebuild + /// per item. This is independent of what any binding declares: an + /// inexpressible item is a hard error whether or not it is ever named. + pub fn builder(flat: crate::api::core::flat::Flat) -> Result, ScanError> { + let entries: Vec = flat + .unsupported() + .map(|u| NotExpressibleEntry { + name: u.name.clone(), + reason: u.error.to_string(), + location: (*u.origin.location).clone(), + }) + .collect(); + if !entries.is_empty() { + return Err(ScanError::NotExpressible { entries }); + } + + let mut registry = Registry::empty(); + registry.flat = flat; + Ok(RegistryBuilder { + registry, + built: HashMap::new(), + order: None, + }) + } +} + +impl RegistryBuilder { + // ── configure: what this binding builds ─────────────────────────── + // + // Pushed in by the generator before `resolve`. The registry never asks — + // it records, then derives the crossing set from what it was given. + + /// An element this binding **exports**. + /// + /// The model says how to derive its crossings, so the caller does not: a + /// function's signature gives its parameters (in) and its return (out); a + /// const gives its value type (out). A name matching no element is an + /// error, reported with every other missing name at once by `resolve` + /// rather than here — a build script with three typos should learn all + /// three in one build. + pub fn export(mut self, name: &syn::Ident) -> Self { + self.registry.declared.functions.insert(name.clone()); + self + } + + /// A const this binding exports. + /// + /// Separate from [`Self::export`] only because *having a const mechanism at + /// all* is itself a fact: a binding that never calls this re-emits every + /// captured const verbatim, while one that calls it emits exactly what it + /// names. See [`Self::declares_consts`]. + pub fn export_const(mut self, name: &syn::Ident) -> Self { + self.registry + .declared + .consts + .get_or_insert_with(HashSet::new) + .insert(name.clone()); + self + } + + /// Declare that this binding has a const mechanism, even if it exports no + /// consts. Without it every captured const is re-emitted verbatim. + pub fn declares_consts(mut self) -> Self { + self.registry + .declared + .consts + .get_or_insert_with(HashSet::new); + self + } + + /// A type this binding **exports**: it crosses in both directions, and its + /// body — a struct's fields, an enum's payloads — is scanned too. + pub fn export_type(mut self, key: TypeKey) -> Self { + self.registry.declared.types.insert(key); + self + } + + /// A type that **crosses** in one direction without being exported. + /// + /// The escape hatch for a crossing no signature can yield: a re-exported + /// foreign type named by a class declaration, or the value type of a + /// constant the binding synthesizes. Direction is explicit because these + /// are genuinely one-sided — which is what stops an output-only crossing + /// from silently lacking its input twin, the asymmetry the old + /// `required_output_types` had. + pub fn cross(mut self, dir: Direction, ty: &syn::Type) -> Self { + self.registry.declared.crossings.push((dir, ty.clone())); + self + } + + /// `from`'s conversion needs `on`'s to exist first. + /// + /// [`Self::crossings`] derives its order from the type structure, which + /// covers almost everything: an `Option` visibly contains a `T`. It + /// cannot see a dependency the *declaration* creates — a `convert!` whose + /// body chains through a helper function's parameter type, say, where + /// nothing about the target type mentions the other side. + /// + /// State those here, and the order accounts for them. Getting it wrong is + /// not silent: the conversion that needed the missing one simply cannot be + /// built, and [`Self::build`] names it. + pub fn depends(mut self, from: Crossing, on: Crossing) -> Self { + self.registry.declared.edges.push((from, on)); + self + } + + /// A function this binding **references but never emits** — a helper whose + /// name appears in a declaration. Its absence is an error; its presence + /// emits nothing. + pub fn reference(mut self, name: &syn::Ident) -> Self { + self.registry.declared.helper_functions.insert(name.clone()); + self + } + + /// A function the **binding crate itself** defines, with the module path + /// generated calls should qualify it by. + /// + /// There is no `#[prebindgen]` item behind it, so this is the one input + /// that adds to the model rather than selecting from it: only the + /// signature is read, never the body. A name colliding with a captured + /// item is an error — the generated call would resolve the wrong function. + pub fn local_function( + mut self, + item_fn: syn::ItemFn, + origin: String, + ) -> Result { + let ident = item_fn.sig.ident.clone(); + // Written by hand in a build script, so the grammar is checked here or + // nowhere: a dropped `self` receiver would surface as an arity mismatch + // out of rustc on generated code, which is the wrong end of the pipeline + // to learn about a build.rs typo. + let lowered = self + .registry + .flat + .lower_signature(&item_fn) + .map_err(|error| ScanError::AdapterInvariant { + message: format!("binding-local fn `{ident}`: {error}"), + })?; + if self.registry.flat.element(&ident).is_some() { + return Err(ScanError::AdapterInvariant { + message: format!( + "binding-local fn `{ident}` collides with a `#[prebindgen]` item — \ + the generated call would resolve the wrong fn; rename the \ + binding-local fn" + ), + }); + } + self.registry.flat.add_local_function(lowered, origin); + Ok(self) + } + + /// A function a decomposition reaches through rather than emits — excluded + /// from constructor composition, and the only functions a decomposer record + /// may name. + /// + /// Rides here until decompositions carry their own shape (step 2 of #251); + /// it is a property of the decomposition, not of the binding. + pub fn accessor(mut self, name: &syn::Ident) -> Self { + self.registry.declared.accessors.insert(name.clone()); + self + } + + /// The receiver type of a function emitted as a method. Same temporary + /// home as [`Self::accessor`]. + pub fn method_receiver(mut self, name: &syn::Ident, receiver: TypeKey) -> Self { + self.registry + .declared + .method_receivers + .insert(name.clone(), receiver); + self + } + + /// How this binding's composites cross **in pieces** instead of whole. + /// + /// Stated once, before [`Self::resolve`]. Replaces five separate callbacks + /// the registry used to make into the generator; see [`Decompositions`]. + pub fn decompose(mut self, d: Decompositions) -> Self { + self.registry.declared.decompositions = d; + self + } +} + +impl RegistryBuilder { + /// The model being described. Complete from the first call: everything that + /// adds to it ([`Self::local_function`]) is a declaration, not a derivation. + pub fn flat(&self) -> &crate::api::core::flat::Flat { + &self.registry.flat + } + + /// Module paths of every ingested source, ingestion order. + /// + /// A model question, and the model is complete from the first call — so the + /// builder answers it exactly as the finished registry does. + pub fn all_source_modules(&self) -> Vec { + self.registry.all_source_modules() + } + + /// The origin crate's module path for an item — see + /// [`Registry::origin_module`]. + pub fn origin_module(&self, ident: &syn::Ident) -> Option { + self.registry.origin_module(ident) + } + + /// The default module for references with no recorded origin — see + /// [`Registry::default_module`]. + pub fn default_module(&self) -> Option { + self.registry.default_module() + } + + /// Every **named** item the model holds — see + /// [`Registry::named_item_idents`]. + pub fn named_item_idents(&self) -> impl Iterator { + self.registry.named_item_idents() + } + + /// Whether the source declares a type under this name — see + /// `Registry::declares_type`. + #[cfg(test)] + pub(crate) fn declares_type(&self, ident: &syn::Ident) -> bool { + self.registry.declares_type(ident) + } + + /// Run the scan and apply the decompositions, once. + /// + /// Private and idempotent: three entry points need it to have happened, and + /// none of them should care whether it already did. + fn derive(&mut self) -> Result<&[Crossing], WriteRustError> { + if self.order.is_none() { + let mut declared = std::mem::take(&mut self.registry.declared); + let out = (|| { + self.registry.scan_declared_items(&declared)?; + self.registry.apply_adapter_plans(&mut declared) + })(); + self.registry.declared = declared; + out?; + self.order = Some(self.registry.crossings()); + } + Ok(self.order.as_deref().unwrap_or_default()) + } + + /// What a conversion — or a validation — is written against right now: the + /// model, the full crossing population, and whatever has been built so far. + fn view(&self) -> Building<'_, M> { + Building::new( + &self.registry, + &self.built, + self.order.as_deref().unwrap_or_default(), + ) + } + + /// Check this binding against a generator's own invariants, now that the + /// scan has read every declared signature. + /// + /// Earliest it can run: a missing declaration has already hard-errored, so + /// a check here sees only items that exist. + pub fn validate_with(mut self, adapter: &E) -> Result + where + E: Prebindgen, + { + self.derive()?; + adapter + .validate(&self.view()) + .map_err(|message| ScanError::AdapterInvariant { message })?; + Ok(self) + } + + /// Every crossing this binding needs a conversion for, **inner types + /// first** — see [`Registry::crossings`] for what the order guarantees. + /// + /// Take this when you want to drive the loop yourself and hand the result + /// back through [`Self::conversions`]. [`Self::convert_with`] is the same + /// walk with the loop written for you. + pub fn crossings(&mut self) -> Result, WriteRustError> { + Ok(self.derive()?.to_vec()) + } + + /// Build a conversion for each crossing, in dependency order. + /// + /// `f` is called once per crossing with the conversions already built, so + /// by the time it sees `Option` it can look up `Handle`. Returning + /// `None` records a gap — whether that gap matters is decided by + /// [`Self::build`], not here. + /// + /// This is a convenience over [`Self::crossings`] + [`Self::conversions`], + /// not a second mechanism: it is the same list, walked in the same order. + /// Nothing about it lets the registry choose when to call back — the + /// closure is yours, and the walk is finished before this returns. + pub fn convert_with(mut self, mut f: F) -> Result + where + F: FnMut( + &Crossing, + &Building<'_, M>, + ) -> Option>, + { + let order = self.derive()?.to_vec(); + for crossing in &order { + let conv = f(crossing, &self.view()); + if let Some(c) = conv { + self.built + .insert(crossing.clone(), TypeEntry::from_converter(c)); + } + } + Ok(self) + } + + /// Hand over conversions built elsewhere — the bulk peer of + /// [`Self::convert_with`], for a generator that walked + /// [`Self::crossings`] itself. + /// + /// Accumulates, so it composes with `convert_with` and with itself. + pub fn conversions(mut self, conversions: HashMap>) -> Self { + self.built.extend(conversions); + self + } + + /// The scanned registry, with no conversions applied and no completeness + /// check. + /// + /// Test-only, and deliberately so: it is the state between "described" and + /// "answerable", which is exactly what the split exists to keep out of + /// everyone else's hands. + #[cfg(test)] + pub(crate) fn scanned(mut self) -> Result, ScanError> { + // Narrower than `build`'s error on purpose: the scan is the only phase + // this runs, so a test matching on `ScanError` says what it means. + match self.derive() { + Ok(_) => Ok(self.registry), + Err(WriteRustError::Scan(e)) => Err(e), + Err(other) => panic!("scanned(): unexpected non-scan failure: {other}"), + } + } + + /// Close the binding: apply every conversion, check the set is complete, + /// and hand back a registry that can only be read. + /// + /// A crossing with no conversion is not itself a failure — the scan + /// over-approximates on purpose. What fails is a crossing *reachable from + /// an export* with none, and the error names every one at once. + pub fn build(mut self) -> Result, WriteRustError> { + self.derive()?; + for ((dir, key), entry) in self.built { + if let Some(cell) = self.registry.type_table_mut(dir).get_mut(&key) { + cell.entry = Some(entry); + } + } + crate::api::core::resolve::check_complete(&self.registry)?; + Ok(self.registry) + } +} + +/// A builder answers the same questions a finished registry does — with one +/// difference that is the whole point of the split: [`conversion`] sees only +/// what has been handed over *so far*. +/// +/// That is what a generator writing a conversion needs (its inners, already +/// built) and it is all it should be able to see. Everything else — the model, +/// the decompositions — is complete from the moment it is declared. +/// +/// [`conversion`]: Conversions::conversion +impl Conversions for RegistryBuilder { + fn flat(&self) -> &crate::api::core::flat::Flat { + &self.registry.flat + } + fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { + self.built.get(&(dir, TypeKey::from_type(ty))) + } + fn crossing_keys(&self, dir: Direction) -> Vec { + self.order + .as_deref() + .unwrap_or_default() + .iter() + .filter(|(d, _)| *d == dir) + .map(|(_, k)| k.clone()) + .collect() + } + fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan> { + self.registry.callback_arg_plans.get(key) + } + fn callback_arg_plans(&self) -> &HashMap { + &self.registry.callback_arg_plans + } + fn unfold_plans(&self) -> &HashMap { + &self.registry.unfold_plans + } + fn error_plans(&self) -> &HashMap { + &self.registry.error_plans + } + fn decon_plans( + &self, + ) -> &HashMap { + &self.registry.decon_plans + } +} diff --git a/prebindgen/src/api/core/registry/error.rs b/prebindgen/src/api/core/registry/error.rs new file mode 100644 index 00000000..1963428f --- /dev/null +++ b/prebindgen/src/api/core/registry/error.rs @@ -0,0 +1,222 @@ +//! What can go wrong before a binding is written. + +use std::fmt; + +use crate::SourceLocation; + +impl From for ScanError { + fn from(e: crate::api::core::flat::ParseError) -> Self { + match e { + crate::api::core::flat::ParseError::DuplicateName(d) => { + ScanError::DuplicateName(Box::new(DuplicateNameError { + name: d.name, + first: d.first, + second: d.second, + first_crate: d.first_crate, + second_crate: d.second_crate, + })) + } + } + } +} + +/// One item of a [`ScanError::NotExpressible`] report. +#[derive(Debug)] +pub struct NotExpressibleEntry { + /// The item's name, or `None` for an item kind that has none. + pub name: Option, + /// Rendered [`ItemError`](crate::core::flat::ItemError) — the frontend's own + /// message, so one authority produces it. + pub reason: String, + pub location: SourceLocation, +} + +/// Payload of [`ScanError::DuplicateName`], boxed to keep the error enum +/// small (`clippy::result_large_err`). +#[derive(Debug)] +pub struct DuplicateNameError { + pub name: syn::Ident, + pub first: SourceLocation, + pub second: SourceLocation, + /// Origin crates of the colliding items, when known (multi-source + /// ingestion via several `Flat::builder().source(..)` feeders) — the `SourceLocation` + /// file paths are crate-relative, so with several sources they alone + /// may not identify the colliding crates. + pub first_crate: Option, + pub second_crate: Option, +} + +/// Errors surfaced by the scan phase. +#[derive(Debug)] +pub enum ScanError { + DuplicateName(Box), + /// Items the flat language cannot express, all of them at once. + /// + /// The message for each comes from + /// [`ItemError`](crate::core::flat::ItemError), so one authority produces it. + /// This replaces the per-item guards the registry used to duplicate — a `self` + /// receiver, a non-ident parameter pattern, a disallowed `impl Trait` — which + /// the frontend now catches with a richer diagnosis (it names the parameter). + NotExpressible { + entries: Vec, + }, + /// An adapter-invariant check failed — see [`Prebindgen::validate`]. + /// The message is adapter-authored and printed verbatim. + AdapterInvariant { + message: String, + }, + /// Explicitly declared items (functions, helper functions, constants) + /// that match no indexed `#[prebindgen]` item. A declaration is a + /// statement of intent — its target being absent is always a bug (a + /// typo in build.rs, or the item was renamed/removed in the source + /// crate), so this is a hard error, unlike the soft warnings for stale + /// *ignore* entries. All missing names are collected before failing. + DeclaredNotFound { + entries: Vec<(&'static str, String)>, + }, + /// Declared type keys that qualify a source item with its crate path + /// (`ptr_class!(myflat::Foo)` where `myflat` is a chained source crate). + /// Source items live in one flat namespace and are keyed by their bare + /// name — the qualified spelling can never match a captured signature, + /// so it is a hard error with a fix-it instead of a silent miss (issue + /// #95). All offenders are collected before failing. + QualifiedDeclaredTypes { + /// `(qualified spelling, bare fix-it name)` pairs. + entries: Vec<(String, String)>, + }, +} + +impl fmt::Display for ScanError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ScanError::DuplicateName(e) => { + let in_crate = |c: &Option| match c { + Some(c) => format!(" in crate `{c}`"), + None => String::new(), + }; + write!( + f, + "duplicate prebindgen name `{}`: first{} at {}, second{} at {} — prebindgen \ + items live in one flat namespace across all sources; rename one of them", + e.name, + in_crate(&e.first_crate), + e.first, + in_crate(&e.second_crate), + e.second + ) + } + ScanError::NotExpressible { entries } => { + write!( + f, + "{} `#[prebindgen]` item(s) the flat language cannot express:", + entries.len() + )?; + for e in entries { + // The crate, because a captured path is crate-relative: with + // several sources, two offenders both read `src/lib.rs:..` + // and the location alone says nothing about which one to fix. + // Same reason the duplicate-name diagnostic carries it. + let in_crate = match &e.location.crate_name { + Some(c) => format!(" in crate `{c}`"), + None => String::new(), + }; + match &e.name { + Some(name) => { + write!(f, "\n {}{in_crate}: {name} {}", e.location, e.reason)? + } + None => write!(f, "\n {}{in_crate}: {}", e.location, e.reason)?, + } + } + Ok(()) + } + ScanError::AdapterInvariant { message } => write!(f, "{}", message), + ScanError::DeclaredNotFound { entries } => { + writeln!( + f, + "{} declared item(s) not found among #[prebindgen] items:", + entries.len() + )?; + for (kind, name) in entries { + writeln!(f, " - {kind} `{name}`")?; + } + write!( + f, + "a declaration names an item that does not exist — typo in build.rs, \ + or renamed/removed in the source crate?" + ) + } + ScanError::QualifiedDeclaredTypes { entries } => { + writeln!( + f, + "{} declared type(s) qualify a source item with its crate path:", + entries.len() + )?; + for (spelled, bare) in entries { + writeln!(f, " - `{spelled}` — declare it as `{bare}`")?; + } + write!( + f, + "source items live in one flat namespace keyed by their bare name; \ + a crate-qualified spelling never matches captured signatures" + ) + } + } + } +} + +impl std::error::Error for ScanError {} + +/// Combined error surfaced by `RegistryBuilder::build` and by a generator's +/// own `build` / `write_rust`. +#[derive(Debug)] +pub enum WriteRustError { + Scan(ScanError), + Expand(crate::api::core::expand::ExpandError), + Unfold(crate::api::core::unfold::UnfoldError), + Resolve(crate::api::core::resolve::ResolveError), + Write(crate::api::core::write::WriteError), +} + +impl fmt::Display for WriteRustError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WriteRustError::Scan(e) => write!(f, "{}", e), + WriteRustError::Expand(e) => write!(f, "{}", e), + WriteRustError::Unfold(e) => write!(f, "{}", e), + WriteRustError::Resolve(e) => write!(f, "{}", e), + WriteRustError::Write(e) => write!(f, "{}", e), + } + } +} + +impl std::error::Error for WriteRustError {} + +impl From for WriteRustError { + fn from(e: ScanError) -> Self { + WriteRustError::Scan(e) + } +} + +impl From for WriteRustError { + fn from(e: crate::api::core::expand::ExpandError) -> Self { + WriteRustError::Expand(e) + } +} + +impl From for WriteRustError { + fn from(e: crate::api::core::unfold::UnfoldError) -> Self { + WriteRustError::Unfold(e) + } +} + +impl From for WriteRustError { + fn from(e: crate::api::core::resolve::ResolveError) -> Self { + WriteRustError::Resolve(e) + } +} + +impl From for WriteRustError { + fn from(e: crate::api::core::write::WriteError) -> Self { + WriteRustError::Write(e) + } +} diff --git a/prebindgen/src/api/core/registry/key.rs b/prebindgen/src/api/core/registry/key.rs new file mode 100644 index 00000000..df32d995 --- /dev/null +++ b/prebindgen/src/api/core/registry/key.rs @@ -0,0 +1,111 @@ +//! The canonical identity of a type: its normalized token string. + +use std::fmt; + +use quote::ToTokens; + +/// Canonical type-shape key: identity is the token string of the +/// **normalized** type ([`crate::api::core::types_util::normalize_type`] — +/// group/paren unwrap, `crate::`/`self::` and std-prelude path reduction; +/// the complete equivalence rule set is documented there). The normalized +/// parsed form is kept alongside the string, so [`Self::to_type`] is an +/// infallible clone — no core invariant depends on serialize-then-reparse +/// round trips (issue #95). +#[derive(Clone)] +pub struct TypeKey { + /// Canonical token string — the identity `Eq`/`Hash` compare. + canon: std::rc::Rc, + /// The normalized parsed form the string was rendered from. + ty: std::rc::Rc, +} + +impl PartialEq for TypeKey { + fn eq(&self, other: &Self) -> bool { + self.canon == other.canon + } +} +impl Eq for TypeKey {} +impl std::hash::Hash for TypeKey { + fn hash(&self, state: &mut H) { + self.canon.hash(state) + } +} +impl PartialOrd for TypeKey { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for TypeKey { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.canon.cmp(&other.canon) + } +} +// Keep the historical single-field tuple rendering (`TypeKey("Vec < u8 >")`) +// — error text and test expectations format keys through it. +impl fmt::Debug for TypeKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("TypeKey").field(&&*self.canon).finish() + } +} + +/// Structured failure of [`TypeKey::parse`]: the offending input plus the +/// underlying `syn` parse error. +#[derive(Debug)] +pub struct TypeKeyParseError { + pub input: String, + pub error: syn::Error, +} + +impl fmt::Display for TypeKeyParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid type `{}`: {}", self.input, self.error) + } +} + +impl std::error::Error for TypeKeyParseError {} + +impl TypeKey { + /// Build a key by parsing the input as a type and normalizing. + pub fn parse(s: &str) -> Result { + let ty: syn::Type = syn::parse_str(s).map_err(|error| TypeKeyParseError { + input: s.to_string(), + error, + })?; + Ok(Self::from_type(&ty)) + } + + /// Build a key directly from a `syn::Type` (normalizing a clone; the + /// input is not modified). + pub fn from_type(ty: &syn::Type) -> Self { + // Off the shared reduction, so this key and the model's type index + // cannot drift apart about what a type is called. + let t = crate::api::core::types_util::canonical_type(ty); + Self { + canon: t.to_token_stream().to_string().into(), + ty: std::rc::Rc::new(t), + } + } + + /// Build a key for a bare item ident — infallible by construction (an + /// ident IS a single-segment path type; nothing to parse or normalize). + pub fn from_ident(ident: &syn::Ident) -> Self { + Self::from_type(&crate::api::core::types_util::type_from_ident(ident)) + } + + /// The canonical string form. + pub fn as_str(&self) -> &str { + &self.canon + } + + /// The normalized parsed form. Infallible — a clone of the stored type, + /// never a reparse. + pub fn to_type(&self) -> syn::Type { + (*self.ty).clone() + } +} + +impl fmt::Display for TypeKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.canon) + } +} diff --git a/prebindgen/src/api/core/registry/mod.rs b/prebindgen/src/api/core/registry/mod.rs new file mode 100644 index 00000000..0e9a90e4 --- /dev/null +++ b/prebindgen/src/api/core/registry/mod.rs @@ -0,0 +1,364 @@ +//! Which type conversions a binding needs, and whether it has them all. +//! +//! # The boundary +//! +//! [`Flat`](crate::core::flat::Flat) describes the source Rust code. A binding +//! puts a wrapper on each side of an FFI boundary — generated Rust that the +//! destination language can call, and destination-language code shaped to match +//! it (`#[repr(C)]` structs and a C header; JNI externs and Kotlin classes). +//! +//! ```text +//! source flat API generated wrapper destination +//! (idiomatic Rust) language +//! ──────────────── ───────────────── ──────────── +//! fn ledger_filed(&Ledger) ──► #[no_mangle] extern fn ◄──► external fun +//! -> Option (jlong) -> jlong fun filed(): Report? +//! ▲ +//! └── the boundary: the WIRE +//! jlong / jint / jobject (JNI) +//! *const T / size_t (C) +//! ``` +//! +//! The wrapper's **body** speaks source Rust; its **signature** speaks wire. The +//! translation between the two is a *conversion*, and collecting them is this +//! module's whole job. +//! +//! # What a conversion is +//! +//! A [`TypeEntry`]: a `destination` (the wire type), a wire-facing `function`, +//! and `pre_stages` — the Rust-side stages that compose with it. **A chain, not a +//! function**, which is how composition works: `Option`'s chain embeds +//! `Handle`'s. +//! +//! A composite need not cross whole. `Option` may cross as a `T` carrying a +//! niche value, as a `(bool, T)` pair, or as leaves delivered separately — which, +//! is the adapter's choice, and the registry records it so the emitter can call +//! it by name and the destination side can be written to match. +//! +//! Conversions are **directional**, which is why [`Direction`] is half of a +//! [`Crossing`] rather than a name prefix on two tables. `&str` inbound is a +//! `jstring` to decode, outbound a `jstring` to allocate, and one direction may +//! be convertible while the other is not. A callback flips it — `impl +//! Fn(Sample)` is an *input* whose argument crosses *outbound*. +//! +//! # What the registry does +//! +//! It **derives** the set, then **checks it is complete**. +//! +//! A binding names a surface: these functions, these types, these consts. Far +//! more types than that must convert — parameter and return types, type +//! arguments, struct fields, enum payloads, callback arguments in the flipped +//! direction, and the leaves a decomposed value arrives in. Computing that +//! closure is the work; completeness is meaningful precisely because the set is +//! derived here rather than handed over. +//! +//! **It never writes a conversion.** It cannot — only a language adapter knows +//! what a `jlong` handle or a `*const T` is. The registry decides *which* are +//! needed, asks the adapter for each, and fails naming any that could not be +//! supplied. +//! +//! # In, and out +//! +//! | in | | +//! |---|---| +//! | the model | [`Flat`](crate::core::flat::Flat) — what the source offers | +//! | the crossings | which `(direction, type)` pairs actually cross | +//! | the decompositions | how a composite crosses in pieces: which leaf crossings that adds, and which whole-value crossing it removes | +//! | a conversion builder | the [`Prebindgen`] adapter | +//! +//! Out: a conversion for every type in the closure — or a failure naming the +//! ones that must convert and cannot. The emitter then writes the file: the +//! conversions, and the per-item wrappers that call them. +//! +//! # Using a registry +//! +//! **Describe it, hand over the answers, read it.** Two types, because those +//! are two different things: a [`RegistryBuilder`] is still being described, +//! and a [`Registry`] is finished and answerable. +//! +//! ```text +//! describe builder(flat) · export · cross · decompose · depends +//! ↓ +//! the demand crossings() → every crossing needing a conversion, +//! ↓ sorted so each type's inners come first +//! the answers convert_with(f) → one call per crossing, in that order +//! ↓ conversions(map) → or hand over a map you built yourself +//! ↓ +//! close build() → fails naming any reachable crossing with +//! ↓ no conversion +//! read flat · exports · conversion(dir, ty) · decomposition(site) · … +//! ``` +//! +//! Most types need no declaring: they are reached by walking a declared +//! element's signature, and deriving them per **usage** is what keeps an +//! output-only type from being demanded as an input too. Measured: dropping the +//! declaration-as-root for every type with a captured body leaves the generated +//! output byte-identical. +//! +//! But a type with **no captured item behind it** — `ptr_class!(zenoh::KeyExpr<'static>)` +//! on a re-exported foreign type — appears in no signature this model can walk, +//! so nothing derives it and the declaration is the only statement that it +//! crosses at all. That is what `cross` is for, and why the input cannot be +//! elements alone. +//! +//! ```ignore +//! let mut builder = Registry::builder(flat)?; +//! for name in &self.exported { builder = builder.export(name); } +//! for ty in &self.foreign_types { builder = builder.cross(Direction::Output, ty); } +//! +//! let registry = builder +//! .decompose(self.decompositions()) +//! // `built` already holds everything this crossing composes from: that is +//! // what sorted means. +//! .convert_with(|crossing, built| self.convert(crossing, built))? +//! .build()?; +//! +//! self.emit(®istry, out) // read-only from here +//! ``` +//! +//! Prefer to drive the walk yourself? `crossings()` hands over the same list in +//! the same order, and `conversions(map)` takes the result — the two compose, +//! and neither is a second mechanism. +//! +//! **Nothing here calls back into the generator** — not by trait hook, and not +//! by a `next_request`/`supply` pull loop either, which is the same protocol +//! with the arrow flipped. `convert_with` is not that: the walk finishes before +//! it returns, the closure is the caller's, and the builder chooses nothing +//! about when it runs. It is `crossings()` plus a `for` loop, written once. +//! +//! What makes a single hand-off possible is the **sort**. The demand's edges +//! (`immediate_edges` — generic arguments, tuple/reference/slice targets, +//! declared struct fields, and `impl Fn` arguments with the direction flipped) +//! are structural, so they are known without asking anyone. Ordering +//! the closure by them means a generator building `Option` already holds +//! `Handle`, which is why it can work from a flat list instead of being called +//! back per type. It also means each crossing is offered exactly once: a +//! generator's `None` says *cannot*, never *not yet*. +//! +//! A `None` is not itself a failure. The scan over-approximates deliberately — +//! every nested position, every declared struct in both directions — so whether +//! a gap matters is reachability from the exports, which `build` decides. +//! +//! The structure covers almost every dependency, because an `Option` +//! visibly contains a `T`. What it cannot show is one a *declaration* creates — +//! a `convert!` chaining through a helper's parameter type, or a callback +//! argument delivered as plan leaves. Those are stated with `depends`, and +//! getting one wrong is not silent: the conversion that needed the missing one +//! cannot be built, and `build` names it. +//! +//! **Cycles** are the one place the order cannot be honoured: a self-referential +//! type (`struct Node { next: Option> }`) has none. `crossings` breaks +//! such a cycle at its entry, so exactly one member is offered before an inner +//! it contains. A generator that cannot build it omits it, and it is reported +//! like any other gap. +//! +//! Direction is a **parameter**, never part of a name: [`Direction`] already +//! carries it, and one `conversion(dir, ty)` cannot drift the way an +//! `input_`/`output_` pair can — as `required_output_types`, which never grew an +//! input peer, shows. + +use std::collections::{HashMap, HashSet}; + +use crate::{ + api::core::{ + niches::Niches, + prebindgen::{Prebindgen, Stage}, + types_util::bare_path_ident, + }, + SourceLocation, +}; + +mod cell; +pub(crate) use self::cell::{TypeCell, TypeSubject}; +mod declare; +mod error; +mod key; +mod model; +mod order; +mod run; +mod scan; +mod view; +mod walk; + +pub use self::{ + cell::{Direction, TypeEntry}, + declare::RegistryBuilder, + error::{DuplicateNameError, NotExpressibleEntry, ScanError, WriteRustError}, + key::{TypeKey, TypeKeyParseError}, + view::{Building, Conversions, Crossing}, + walk::{extract_fn_trait_args, immediate_subtype_positions}, +}; + +/// Single owner of everything parsed from the prebindgen source stream. +/// +/// The metadata parameter `M` is the language adapter's per-converter +/// extra type, supplied via +/// [`crate::api::core::prebindgen::Prebindgen::Metadata`]. Each +/// [`TypeEntry`] carries one `M` copied in by the resolver from the +/// [`crate::api::core::prebindgen::ConverterImpl`] that produced it. +/// Adapters that don't carry extras leave `M = ()`. +pub struct Registry { + /// The parsed model these maps project. Held rather than discarded, so a + /// later stage can ask it what a name means through the registry it already + /// has — see [`Self::flat`]. + flat: crate::api::core::flat::Flat, + /// What the binding declared, pushed in through `RegistryBuilder`'s + /// `export` / `export_type` / `cross` / `reference` before its `build`. + /// + /// Stored rather than asked for: the registry never calls the generator to + /// find out what to build. It is also read after resolution — `write`'s + /// emission gate is "did the binding declare this item" — so it outlives + /// the scan that consumes it. + declared: Declared, + /// Type tables, one per direction. Each scanned type gets a [`TypeCell`] + /// holding what the key names, whether the binding asks for it directly, and + /// the conversion once the generator supplies one. + /// + /// **Crate-internal.** Outside, a table is reached through + /// [`Conversions::conversion`] — which is what makes direction a parameter + /// rather than half of a field name, and what stops anyone observing a cell + /// before `RegistryBuilder::build` has graded it. + pub(crate) input_types: HashMap>, + pub(crate) output_types: HashMap>, + + /// Resolved constructor-expansion plans, keyed by `(function, parameter)`. + /// Filled by [`crate::api::core::expand::apply`] before resolution; read + /// by language adapters at the parameter-emission site. Empty unless the + /// adapter declared expansions. + pub(crate) expansion_plans: + HashMap<(syn::Ident, syn::Ident), crate::api::core::expand::FoldPlan>, + + /// Resolved output-expansion plans, keyed by function ident. Filled by + /// [`crate::api::core::unfold::apply`] before resolution; read by language + /// adapters at the return-emission site. Empty unless the adapter declared + /// deconstructors. + pub(crate) unfold_plans: HashMap, + + /// Resolved **error**-position expansion plans, keyed by function ident: the + /// decomposition of a fallible fn's `Result<_, E>` domain error `E` (from + /// `.convert_error` / `.deconstruct_error`). Separate from + /// [`Self::unfold_plans`] — a fn may have both an output and an error plan. + pub(crate) error_plans: HashMap, + + /// Default decomposition of a **callback argument** type — the `T` of a + /// declared fn's `impl Fn(T, …)` parameter — keyed by the bare arg type + /// (type-level, fn-independent). Filled by + /// [`crate::api::core::unfold::apply`] from the type's default + /// deconstructor (`by_ref = false`: the trampoline owns the value); read by + /// language adapters when emitting the callback trampoline. A type without + /// a default deconstructor has no entry and is delivered whole. + pub(crate) callback_arg_plans: HashMap, + + /// The declaration-default decomposition per deconstructor declaration + /// ([`crate::api::core::unfold::DeconId`]) — resolved once with + /// normalized inputs, independent of using functions and processing + /// order. The single source language adapters derive declaration-keyed + /// signature artifacts (e.g. generated callback interfaces) from, so + /// every function selecting the same declaration sees one signature by + /// construction. + pub(crate) decon_plans: + HashMap, +} + +// Opaque — exists so `Result::expect_err` works in tests, the way +// `Generation`'s did before the generators took ownership of the built object. +impl std::fmt::Debug for Registry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Registry(..)") + } +} + +impl Registry { + /// An empty registry: no model, no items, no types. + /// + /// **Not public.** A `Registry` is a projection of a [`Flat`], and one built + /// this way projects nothing — [`Self::flat`] would hand a later stage an + /// empty model that claims to be this registry's source. Outside this crate + /// the entry point is [`Self::new`], which has a model behind it. + pub(crate) fn empty() -> Self { + Self { + flat: crate::api::core::flat::Flat::default(), + declared: Declared::default(), + input_types: Default::default(), + output_types: Default::default(), + expansion_plans: HashMap::new(), + unfold_plans: HashMap::new(), + error_plans: HashMap::new(), + callback_arg_plans: HashMap::new(), + decon_plans: HashMap::new(), + } + } +} + +/// Everything the caller declares about what a binding emits. +/// +/// **The registry's construction input.** It used to be assembled by calling +/// twenty-one getters back into the adapter from inside `resolve`, which put +/// "configuring" and "using" in the same call — and that is what let a converter +/// read a half-built registry, which is what made `None` ambiguous between +/// *defer* and *cannot*. The caller fills this first; `resolve` then passes or +/// fails. +#[derive(Default)] +pub(crate) struct Declared { + pub(crate) functions: HashSet, + /// Signature-scanned but not emitted — see [`Prebindgen::helper_functions`]. + pub(crate) helper_functions: HashSet, + pub(crate) accessors: HashSet, + pub(crate) method_receivers: HashMap, + pub(crate) types: HashSet, + /// Consts to scan and emit, or `None` when the adapter has no const + /// declaration mechanism — then every captured const is re-emitted + /// verbatim (see the const gate in [`crate::api::core::write`]). + /// + /// The two are identical for the *crossing set* — neither scans anything — + /// so this would be a plain `HashSet` if scanning were all it drove. It is + /// emission that needs the distinction, which is why the sentinel outlives + /// the skip warnings it also used to gate. + pub(crate) consts: Option>, + /// Crossings with no `#[prebindgen]` element behind them, each in the one + /// direction it actually crosses — see [`Registry::cross`]. + pub(crate) crossings: Vec<(Direction, syn::Type)>, + /// How composites cross in pieces — see [`Registry::decompose`]. + pub(crate) decompositions: Decompositions, + /// Ordering edges no syntax shows — see [`Registry::depends`]. + pub(crate) edges: Vec<(Crossing, Crossing)>, +} + +/// How a binding's composites cross **in pieces** instead of whole. +/// +/// One value, pushed once through `RegistryBuilder::decompose`, in place of the five +/// separate hooks the registry used to call back for (`expansions`, +/// `deconstructors`, `value_struct_decons`, `sum_decons`, +/// `leaf_vec_fold_elements`). All five are implemented by one adapter and none +/// of them ever needed more than the model, which is what makes stating them up +/// front possible. +/// +/// The fields are still the five declaration families, because unifying the +/// plan IRs behind them is its own problem (see issue #223) and pretending +/// otherwise here would only move the seam. What this settles is *when* they +/// are stated and *by whom*. +#[derive(Default)] +pub struct Decompositions { + /// Parameter-side: values built on the Rust side from ingredients that + /// cross separately. + pub expansions: Option, + /// Return/error-side: values delivered as leaves the far side reassembles. + pub deconstructors: Option, + /// By-value struct decompositions whose leaves the adapter computed. + pub value_structs: Vec, + /// The selector-carrying sibling: a tag plus one leaf group per + /// alternative. + pub sums: Vec, + /// Element types of a `Vec`/`&[T]` delivered element-by-element. + pub leaf_vec_elements: Vec, + /// The whole-value crossings these decompositions make unnecessary. + /// + /// Stated **with** the decompositions rather than beside them: a type + /// crosses only in pieces *because* something decomposes it, and once the + /// plans are applied its own direct converter is genuinely not needed — for + /// a type with no destination representation, not even resolvable. + pub replaces: HashSet, +} + +#[cfg(test)] +mod tests; diff --git a/prebindgen/src/api/core/registry/model.rs b/prebindgen/src/api/core/registry/model.rs new file mode 100644 index 00000000..65f72d7a --- /dev/null +++ b/prebindgen/src/api/core/registry/model.rs @@ -0,0 +1,94 @@ +//! Questions about the model, answered through the registry that projects it. + +use std::collections::HashMap; + +use super::{ + view::{default_module_of, origin_module_of}, + *, +}; + +impl Registry { + /// The parameter-side fold for each `(function, parameter)` position. + /// + /// Inherent rather than on [`Conversions`]: a fold is read when a wrapper's + /// parameters are emitted, never while a conversion is being built, so no + /// generic caller needs it. + pub fn expansion_plans( + &self, + ) -> &HashMap<(syn::Ident, syn::Ident), crate::api::core::expand::FoldPlan> { + &self.expansion_plans + } + + /// What the binding declared — read by the emitter's gate. + pub(crate) fn declared(&self) -> &Declared { + &self.declared + } + + /// The parsed model this registry projects. + pub fn flat(&self) -> &crate::api::core::flat::Flat { + &self.flat + } + + /// Every **named** item the model holds — functions, structs, either enum + /// shape, consts — regardless of whether the stream carried an origin stamp. + /// + /// Lives here so an adapter that needs "anything the source crate defines" + /// does not enumerate element kinds itself: a new kind is taught here once + /// instead of drifting in each adapter. An **alias is deliberately absent** + /// — see the arm below — and callers are expected to pair this with + /// `origin_module(..).unwrap_or_else(default_module)`. + pub fn named_item_idents(&self) -> impl Iterator { + use crate::api::core::flat::{Element, Type}; + self.flat.elements().filter_map(|e| match e { + // An `Extern` names a type without declaring a body, and is + // deliberately absent: its caller decides which names to qualify in + // generated Rust, and qualifying an alias would move that output. + Element::Type(Type::Extern(_)) => None, + Element::Function(_) | Element::Type(_) | Element::Constant(_) => e.name(), + Element::Guard(_) | Element::Unsupported(_) => None, + }) + } + + /// Whether the source declares a type under this name — **including an + /// alias**. + /// + /// An alias counts because `#[prebindgen] pub type Handle = ..` *is* a + /// declaration of that name: it can be declared bare by an adapter (landing + /// in the no-indexed-body branch below, which is what + /// `ptr_class(ZKeyExpr<'static>)` relies on), so a diagnostic that says + /// "no such captured item" would be false. + pub(super) fn declares_type(&self, ident: &syn::Ident) -> bool { + self.flat.declared_type(ident).is_some() + } + + /// The origin crate's **module path** for an item, read off the element's + /// own [`SourceLocation`] stamp, or `None` when unknown — callers then fall + /// back to [`Self::default_module`]. + pub fn origin_module(&self, ident: &syn::Ident) -> Option { + // Off the element's own location, which covers both populations: a + // captured item stamped at capture time, and a binding-local fn stamped + // by `add_local_function`. + origin_module_of(&self.flat, ident) + } + + /// The default module for references with no recorded origin: the + /// first-seen item origin. `None` for an origin-less item-level + /// registry (adapters then fall back to `crate`). To change a module + /// name, override it at the source — a stream's origin stamps + /// (`Source::builder(dir).crate_name("myflat")`) — never here: a + /// registry-level override could only fix ONE module, which is + /// incomplete with chained multi-source streams. + pub fn default_module(&self) -> Option { + default_module_of(&self.flat) + } + + /// Module paths of every ingested source, ingestion order — e.g. for a + /// glob import that must see all sources' items. + pub fn all_source_modules(&self) -> Vec { + self.flat + .source_modules() + .iter() + .filter_map(|m| syn::parse_str(m).ok()) + .collect() + } +} diff --git a/prebindgen/src/api/core/registry/order.rs b/prebindgen/src/api/core/registry/order.rs new file mode 100644 index 00000000..e020a8c2 --- /dev/null +++ b/prebindgen/src/api/core/registry/order.rs @@ -0,0 +1,115 @@ +//! Hand the demand over, and grade the answers. +//! +//! The two halves of the exchange with a generator: [`Registry::crossings`] +//! sorts the derived set inner-first, `RegistryBuilder::build` takes every +//! conversion back at once and names whatever is missing. + +use std::collections::HashSet; + +use super::*; + +impl Registry { + /// Every crossing this binding needs a conversion for, **inner types + /// first**. + /// + /// The order is the whole point: a generator walking this list has already + /// built everything a given crossing can compose from, so it can work from + /// a flat list instead of being called back per type. Derived from + /// [`Self::immediate_edges`], which is structural — generic arguments, + /// tuple/reference/slice targets, declared struct fields, and `impl Fn` + /// arguments with the direction flipped — so no generator is consulted to + /// produce it. + /// + /// **Cycles.** A self-referential type (`struct Node { next: + /// Option> }`) has no topological order. The walk breaks such a + /// cycle at its entry, so exactly one member is handed out before an inner + /// it contains; a generator that cannot build it supplies nothing, and + /// `RegistryBuilder::build` reports it like any other gap. + pub(crate) fn crossings(&self) -> Vec { + // Post-order DFS: a node is emitted only after everything it reaches, + // which IS inner-first. `visiting` breaks cycles — the back edge is + // simply not followed, so the node it points at lands later than its + // dependent, and that is the one documented exception above. + let mut order: Vec = Vec::new(); + let mut done: HashSet = HashSet::new(); + let mut visiting: HashSet = HashSet::new(); + + // Deterministic roots: same list every build, so a generator's output + // cannot depend on hash order. + let mut roots: Vec = Vec::new(); + for dir in [Direction::Input, Direction::Output] { + let mut keys: Vec<&TypeKey> = self.type_table(dir).keys().collect(); + keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); + roots.extend(keys.into_iter().map(|k| (dir, k.clone()))); + } + + for root in roots { + self.visit_crossing(root, &mut order, &mut done, &mut visiting); + } + order + } + + pub(super) fn visit_crossing( + &self, + node: Crossing, + order: &mut Vec, + done: &mut HashSet, + visiting: &mut HashSet, + ) { + if done.contains(&node) || !visiting.insert(node.clone()) { + return; + } + let (dir, key) = node.clone(); + let ty = key.to_type(); + let mut edges: Vec = self + .immediate_edges(dir, &ty) + .into_iter() + .chain(self.plan_edges(dir, &ty)) + .chain( + self.declared + .edges + .iter() + .filter(|(from, _)| *from == node) + .map(|(_, on)| (on.0, on.1.to_type())), + ) + .map(|(d, t)| (d, TypeKey::from_type(&t))) + // Only crossings the scan actually registered: a structural edge to + // a type nothing asked for is not a crossing. + .filter(|c| self.type_table(c.0).contains_key(&c.1)) + .collect(); + edges.sort_by(|a, b| (a.0 as u8, a.1.as_str()).cmp(&(b.0 as u8, b.1.as_str()))); + for edge in edges { + self.visit_crossing(edge, order, done, visiting); + } + visiting.remove(&node); + if done.insert(node.clone()) { + order.push(node); + } + } + + /// Dependencies a **decomposition** adds, which the structural walk cannot + /// see. + /// + /// A callback argument delivered as leaves needs each leaf's own conversion + /// before the callback's can be built — and a leaf is named by a plan, not + /// by the argument's syntax. Without this the order would be structurally + /// correct and still wrong, which is exactly the kind of gap the old + /// fixed-point loop papered over by retrying. + pub(super) fn plan_edges(&self, dir: Direction, ty: &syn::Type) -> Vec<(Direction, syn::Type)> { + if dir != Direction::Input { + return Vec::new(); + } + let Some(args) = crate::api::core::flat::extract_fn_trait_args(ty) else { + return Vec::new(); + }; + let mut out = Vec::new(); + for arg in args { + if let Some(plan) = self.callback_arg_plans.get(&TypeKey::from_type(&arg)) { + for leaf in &plan.leaves { + out.push((Direction::Output, leaf.out_ty.clone())); + } + } + } + out + } +} diff --git a/prebindgen/src/api/core/registry/run.rs b/prebindgen/src/api/core/registry/run.rs new file mode 100644 index 00000000..10d47d92 --- /dev/null +++ b/prebindgen/src/api/core/registry/run.rs @@ -0,0 +1,68 @@ +//! Bind a finished registry to the generator that filled it. + +use super::*; + +impl Registry { + pub(super) fn apply_adapter_plans( + &mut self, + declared: &mut Declared, + ) -> Result<(), WriteRustError> { + // The set of declared fns drives `.default()` auto-apply: a defaulted + // constructor/deconstructor is synthesized for every matching declared + // fn. `accessors` is the `.fun_accessor` subset: excluded from + // constructor composition and the only fns a decomposer record may + // reference. + let d = &mut declared.decompositions; + if let Some(exp) = &d.expansions { + crate::api::core::expand::apply( + self, + exp, + &declared.functions, + &declared.accessors, + &declared.method_receivers, + )?; + } + if let Some(dec) = &d.deconstructors { + crate::api::core::unfold::apply(self, dec, &declared.functions, &declared.accessors)?; + } + // Synthesized by-value `data_class` decompositions: the adapter already + // built the leaves; this wires them into fixed-builder plans. + if !d.value_structs.is_empty() { + crate::api::core::unfold::apply_value_structs( + self, + std::mem::take(&mut d.value_structs), + &declared.functions, + )?; + } + // The same wiring for a value whose alternatives are chosen at runtime + // (tag + one leaf group per variant) rather than being a fixed product. + if !d.sums.is_empty() { + crate::api::core::unfold::apply_sum_returns( + self, + std::mem::take(&mut d.sums), + &declared.functions, + )?; + } + // Single-leaf `Vec`/`&[T]` whole-element folds — the dual of the + // `data_class` folds above, for String / scalar / handle elements + // (so the list is built on the foreign side, not via a Rust ArrayList). + if !d.leaf_vec_elements.is_empty() { + crate::api::core::unfold::apply_leaf_vec_folds( + self, + std::mem::take(&mut d.leaf_vec_elements), + &declared.functions, + )?; + } + // Every crossing these types make is now covered by a plan, so the + // scan-time direct converter requirement is stale — and typically + // unresolvable, since such a type has no destination representation. + // Drop it both ways; the cell stays, so a converter is still produced + // if one happens to resolve. + for key in &declared.decompositions.replaces { + let ty = key.to_type(); + self.unrequire_input(&ty); + self.unrequire_output(&ty); + } + Ok(()) + } +} diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs new file mode 100644 index 00000000..481c5626 --- /dev/null +++ b/prebindgen/src/api/core/registry/scan.rs @@ -0,0 +1,388 @@ +//! Derive the crossing set: walk what was declared, and register every type +//! position it reaches. +//! +//! Deliberately over-approximating — every nested position, every declared +//! struct in both directions. What must actually convert is reachability from +//! the roots, which `order` decides once the graph is complete. + +use std::collections::{HashMap, HashSet}; + +use quote::ToTokens; + +use super::*; + +impl Registry { + pub(super) fn scan_declared_items(&mut self, declared: &Declared) -> Result<(), ScanError> { + // Source-qualified declared types are a hard error (issue #95). The + // key's own normalization already reduced `crate::`/`self::` and std + // prelude spellings, so a remaining multi-segment declared path + // either qualifies a SOURCE item with its crate name (can never + // match — the flat namespace keys are bare) or names a genuinely + // foreign type (supported verbatim; warned about below only when it + // shadows a captured item's name — the likely-mistake heuristic). + let mut qualified: Vec<(String, String)> = Vec::new(); + let mut probed: HashSet<&TypeKey> = HashSet::new(); + for key in declared + .types + .iter() + .chain(declared.decompositions.replaces.iter()) + { + if !probed.insert(key) { + continue; + } + let ty = key.to_type(); + // Peel one reference level; the qualified head only appears on + // path types. + let inner = match &ty { + syn::Type::Reference(r) => &*r.elem, + other => other, + }; + let syn::Type::Path(tp) = inner else { continue }; + if tp.qself.is_some() || tp.path.segments.len() < 2 { + continue; + } + let head = tp + .path + .segments + .first() + .expect("len checked") + .ident + .to_string(); + let last = tp.path.segments.last().expect("len checked"); + if self.flat.source_modules().contains(&head) { + qualified.push((key.to_string(), last.to_token_stream().to_string())); + } else if self.declares_type(&last.ident) { + println!( + "cargo:warning=prebindgen: declared type `{}` is path-qualified, but a \ + captured #[prebindgen] item `{}` exists — if you meant the source item, \ + declare it by its bare name", + key, last.ident + ); + } + } + if !qualified.is_empty() { + qualified.sort(); + return Err(ScanError::QualifiedDeclaredTypes { entries: qualified }); + } + + // Declared-but-missing items are collected across all three loops and + // reported together as one hard error (see + // [`ScanError::DeclaredNotFound`]). + let mut missing: Vec<(&'static str, String)> = Vec::new(); + + // Scan declared functions. + for ident in &declared.functions { + if let Some(item_fn) = self.flat.function(&ident).map(|f| f.origin.syntax.clone()) { + self.scan_fn_signature(&item_fn)?; + } else { + missing.push(("function", ident.to_string())); + } + } + + // Helper functions: never emitted, no blanket signature scan (the + // adapter registers the specific requirements via + // `extra_required_types`) — but they are referenced by name from + // adapter declarations, so a missing one is a hard error. + for ident in &declared.helper_functions { + if self.flat.function(&ident).is_none() { + missing.push(("helper function", ident.to_string())); + } + } + + // Scan declared consts: a const is a nullary source of its type, so + // the type is required in the output direction only. + for ident in declared.consts.iter().flatten() { + if let Some(item_const) = self.flat.constant(&ident).map(|c| c.origin.syntax.clone()) { + self.ensure_entry(Direction::Output, &item_const.ty, true); + } else { + missing.push(("constant", ident.to_string())); + } + } + + if !missing.is_empty() { + missing.sort(); + return Err(ScanError::DeclaredNotFound { entries: missing }); + } + + // Declared crossings with no element behind them (a foreign class type, + // a synthesized constant's value type), each in its own direction. + for (dir, ty) in &declared.crossings { + self.ensure_entry(*dir, ty, true); + } + + // Scan declared types. + for key in &declared.types { + let ty = key.to_type(); + let mut matched = false; + if let Some(ident) = bare_path_ident(&ty) { + if let Some(s) = self + .flat + .struct_type(&ident) + .map(|s| s.origin.syntax.clone()) + { + self.scan_struct(&s)?; + self.ensure_entry(Direction::Input, &ty, true); + self.ensure_entry(Direction::Output, &ty, true); + matched = true; + } else if let Some(e) = self.flat.enum_item(&ident).cloned() { + self.scan_enum(&e)?; + self.ensure_entry(Direction::Input, &ty, true); + self.ensure_entry(Direction::Output, &ty, true); + matched = true; + } + } + if !matched { + // Declared type without an indexed body (e.g. + // `ptr_class(ZKeyExpr<'static>)` on a re-exported + // foreign type). Still mark required so the resolver + // tries to produce a converter for it. + self.ensure_entry(Direction::Input, &ty, true); + self.ensure_entry(Direction::Output, &ty, true); + } + } + + Ok(()) + } + + pub(super) fn scan_fn_signature(&mut self, f: &syn::ItemFn) -> Result<(), ScanError> { + // Mechanical: register every fn-signature type as the user wrote it. + // No semantic transformations (no &T→T strip, no ZResult→T strip, + // no skip for () / ZResult<()>). The adapter handles structural + // wrappers; propagation through `subs` then marks transitive deps + // (e.g. &Foo's `&_` converter returns subs=[Foo], so Foo becomes + // required). + // No receiver or non-ident pattern can reach here: a captured item was + // refused by the frontend and `from_flat` failed before indexing it, and + // a binding-local fn was checked against the same grammar + // (`Flat::lower_signature`) when `resolve` synthesized it. + for input in &f.sig.inputs { + match input { + syn::FnArg::Receiver(_) => continue, + syn::FnArg::Typed(pt) => { + self.register_type_recursive(Direction::Input, &pt.ty, true)?; + } + } + } + let ret_ty: syn::Type = match &f.sig.output { + syn::ReturnType::Default => syn::parse_quote!(()), + syn::ReturnType::Type(_, ty) => (**ty).clone(), + }; + self.register_type_recursive(Direction::Output, &ret_ty, true)?; + Ok(()) + } + + pub(super) fn scan_struct(&mut self, s: &syn::ItemStruct) -> Result<(), ScanError> { + // The struct itself can appear in either direction. + let ty: syn::Type = crate::api::core::types_util::type_from_ident(&s.ident); + self.ensure_entry(Direction::Input, &ty, false); + self.ensure_entry(Direction::Output, &ty, false); + + if let syn::Fields::Named(named) = &s.fields { + for field in &named.named { + self.register_type_recursive(Direction::Input, &field.ty, false)?; + self.register_type_recursive(Direction::Output, &field.ty, false)?; + } + } + Ok(()) + } + + pub(super) fn scan_enum(&mut self, e: &syn::ItemEnum) -> Result<(), ScanError> { + let ty: syn::Type = crate::api::core::types_util::type_from_ident(&e.ident); + self.ensure_entry(Direction::Input, &ty, false); + self.ensure_entry(Direction::Output, &ty, false); + + for variant in &e.variants { + for field in &variant.fields { + self.register_type_recursive(Direction::Input, &field.ty, false)?; + self.register_type_recursive(Direction::Output, &field.ty, false)?; + } + } + Ok(()) + } + + /// Register `ty` as a cell in the given direction, then recurse into every + /// nested position. `root` applies only to `ty` itself — a nested position is + /// never something the binding asked for directly. + pub(super) fn register_type_recursive( + &mut self, + dir: Direction, + ty: &syn::Type, + root: bool, + ) -> Result<(), ScanError> { + let mut visited: HashSet = HashSet::new(); + self.register_type_inner(dir, ty, root, &mut visited) + } + + pub(super) fn register_type_inner( + &mut self, + dir: Direction, + ty: &syn::Type, + is_top: bool, + visited: &mut HashSet, + ) -> Result<(), ScanError> { + // A disallowed `impl Trait` cannot reach here: every fn whose signature + // reaches this point passed the frontend's grammar — captured items at + // ingestion, binding-local ones at synthesis — and it names the + // parameter the bad type sits on. + + let key = TypeKey::from_type(ty); + if !visited.insert(key.clone()) { + return Ok(()); // cycle guard + } + + self.ensure_entry(dir, ty, is_top); + + for (child_dir, sub) in self.immediate_edges(dir, ty) { + self.register_type_inner(child_dir, &sub, false, visited)?; + } + Ok(()) + } + + /// Create the cell for `ty` in `dir` if it has none, and mark it a root when + /// the binding asked for it directly. + /// + /// The one place a cell is born, which is what lets the subject be decided + /// once: the model's reading if the flat API mentions this type, an + /// adapter-authored type otherwise. + pub(super) fn ensure_entry(&mut self, dir: Direction, ty: &syn::Type, root: bool) { + let key = TypeKey::from_type(ty); + let subject = match self.flat.type_ref(ty) { + Some(t) => TypeSubject::Source(Box::new(t.clone())), + None => TypeSubject::Adapter, + }; + let cell = self + .type_table_mut(dir) + .entry(key) + .or_insert_with(|| TypeCell { + subject, + root: false, + entry: None, + }); + cell.root |= root; + } + + /// Enumerate the immediate type-graph edges out of `(dir, ty)`: + /// generic args / Fn args / tuple elements / ref/array/slice/ptr targets, + /// plus — if `ty` is the bare ident of an indexed struct or enum — the + /// field types of that struct/enum. + /// + /// `impl Fn(args)` arg types flow with `dir.flip()`; everything else + /// inherits `dir`. Used by both `register_type_inner` (during scan) and + /// the unresolved-descendants BFS in `resolve` (for diagnostics). + pub(crate) fn immediate_edges( + &self, + dir: Direction, + ty: &syn::Type, + ) -> Vec<(Direction, syn::Type)> { + let mut out: Vec<(Direction, syn::Type)> = Vec::new(); + let (positions, child_dir) = if let Some(args) = extract_fn_trait_args(ty) { + (args, dir.flip()) + } else { + (immediate_subtype_positions(ty), dir) + }; + for sub in positions { + out.push((child_dir, sub)); + } + // A declared type's own fields, read off the element rather than off its + // `syn::Fields`: a positional field is an ordinary `Field` there, so the + // named-only asymmetry the syntax walk had does not arise. An `Enum` has + // no fields and an `Extern` declares none, which is what makes both + // contribute nothing here. + if let Some(name) = bare_path_ident(ty) { + use crate::api::core::flat::{Field, Type}; + let fields: Vec<&Field> = match self.flat.declared_type(&name) { + Some(Type::Struct(s)) => s.fields.iter().collect(), + Some(Type::Variant(v)) => v + .alternatives + .iter() + .flat_map(|a| a.fields.iter()) + .collect(), + Some(Type::Enum(_) | Type::Extern(_)) | None => Vec::new(), + }; + for field in fields { + out.push((dir, field.ty.origin.syntax.clone())); + } + } + out + } + + /// Register `ty` (and its nested positions) as a required **input** so + /// the resolver produces a converter for it. Used by + /// [`crate::api::core::expand`] to pull in the leaf types a fold needs. + pub(crate) fn require_input(&mut self, ty: &syn::Type) { + // Leaf/expansion types are concrete (no disallowed `impl Trait`), so + // the recursive registration cannot fail here. + let _ = self.register_type_recursive(Direction::Input, ty, true); + } + + /// Register `ty` (and its nested positions) as a required **output** so the + /// resolver produces a converter for it. The output-side peer of + /// [`Self::require_input`]; used by [`crate::api::core::unfold`] to pull in + /// the leaf types a decomposition delivers. + pub(crate) fn require_output(&mut self, ty: &syn::Type) { + let _ = self.register_type_recursive(Direction::Output, ty, true); + } + + /// Drop `ty` from the required-output scan set. The type's table entry is + /// left intact (so [`crate::api::core::resolve`]'s PASS A still resolves it + /// if it can, and emits it when resolved), but a `None` resolution no longer + /// counts as an unresolved-required error. Used by + /// [`crate::api::core::unfold::apply_leaf_vec_folds`]: when a `Vec` / + /// `Option>` return is delivered element-by-element through a fold, + /// the whole-collection converter is genuinely not needed — and for a + /// `Vec` it cannot resolve at all (a `jlong` wire is not + /// JObject-shaped), so requiring it would wrongly fail resolution. + pub(crate) fn unrequire_output(&mut self, ty: &syn::Type) { + self.clear_root(Direction::Output, ty); + } + + /// Drop `ty` from the required-input scan set — the input-side peer of + /// [`Self::unrequire_output`]. Used by [`Self::apply_adapter_plans`] for + /// the adapter's boundary-only types: a fold plan replaces every direct + /// crossing of the type with its ingredients, so the type's own input + /// converter is genuinely not needed (and for an undeclared type cannot + /// resolve at all). + pub(crate) fn unrequire_input(&mut self, ty: &syn::Type) { + self.clear_root(Direction::Input, ty); + } + + /// Stop treating `ty` as a root. The cell stays, so the resolver still fills + /// it if it can — only the demand that it *must* resolve is dropped. + pub(super) fn clear_root(&mut self, dir: Direction, ty: &syn::Type) { + let key = TypeKey::from_type(ty); + if let Some(cell) = self.type_table_mut(dir).get_mut(&key) { + cell.root = false; + } + } + + /// Direction-indexed read access to the type-resolution tables. + pub(crate) fn type_table(&self, dir: Direction) -> &HashMap> { + match dir { + Direction::Input => &self.input_types, + Direction::Output => &self.output_types, + } + } + + /// Direction-indexed mutable access to the type-resolution tables. + pub(crate) fn type_table_mut(&mut self, dir: Direction) -> &mut HashMap> { + match dir { + Direction::Input => &mut self.input_types, + Direction::Output => &mut self.output_types, + } + } + + /// Look up the resolved input entry for `ty`, returning `None` if it + /// was never registered or is still unresolved. The returned entry's + /// `function.sig.ident` is the converter's call name; `destination` is + /// its wire form. + pub fn input_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { + let key = TypeKey::from_type(ty); + self.type_table(Direction::Input).get(&key)?.entry.as_ref() + } + + /// Look up the resolved output entry for `ty`. See [`Self::input_entry`]. + pub fn output_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { + let key = TypeKey::from_type(ty); + self.type_table(Direction::Output).get(&key)?.entry.as_ref() + } +} diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 79794b60..0c15aff7 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -1,56 +1,99 @@ use std::collections::HashSet; use proc_macro2::TokenStream; +use quote::ToTokens; use super::*; use crate::api::core::{ + flat::Flat, niches::Niches, prebindgen::{ConverterImpl, Prebindgen}, + registry::RegistryBuilder, }; +/// Push-then-resolve, the way a real generator's `resolve` does. Test-only: +/// production generators own this pairing themselves (`JniGenBuilder::resolve`). +trait DeclareAndResolve { + fn declare_and_resolve(self, ext: E) -> Result, WriteRustError> + where + E: Prebindgen + AsStub; +} + +/// How a test stub states what it declares, and what it can convert. +trait AsStub { + fn stub(&self) -> &StubExt; + /// Default: converts nothing, so every required crossing is a gap. + fn converter(&self, _ty: &syn::Type) -> Option> { + None + } +} +impl AsStub for StubExt { + fn stub(&self) -> &StubExt { + self + } +} + +impl DeclareAndResolve<()> for RegistryBuilder<()> { + fn declare_and_resolve(self, ext: E) -> Result, WriteRustError> + where + E: Prebindgen + AsStub, + { + let registry = ext + .stub() + .declare_into_any(self)? + .validate_with(&ext)? + .convert_with(|crossing, _built| ext.converter(&crossing.1.to_type()))? + .build()?; + ext.validate_resolved(®istry) + .map_err(|message| ScanError::AdapterInvariant { message })?; + Ok(registry) + } +} + /// Minimal `Prebindgen` for scan-pipeline tests. Carries the /// declared sets the test wants and stubs every emission/converter /// hook into something inert. #[derive(Default)] struct StubExt { functions: HashSet, - ignored_functions: HashSet, - ignored_name_predicates: Vec, helper_functions: HashSet, consts: Option>, types: HashSet, - ignored_types: HashSet, local_fns: Vec<(syn::ItemFn, String)>, } +impl StubExt { + /// Push what this stub declares, the way a real generator does. Generic + /// over `M` so a stub can configure any adapter's registry. + fn declare_into_any( + &self, + mut reg: RegistryBuilder, + ) -> Result, ScanError> { + for (item_fn, origin) in self.local_fns.clone() { + reg = reg.local_function(item_fn, origin)?; + } + for i in &self.functions { + reg = reg.export(i); + } + for i in &self.helper_functions { + reg = reg.reference(i); + } + if let Some(consts) = &self.consts { + reg = reg.declares_consts(); + for i in consts { + reg = reg.export_const(i); + } + } + for k in &self.types { + reg = reg.export_type(k.clone()); + } + Ok(reg) + } +} + impl Prebindgen for StubExt { type Metadata = (); - fn declared_functions(&self) -> HashSet { - self.functions.clone() - } - fn ignored_functions(&self) -> HashSet { - self.ignored_functions.clone() - } - fn ignored_name_predicates(&self) -> Vec { - self.ignored_name_predicates.clone() - } - fn helper_functions(&self) -> HashSet { - self.helper_functions.clone() - } - fn declared_consts(&self) -> Option> { - self.consts.clone() - } - fn declared_types(&self) -> HashSet { - self.types.clone() - } - fn ignored_types(&self) -> HashSet { - self.ignored_types.clone() - } - fn local_functions(&self) -> Vec<(syn::ItemFn, String)> { - self.local_fns.clone() - } - fn on_function(&self, _f: &syn::ItemFn, _registry: &Registry<()>) -> TokenStream { TokenStream::new() } @@ -60,20 +103,6 @@ impl Prebindgen for StubExt { fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry<()>) -> TokenStream { TokenStream::new() } - fn on_input_type( - &self, - _ty: &syn::Type, - _registry: &Registry<()>, - ) -> Option> { - None - } - fn on_output_type( - &self, - _ty: &syn::Type, - _registry: &Registry<()>, - ) -> Option> { - None - } } // suppress unused warning on Niches — kept available for richer tests @@ -90,9 +119,13 @@ fn fn_item(src: &str) -> (syn::Item, SourceLocation) { #[test] fn scan_declared_empty_ext_marks_nothing_required() { let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let ext = StubExt::default(); - reg.scan_declared(&ext).expect("empty ext = no scan"); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .expect("empty ext = no scan"); assert!(!reg.input_types.values().any(|c| c.root)); assert!(!reg.output_types.values().any(|c| c.root)); } @@ -103,10 +136,14 @@ fn scan_declared_marks_types_required_only_for_declared_fns() { fn_item("fn a(x: u64) -> u64 { x }"), fn_item("fn b(x: u32) -> u32 { x }"), ]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("a").unwrap()); - reg.scan_declared(&ext).unwrap(); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .unwrap(); let is_root = |t: &HashMap>, k: &str| { t.get(&TypeKey::parse(k).expect("test type")) .is_some_and(|c| c.root) @@ -117,51 +154,21 @@ fn scan_declared_marks_types_required_only_for_declared_fns() { assert!(!is_root(®.output_types, "u32")); } -#[test] -fn scan_declared_rejects_function_declared_and_ignored_overlap() { - let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); - let ident: syn::Ident = syn::parse_str("good").unwrap(); - let mut ext = StubExt::default(); - ext.functions.insert(ident.clone()); - ext.ignored_functions.insert(ident.clone()); - - match reg.scan_declared(&ext) { - Err(ScanError::ConflictingFunctionIntent { name }) if name == ident => (), - other => panic!("expected ConflictingFunctionIntent, got {:?}", other), - } -} - -#[test] -fn scan_declared_rejects_type_declared_and_ignored_overlap() { - let item: syn::ItemStruct = syn::parse_str("struct Thing { value: u64 }").unwrap(); - let items = vec![(syn::Item::Struct(item), SourceLocation::default())]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); - let key = TypeKey::parse("Thing").expect("test type"); - let mut ext = StubExt::default(); - ext.types.insert(key.clone()); - ext.ignored_types.insert(key.clone()); - - match reg.scan_declared(&ext) { - Err(ScanError::ConflictingTypeIntent { key: actual }) if actual == key => (), - other => panic!("expected ConflictingTypeIntent, got {:?}", other), - } -} - /// A declared function that matches no indexed item is a hard error, not a /// warning — explicit intent gone wrong (I7). #[test] fn scan_declared_missing_function_is_hard_error() { let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("good").unwrap()); ext.functions.insert(syn::parse_str("typo_fn").unwrap()); - match reg.scan_declared(&ext) { + match ext.declare_into_any(reg).expect("declare").scanned() { Err(ScanError::DeclaredNotFound { entries }) => { assert_eq!(entries, vec![("function", "typo_fn".to_string())]); } - other => panic!("expected DeclaredNotFound, got {:?}", other), + Ok(_) => panic!("expected DeclaredNotFound, scan succeeded"), + Err(other) => panic!("expected DeclaredNotFound, got {other:?}"), } } @@ -170,13 +177,13 @@ fn scan_declared_missing_function_is_hard_error() { #[test] fn scan_declared_collects_all_missing_kinds_in_one_error() { let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("typo_fn").unwrap()); ext.helper_functions .insert(syn::parse_str("typo_helper").unwrap()); ext.consts = Some(HashSet::from([syn::parse_str("TYPO_CONST").unwrap()])); - match reg.scan_declared(&ext) { + match ext.declare_into_any(reg).expect("declare").scanned() { Err(ScanError::DeclaredNotFound { entries }) => { assert_eq!( entries, @@ -190,55 +197,11 @@ fn scan_declared_collects_all_missing_kinds_in_one_error() { let msg = ScanError::DeclaredNotFound { entries }.to_string(); assert!(msg.contains("typo_fn") && msg.contains("TYPO_CONST")); } - other => panic!("expected DeclaredNotFound, got {:?}", other), + Ok(_) => panic!("expected DeclaredNotFound, scan succeeded"), + Err(other) => panic!("expected DeclaredNotFound, got {other:?}"), } } -/// A stale *ignore* entry stays a warning: the scan succeeds. -#[test] -fn scan_declared_missing_ignore_is_not_an_error() { - let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); - let mut ext = StubExt::default(); - ext.ignored_functions - .insert(syn::parse_str("gone_fn").unwrap()); - reg.scan_declared(&ext) - .expect("stale ignore must only warn"); -} - -/// An ignore predicate acknowledges matching undeclared items of EVERY -/// kind — fn, struct/enum, const (one flat namespace, so a name filter -/// needs no kind) — and is silent when it matches nothing: a filter, not a -/// claim. -#[test] -fn scan_declared_accepts_ignore_predicates() { - let s: syn::ItemStruct = syn::parse_str("struct HelperThing { v: u64 }").unwrap(); - let c: syn::ItemConst = syn::parse_str("const HELPER_MAX: u64 = 1;").unwrap(); - let items = vec![ - fn_item("fn helper_a(x: u64) -> u64 { x }"), - fn_item("fn helper_b(x: u64) -> u64 { x }"), - (syn::Item::Struct(s), SourceLocation::default()), - (syn::Item::Const(c), SourceLocation::default()), - ]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); - // Const skip-warnings only run for adapters WITH a const mechanism. - let mut ext = StubExt { - consts: Some(HashSet::new()), - ..StubExt::default() - }; - ext.ignored_name_predicates - .push(std::sync::Arc::new(|n: &str| { - let l = n.to_lowercase(); - l.starts_with("helper") - })); - // A second, zero-match predicate is fine too. - ext.ignored_name_predicates - .push(std::sync::Arc::new(|n: &str| n.starts_with("nothing_"))); - reg.scan_declared(&ext).expect("predicates must scan clean"); - // Nothing was declared, so nothing became a root. - assert!(!reg.input_types.values().any(|c| c.root)); -} - #[test] fn type_entry_helpers_expose_converter_chain_contract() { let entry = TypeEntry { @@ -317,7 +280,7 @@ fn type_entry_helpers_expose_converter_chain_contract() { /// judgement now, and its diagnosis is richer: it names the parameter. #[test] fn from_items_rejects_what_the_language_cannot_express() { - let err = match Registry::<()>::from_items(vec![ + let err = match crate::api::test_util::reg_from_items::<(), _>(vec![ fn_item("fn bogus(x: u64) -> impl std::fmt::Debug { 0u64 }"), fn_item("fn worse(self) -> u64 { 0 }"), ]) { @@ -366,10 +329,11 @@ fn duplicate_name_across_sources_names_both_crates() { let a = make_source("first-crate"); let b = make_source("second-crate"); - let msg = match Registry::<()>::from_items(a.items_all().chain(b.items_all())) { - Ok(_) => panic!("collision must fail"), - Err(e) => e.to_string(), - }; + let msg = + match crate::api::test_util::reg_from_items::<(), _>(a.items_all().chain(b.items_all())) { + Ok(_) => panic!("collision must fail"), + Err(e) => e.to_string(), + }; assert!(msg.contains("same_name"), "{msg}"); assert!(msg.contains("first-crate"), "{msg}"); assert!(msg.contains("second-crate"), "{msg}"); @@ -390,7 +354,8 @@ fn from_items_records_origins_from_location_stamps() { let f_b: syn::ItemFn = syn::parse_str("fn from_helper(x: u64) -> u64 { x }").unwrap(); let a = vec![(syn::Item::Fn(f_a), loc("flat-crate"))]; let b = vec![(syn::Item::Fn(f_b), loc("helper-crate"))]; - let reg: Registry<()> = Registry::from_items(a.into_iter().chain(b)).unwrap(); + let reg: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(a.into_iter().chain(b)).unwrap(); let path = |p: syn::Path| p.to_token_stream().to_string(); assert_eq!( @@ -423,9 +388,14 @@ fn from_items_records_origins_from_location_stamps() { #[test] fn resolve_surfaces_adapter_invariant_errors() { struct FailingExt(StubExt); + impl AsStub for FailingExt { + fn stub(&self) -> &StubExt { + &self.0 + } + } impl Prebindgen for FailingExt { type Metadata = (); - fn validate(&self, _registry: &Registry<()>) -> Result<(), String> { + fn validate(&self, _binding: &Building<'_, ()>) -> Result<(), String> { Err("member fun `f` has no receiver".to_string()) } fn on_function(&self, f: &syn::ItemFn, r: &Registry<()>) -> TokenStream { @@ -437,17 +407,11 @@ fn resolve_surfaces_adapter_invariant_errors() { fn on_enum(&self, e: &syn::ItemEnum, r: &Registry<()>) -> TokenStream { self.0.on_enum(e, r) } - fn on_input_type(&self, t: &syn::Type, r: &Registry<()>) -> Option> { - self.0.on_input_type(t, r) - } - fn on_output_type(&self, t: &syn::Type, r: &Registry<()>) -> Option> { - self.0.on_output_type(t, r) - } } let items = vec![fn_item("fn good(x: u64) -> u64 { x }")]; - let reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let err = reg - .resolve(FailingExt(StubExt::default())) + .declare_and_resolve(FailingExt(StubExt::default())) .expect_err("validate Err must abort resolve"); let msg = format!("{err}"); assert!(msg.contains("member fun `f` has no receiver"), "{msg}"); @@ -516,12 +480,16 @@ fn qualified_signature_matches_bare_declaration() { (syn::Item::Struct(s), crate_loc("myflat")), (syn::Item::Fn(f), crate_loc("myflat")), ]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("get").unwrap()); ext.types .insert(TypeKey::parse("Thing").expect("test type")); - reg.scan_declared(&ext).unwrap(); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .unwrap(); assert!(reg.input_types[&TypeKey::parse("&Thing").expect("test type")].root); assert!(reg.output_types[&TypeKey::parse("Vec").expect("test type")].root); // No spelling-variant duplicate cells survive anywhere. @@ -546,14 +514,18 @@ fn multi_source_rename_cross_reference_normalizes() { (syn::Item::Struct(b_ty), crate_loc("cov-helpers")), (syn::Item::Struct(a_ty), crate_loc("srca")), ]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("use_a").unwrap()); ext.types .insert(TypeKey::parse("TypeA").expect("test type")); ext.types .insert(TypeKey::parse("TypeB").expect("test type")); - reg.scan_declared(&ext).unwrap(); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .unwrap(); assert!(reg.input_types[&TypeKey::parse("&TypeA").expect("test type")].root); assert!(reg.output_types[&TypeKey::parse("TypeB").expect("test type")].root); } @@ -565,11 +537,11 @@ fn qualified_declared_type_is_hard_error() { // a collected hard error with the bare fix-it, not a silent miss. let s: syn::ItemStruct = syn::parse_str("pub struct Thing { pub v: u64 }").unwrap(); let items = vec![(syn::Item::Struct(s), crate_loc("myflat"))]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.types .insert(TypeKey::parse("myflat::Thing").expect("test type")); - match reg.scan_declared(&ext) { + match ext.declare_into_any(reg).expect("declare").scanned() { Err(ScanError::QualifiedDeclaredTypes { entries }) => { assert_eq!(entries.len(), 1); assert_eq!(entries[0].0, "myflat :: Thing"); @@ -577,7 +549,8 @@ fn qualified_declared_type_is_hard_error() { let msg = ScanError::QualifiedDeclaredTypes { entries }.to_string(); assert!(msg.contains("declare it as `Thing`"), "{msg}"); } - other => panic!("expected QualifiedDeclaredTypes, got {:?}", other), + Ok(_) => panic!("expected QualifiedDeclaredTypes, scan succeeded"), + Err(other) => panic!("expected QualifiedDeclaredTypes, got {other:?}"), } } @@ -587,11 +560,14 @@ fn foreign_qualified_declared_type_stays_supported() { // module, so the declaration passes through verbatim and is marked // required under its own spelling (the no-indexed-body arm). let items = vec![fn_item("fn touch(x: u64) -> u64 { x }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); let foreign = TypeKey::parse("zenoh::KeyExpr<'static>").expect("test type"); ext.types.insert(foreign.clone()); - reg.scan_declared(&ext) + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() .expect("foreign qualified declaration is supported"); assert!(reg.input_types[&foreign].root); assert!(reg.output_types[&foreign].root); @@ -643,7 +619,8 @@ fn fn_ident(name: &str) -> syn::Ident { #[test] fn builder_reads_a_source_directory() { let dir = write_source_dir("plain", "flat-crate", "marked_fn"); - let registry: Registry<()> = Registry::builder().source(&dir).build().expect("indexes"); + let registry: RegistryBuilder<()> = + Registry::builder(Flat::builder().source(&dir).build().expect("parses")).expect("indexes"); assert!(registry .flat() @@ -668,16 +645,20 @@ fn builder_source_named_overrides_the_captured_crate() { let dir = write_source_dir("renamed", "real-package-name", "helper_fn"); // Without the override, the registry believes the package name. - let plain: Registry<()> = Registry::builder().source(&dir).build().expect("indexes"); + let plain: RegistryBuilder<()> = + Registry::builder(Flat::builder().source(&dir).build().expect("parses")).expect("indexes"); assert_eq!( plain.origin_module(&fn_ident("helper_fn")).map(module), Some("real_package_name".to_string()) ); - let renamed: Registry<()> = Registry::builder() - .source_named(&dir, "as_renamed") - .build() - .expect("indexes"); + let renamed: RegistryBuilder<()> = Registry::builder( + Flat::builder() + .source_named(&dir, "as_renamed") + .build() + .expect("parses"), + ) + .expect("indexes"); assert_eq!( renamed.origin_module(&fn_ident("helper_fn")).map(module), Some("as_renamed".to_string()) @@ -696,19 +677,22 @@ fn builder_composes_directories_and_streams() { let flat = write_source_dir("multi_flat", "flat-crate", "flat_fn"); let helper = write_source_dir("multi_helper", "real-helper-name", "helper_fn"); - let registry: Registry<()> = Registry::builder() - .source(&flat) - .source_named(&helper, "renamed_helper") - .items(vec![( - syn::Item::Fn(syn::parse_quote!( - pub fn synthetic() -> i32 { - 2 - } - )), - crate::SourceLocation::default(), - )]) - .build() - .expect("indexes"); + let registry: RegistryBuilder<()> = Registry::builder( + Flat::builder() + .source(&flat) + .source_named(&helper, "renamed_helper") + .items(vec![( + syn::Item::Fn(syn::parse_quote!( + pub fn synthetic() -> i32 { + 2 + } + )), + crate::SourceLocation::default(), + )]) + .build() + .expect("parses"), + ) + .expect("indexes"); for name in ["flat_fn", "helper_fn", "synthetic"] { assert!( @@ -750,9 +734,11 @@ fn builder_composes_directories_and_streams() { fn builder_and_from_items_agree() { let dir = write_source_dir("agree", "flat-crate", "marked_fn"); - let built: Registry<()> = Registry::builder().source(&dir).build().expect("indexes"); - let streamed: Registry<()> = - Registry::from_items(crate::Source::new(&dir).items_all()).expect("indexes"); + let built: RegistryBuilder<()> = + Registry::builder(Flat::builder().source(&dir).build().expect("parses")).expect("indexes"); + let streamed: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(crate::Source::new(&dir).items_all()) + .expect("indexes"); assert_eq!( built.flat().functions().count(), @@ -784,11 +770,16 @@ fn a_source_type_cell_carries_the_models_typeref() { crate_name: Some("myflat".into()), }; let item: syn::Item = syn::parse_str("pub fn f(v: Option) -> u64 { v.unwrap() }").unwrap(); - let mut reg: Registry<()> = Registry::from_items([(item, loc.clone())]).unwrap(); + let reg: RegistryBuilder<()> = + crate::api::test_util::reg_from_items([(item, loc.clone())]).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("f").unwrap()); - reg.scan_declared(&ext).unwrap(); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .unwrap(); let key = TypeKey::parse("Option").expect("test type"); let cell = ®.input_types[&key]; @@ -812,16 +803,20 @@ fn a_source_type_cell_carries_the_models_typeref() { #[test] fn an_adapter_authored_type_cell_has_no_source_reading() { let items = vec![fn_item("fn f(x: u64) -> u64 { x }")]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.types .insert(TypeKey::parse("Foreign").expect("test type")); - reg.scan_declared(&ext).unwrap(); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .unwrap(); let cell = ®.input_types[&TypeKey::parse("Foreign").expect("test type")]; assert!(cell.root, "the binding asked for it directly"); - assert!(matches!(cell.subject, TypeSubject::Adapter(_))); + assert!(matches!(cell.subject, TypeSubject::Adapter)); assert!(cell.subject.kind().is_none()); assert_eq!(cell.subject.location(), None); } @@ -912,7 +907,7 @@ fn from_flat_projects_each_element_kind() { .items(items) .build() .expect("parse"); - let reg: Registry<()> = Registry::from_flat(flat).expect("project"); + let reg: RegistryBuilder<()> = Registry::builder(flat).expect("project"); let id = |n: &str| syn::parse_str::(n).unwrap(); assert!(reg.flat().function(&id("f").to_string()).is_some()); @@ -982,7 +977,7 @@ fn not_expressible_report_names_the_crate_of_each_offender() { at("helpers"), ), ]; - let Err(err) = Registry::<()>::from_items(items) else { + let Err(err) = crate::api::test_util::reg_from_items::<(), _>(items) else { panic!("both items are inexpressible") }; let msg = err.to_string(); @@ -1014,14 +1009,15 @@ fn a_binding_local_fn_is_checked_against_the_grammar() { ("fn takes_impl(x: impl std::fmt::Debug) {}", "impl Trait"), ("async fn is_async() {}", "async"), ] { - let reg: Registry<()> = - Registry::from_items(vec![fn_item("fn good(x: u64) -> u64 { x }")]).unwrap(); + let reg: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(vec![fn_item("fn good(x: u64) -> u64 { x }")]) + .unwrap(); let ext = StubExt { local_fns: vec![(syn::parse_str(src).expect("parse local fn"), "b".into())], ..Default::default() }; let err = reg - .resolve(ext) + .declare_and_resolve(ext) .expect_err(&format!("`{src}` must be refused")); let msg = err.to_string(); assert!( @@ -1040,8 +1036,9 @@ fn a_binding_local_fn_is_checked_against_the_grammar() { /// declared, which a binding-local fn legitimately may not be. #[test] fn a_well_formed_binding_local_fn_passes() { - let reg: Registry<()> = - Registry::from_items(vec![fn_item("fn good(x: u64) -> u64 { x }")]).unwrap(); + let reg: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(vec![fn_item("fn good(x: u64) -> u64 { x }")]) + .unwrap(); let ext = StubExt { local_fns: vec![( syn::parse_str("fn helper(s: &Undeclared) -> u64 { 0 }").expect("parse"), @@ -1049,7 +1046,7 @@ fn a_well_formed_binding_local_fn_passes() { )], ..Default::default() }; - reg.resolve(ext) + reg.declare_and_resolve(ext) .expect("a grammatical local fn passes, undeclared types and all"); } @@ -1085,7 +1082,7 @@ fn a_guard_never_reaches_the_const_surface() { loc.clone(), ), ]; - let mut reg: Registry<()> = Registry::from_items(items).unwrap(); + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); assert_eq!(reg.flat().guards().count(), 2); assert_eq!(reg.flat().constants().count(), 1); @@ -1097,7 +1094,10 @@ fn a_guard_never_reaches_the_const_surface() { consts: Some(HashSet::new()), ..Default::default() }; - reg.scan_declared(&ext).expect("guards are not declarable"); + ext.declare_into_any(reg) + .expect("declare") + .scanned() + .expect("guards are not declarable"); } // ── One index: what the deleted maps used to guarantee ───────────────── @@ -1110,7 +1110,7 @@ fn a_guard_never_reaches_the_const_surface() { /// one would move generated output. This is the assertion that keeps the filter. #[test] fn named_item_idents_omits_aliases() { - let reg: Registry<()> = crate::api::test_util::reg_with(&[ + let reg: RegistryBuilder<()> = crate::api::test_util::reg_with(&[ "pub fn f(x: u64) -> u64 { x }", "pub struct S { pub a: u64 }", "pub enum E { A }", @@ -1138,7 +1138,7 @@ fn a_binding_local_fn_joins_the_index_but_not_the_source_modules() { crate_name: Some("myflat".into()), ..SourceLocation::default() }; - let reg: Registry<()> = Registry::from_items(vec![( + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(vec![( syn::parse_quote!( pub fn captured(x: u64) -> u64 { x @@ -1157,8 +1157,8 @@ fn a_binding_local_fn_joins_the_index_but_not_the_source_modules() { )], ..Default::default() }; - let gen = reg.resolve(ext).expect("resolve"); - let reg = gen.registry(); + let gen = reg.declare_and_resolve(ext).expect("resolve"); + let reg = &gen; // In the one index, reachable exactly like a captured fn. assert!(reg.flat().function("helper").is_some()); @@ -1180,7 +1180,7 @@ fn a_binding_local_fn_joins_the_index_but_not_the_source_modules() { /// which 30 adapter reads depend on. #[test] fn enum_item_answers_for_both_shapes() { - let reg: Registry<()> = crate::api::test_util::reg_with(&[ + let reg: RegistryBuilder<()> = crate::api::test_util::reg_with(&[ "pub enum Sum { A(u64), B }", "pub enum Flags { X = 1, Y = 2 }", "pub struct S { pub a: u64 }", @@ -1204,7 +1204,7 @@ fn enum_item_answers_for_both_shapes() { /// captured item" about it is simply false. #[test] fn every_declared_type_counts_including_an_alias() { - let reg: Registry<()> = crate::api::test_util::reg_with(&[ + let reg: RegistryBuilder<()> = crate::api::test_util::reg_with(&[ "pub struct S { pub a: u64 }", "pub enum Sum { A(u64), B }", "pub enum Flags { X = 1 }", @@ -1225,52 +1225,32 @@ fn every_declared_type_counts_including_an_alias() { assert!(!reg.declares_type(&id(name)), "`{name}` declares no type"); } - // The sibling that must NOT change: it feeds a "skipping undeclared - // struct/enum" warning, so an alias — which is neither — stays out. - let bodies: HashSet = reg.struct_enum_idents().map(|i| i.to_string()).collect(); - assert_eq!( - bodies, - ["S", "Sum", "Flags"] - .map(String::from) - .into_iter() - .collect(), - "struct_enum_idents feeds a struct/enum message and must exclude aliases" - ); + // The struct/enum population an alias must stay OUT of moved to + // `core::diagnostics` with the skip report that is its only reader. } -/// Both diagnostic sites reach the predicate for an alias, and neither errors. +/// A path-qualified declared type whose tail names an **alias** takes the +/// "did you mean the bare name?" warn-and-pass-through branch, not the +/// `QualifiedDeclaredTypes` hard error. /// -/// `scan_declared` is the entry point for both: a path-qualified declared type -/// whose tail names an alias (the "did you mean the bare name?" heuristic) and -/// an ignored type that names one (the "not found among #[prebindgen] items" -/// check). The messages themselves are `cargo:warning=` on stdout and are not -/// captured here — what this pins is that an alias flows through the same path a -/// struct does, without the `QualifiedDeclaredTypes` hard error. +/// The message itself is a `cargo:warning=` on stdout and is not captured here; +/// what this pins is that an alias flows through the same path a struct does. +/// The ignore side of this question left with the skip report — see +/// `core::diagnostics::ignoring_an_alias_is_not_stale`. #[test] -fn an_alias_flows_through_both_type_diagnostics() { - let build = |declare_qualified: bool| { - let reg: Registry<()> = crate::api::test_util::reg_with(&[ - "pub type Handle = other::Inner;", - "pub fn f(x: u64) -> u64 { x }", - ]); - let mut ext = StubExt::default(); - if declare_qualified { - // Head is NOT a source module, so this is the warn-and-pass-through - // branch rather than the hard error. - ext.types - .insert(TypeKey::parse("foreign::Handle").expect("test type")); - } else { - ext.ignored_types - .insert(TypeKey::parse("Handle").expect("test type")); - } - (reg, ext) - }; - - for qualified in [true, false] { - let (mut reg, ext) = build(qualified); - reg.scan_declared(&ext) - .expect("an alias is a captured item; neither site may fail"); - } +fn a_qualified_alias_warns_rather_than_failing() { + let reg: RegistryBuilder<()> = crate::api::test_util::reg_with(&[ + "pub type Handle = other::Inner;", + "pub fn f(x: u64) -> u64 { x }", + ]); + let mut ext = StubExt::default(); + // Head is NOT a source module, so this is the warn branch. + ext.types + .insert(TypeKey::parse("foreign::Handle").expect("test type")); + ext.declare_into_any(reg) + .expect("declare") + .scanned() + .expect("an alias is a captured item; this must not fail"); } /// A type only a **binding-local** fn writes still has a frontend reading, and @@ -1288,6 +1268,14 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { /// Resolves anything to itself, so declaring the local fn does not also /// require an adapter that can convert its types. struct AnyConverterExt(StubExt); + impl AsStub for AnyConverterExt { + fn stub(&self) -> &StubExt { + &self.0 + } + fn converter(&self, ty: &syn::Type) -> Option> { + Self::converter(ty) + } + } impl AnyConverterExt { fn converter(ty: &syn::Type) -> Option> { Some(ConverterImpl { @@ -1304,12 +1292,6 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { } impl Prebindgen for AnyConverterExt { type Metadata = (); - fn declared_functions(&self) -> HashSet { - self.0.declared_functions() - } - fn local_functions(&self) -> Vec<(syn::ItemFn, String)> { - self.0.local_functions() - } fn on_function(&self, f: &syn::ItemFn, r: &Registry<()>) -> TokenStream { self.0.on_function(f, r) } @@ -1319,17 +1301,12 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { fn on_enum(&self, e: &syn::ItemEnum, r: &Registry<()>) -> TokenStream { self.0.on_enum(e, r) } - fn on_input_type(&self, t: &syn::Type, _r: &Registry<()>) -> Option> { - Self::converter(t) - } - fn on_output_type(&self, t: &syn::Type, _r: &Registry<()>) -> Option> { - Self::converter(t) - } } // `Option` appears nowhere in the captured stream. - let reg: Registry<()> = - Registry::from_items(vec![fn_item("fn captured(x: u64) -> u64 { x }")]).unwrap(); + let reg: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(vec![fn_item("fn captured(x: u64) -> u64 { x }")]) + .unwrap(); assert!( reg.flat() .type_ref(&syn::parse_quote!(Option)) @@ -1348,8 +1325,8 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { .collect(), ..Default::default() }); - let gen = reg.resolve(ext).expect("resolve"); - let reg = gen.registry(); + let gen = reg.declare_and_resolve(ext).expect("resolve"); + let reg = &gen; // The model now holds the reading … let read = reg @@ -1384,8 +1361,9 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { /// position at all. #[test] fn an_unresolved_type_without_a_position_reports_none() { - let reg: Registry<()> = - Registry::from_items(vec![fn_item("fn captured(x: u64) -> u64 { x }")]).unwrap(); + let reg: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(vec![fn_item("fn captured(x: u64) -> u64 { x }")]) + .unwrap(); let ext = StubExt { local_fns: vec![( syn::parse_str("fn helper(v: Option) -> u64 { 0 }").unwrap(), @@ -1399,7 +1377,9 @@ fn an_unresolved_type_without_a_position_reports_none() { }; // `StubExt` supplies no converters, so every scanned type is unresolved: // `Option` reached only through the local fn, `u64` through both. - let err = reg.resolve(ext).expect_err("StubExt resolves nothing"); + let err = reg + .declare_and_resolve(ext) + .expect_err("StubExt resolves nothing"); let msg = err.to_string(); assert!( @@ -1412,7 +1392,7 @@ fn an_unresolved_type_without_a_position_reports_none() { ); // A captured item that DOES have a position still reports it. - let located: Registry<()> = Registry::from_items(vec![( + let located: RegistryBuilder<()> = crate::api::test_util::reg_from_items(vec![( syn::parse_quote!( pub fn f(x: u64) -> u64 { x @@ -1428,10 +1408,125 @@ fn an_unresolved_type_without_a_position_reports_none() { .unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("f").unwrap()); - let err = located.resolve(ext).expect_err("StubExt resolves nothing"); + let err = located + .declare_and_resolve(ext) + .expect_err("StubExt resolves nothing"); assert!( err.to_string().contains("src/lib.rs:12:3: error:"), "a real position must still be reported:\n{}", err ); } + +/// A self-referential type has no topological order, so `crossings` must break +/// the cycle rather than loop or drop a node. +/// +/// The one thing `regen-check` cannot verify: no example declares a recursive +/// type, so byte-identical output says nothing about this path. What is pinned +/// is that the walk terminates, and that every registered crossing is handed +/// out exactly once — a generator can then fail to build the cycle member whose +/// back edge is unanswered, and `build` reports it like any other gap. +#[test] +fn a_recursive_type_is_handed_out_once_and_terminates() { + use std::collections::HashSet as Set; + + let reg: RegistryBuilder<()> = crate::api::test_util::reg_with(&[ + "pub struct Node { pub next: Option>, pub value: u64 }", + "pub fn walk(n: &Node) -> u64 { n.value }", + ]); + let mut ext = StubExt::default(); + ext.functions.insert(syn::parse_str("walk").unwrap()); + ext.types.insert(TypeKey::parse("Node").expect("test type")); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .expect("a recursive struct is scannable"); + + // Terminates, and says each crossing exactly once. + let order = reg.crossings(); + let mut seen: Set = Set::new(); + for c in &order { + assert!(seen.insert(c.clone()), "`{:?}` handed out twice", c); + } + + // Every registered crossing appears — breaking a cycle drops no node. + for dir in [Direction::Input, Direction::Output] { + for key in reg.type_table(dir).keys() { + assert!( + seen.contains(&(dir, key.clone())), + "`{key}` ({dir:?}) never handed out" + ); + } + } + + // And `Node` really is a cycle: it reaches itself. + let node = TypeKey::parse("Node").expect("test type"); + let reaches_self = reg + .immediate_edges(Direction::Output, &node.to_type()) + .into_iter() + .any(|(_, t)| { + crate::api::core::registry::immediate_subtype_positions(&t) + .into_iter() + .any(|inner| { + crate::api::core::registry::immediate_subtype_positions(&inner) + .into_iter() + .any(|i2| TypeKey::from_type(&i2) == node) + }) + }); + assert!(reaches_self, "fixture must actually be recursive"); +} + +/// A built `Registry` has no route back into being described. +/// +/// The phase split is only worth having if it is enforced, and "enforced" here +/// means there is no public `&mut self` on `Registry` at all — not that the +/// obvious ones were removed. `supply` survived two commits that claimed +/// otherwise (#252's `21c403c` and `b708c7e`) because the check for it was a +/// single-line grep and its signature spans lines. +/// +/// So this reads the source with **all** whitespace stripped, which makes a +/// multi-line signature indistinguishable from a one-line one — the exact +/// difference the original grep could not see. +#[test] +fn a_built_registry_exposes_no_mutation() { + let mut offenders: Vec = Vec::new(); + + for entry in std::fs::read_dir(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/api/core/registry" + )) + .expect("registry module dir") + { + let path = entry.expect("dir entry").path(); + if path.extension().is_none_or(|e| e != "rs") { + continue; + } + // `declare.rs` is the builder's own file — `RegistryBuilder` is meant to + // be mutable; that is the whole point of it being a different type. + let name = path + .file_name() + .expect("file name") + .to_string_lossy() + .to_string(); + if name == "declare.rs" || name == "tests.rs" { + continue; + } + let src = std::fs::read_to_string(&path).expect("read source"); + let bare: String = src.chars().filter(|c| !c.is_whitespace()).collect(); + + let mut rest = bare.as_str(); + while let Some(at) = rest.find("pubfn") { + rest = &rest[at + "pubfn".len()..]; + let Some(open) = rest.find('(') else { break }; + if rest[open..].starts_with("(&mutself") { + offenders.push(format!("{name}: pub fn {}(&mut self …)", &rest[..open])); + } + } + } + + assert!( + offenders.is_empty(), + "a built `Registry` must be read-only; found public mutation: {offenders:#?}" + ); +} diff --git a/prebindgen/src/api/core/registry/view.rs b/prebindgen/src/api/core/registry/view.rs new file mode 100644 index 00000000..7c8dd31c --- /dev/null +++ b/prebindgen/src/api/core/registry/view.rs @@ -0,0 +1,193 @@ +//! What a conversion is built against — the partial view during the fill, and +//! the total one after it. + +use std::collections::HashMap; + +use super::*; + +/// One `(direction, type)` pair that crosses the boundary. +/// +/// Direction is part of the identity, not a separate axis: `&str` inbound +/// decodes a `jstring` and outbound allocates one, and one may be convertible +/// while the other is not. +pub type Crossing = (Direction, TypeKey); + +/// What a conversion is built against: the model, and the conversions already +/// available. +/// +/// Two implementors, and the reason there are two is the fill phase. +/// [`Building`] is the partial view a generator sees while it is still +/// producing conversions; [`Registry`] is the total one everything else sees. +/// A helper that serves both — reading a signature off the model, say — takes +/// `&impl Conversions` and works either side of the boundary. +pub trait Conversions { + /// The model. + fn flat(&self) -> &crate::api::core::flat::Flat; + + /// The conversion for `ty` in `dir`, if there is one. + fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry>; + + /// Wire → rust. + fn input_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { + self.conversion(Direction::Input, ty) + } + + /// Rust → wire. + fn output_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { + self.conversion(Direction::Output, ty) + } + + /// The decomposition of a callback argument type, if it has one. + /// + /// On the trait because a callback converter needs it while being built, + /// and the emitter needs it again afterwards. Plans are applied by + /// `prepare`, so they are complete either side of that line. + fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan>; + + /// Every callback-argument decomposition, for the emitters that enumerate + /// them rather than look one up. + fn callback_arg_plans(&self) -> &HashMap; + + /// The return decomposition of a function, if it has one. + fn unfold_plans(&self) -> &HashMap; + + /// The error-position decomposition of a fallible function. + fn error_plans(&self) -> &HashMap; + + /// The declaration-default decomposition behind each deconstructor. + fn decon_plans( + &self, + ) -> &HashMap; + + /// Every type key that crosses in `dir`. + /// + /// The niche allocator needs the whole population, not one lookup: it picks + /// sentinel values no sibling conversion can produce. + fn crossing_keys(&self, dir: Direction) -> Vec; + + /// The origin crate's module path for an item, or `None` when unknown. + fn origin_module(&self, ident: &syn::Ident) -> Option { + origin_module_of(self.flat(), ident) + } + + /// The default module for references with no recorded origin. + fn default_module(&self) -> Option { + default_module_of(self.flat()) + } +} + +impl Conversions for Building<'_, M> { + fn flat(&self) -> &crate::api::core::flat::Flat { + &self.registry.flat + } + fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { + self.built.get(&(dir, TypeKey::from_type(ty))) + } + fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan> { + self.registry.callback_arg_plans.get(key) + } + fn callback_arg_plans(&self) -> &HashMap { + &self.registry.callback_arg_plans + } + fn unfold_plans(&self) -> &HashMap { + &self.registry.unfold_plans + } + fn error_plans(&self) -> &HashMap { + &self.registry.error_plans + } + fn decon_plans( + &self, + ) -> &HashMap { + &self.registry.decon_plans + } + fn crossing_keys(&self, dir: Direction) -> Vec { + self.all_keys + .iter() + .filter(|(d, _)| *d == dir) + .map(|(_, k)| k.clone()) + .collect() + } +} + +impl Conversions for Registry { + fn flat(&self) -> &crate::api::core::flat::Flat { + &self.flat + } + fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { + self.type_table(dir) + .get(&TypeKey::from_type(ty))? + .entry + .as_ref() + } + fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan> { + self.callback_arg_plans.get(key) + } + fn callback_arg_plans(&self) -> &HashMap { + &self.callback_arg_plans + } + fn unfold_plans(&self) -> &HashMap { + &self.unfold_plans + } + fn error_plans(&self) -> &HashMap { + &self.error_plans + } + fn decon_plans( + &self, + ) -> &HashMap { + &self.decon_plans + } + fn crossing_keys(&self, dir: Direction) -> Vec { + self.type_table(dir).keys().cloned().collect() + } +} + +/// The registry mid-fill: the model, plus the conversions supplied so far. +/// +/// What a generator builds a conversion *against*. It sees every crossing it +/// can compose from — `RegistryBuilder::crossings` hands them out inner-first, so by +/// the time `Option` is asked for, `Handle` is already in here. +/// +/// It exposes exactly the reads a conversion needs, which is what keeps the +/// half-filled state from leaking anywhere else: the resolved [`Registry`] is +/// what the emitters get, and it is total. +pub struct Building<'a, M> { + /// The prepared registry: model, decompositions and the full crossing + /// population. Its conversion cells are still empty — [`Self::conversion`] + /// deliberately reads [`Self::built`] instead, so a generator can only see + /// what it has actually produced. + registry: &'a Registry, + built: &'a HashMap>, + /// Every crossing in the binding, resolved or not — the niche allocator + /// reads the population, not just what is built so far. + all_keys: &'a [Crossing], +} + +impl<'a, M> Building<'a, M> { + pub(crate) fn new( + registry: &'a Registry, + built: &'a HashMap>, + all_keys: &'a [Crossing], + ) -> Self { + Self { + registry, + built, + all_keys, + } + } +} + +/// Shared by [`Registry::origin_module`] and [`Building::origin_module`], so the +/// two cannot answer differently. +pub(super) fn origin_module_of( + flat: &crate::api::core::flat::Flat, + ident: &syn::Ident, +) -> Option { + let crate_name = flat.element(ident)?.location().crate_name.as_ref()?; + syn::parse_str(&crate_name.replace('-', "_")).ok() +} + +pub(super) fn default_module_of(flat: &crate::api::core::flat::Flat) -> Option { + flat.source_modules() + .first() + .and_then(|m| syn::parse_str(m).ok()) +} diff --git a/prebindgen/src/api/core/registry/walk.rs b/prebindgen/src/api/core/registry/walk.rs new file mode 100644 index 00000000..fae39092 --- /dev/null +++ b/prebindgen/src/api/core/registry/walk.rs @@ -0,0 +1,42 @@ +//! Structural type-graph helpers, shared by the scan and the diagnostics BFS. + +// ────────────────────────────────────────────────────────────────────── +// Helpers +// ────────────────────────────────────────────────────────────────────── + +/// Immediate child type positions of `ty` (one level deep). +pub fn immediate_subtype_positions(ty: &syn::Type) -> Vec { + match ty { + syn::Type::Path(p) => { + if let Some(last) = p.path.segments.last() { + if let syn::PathArguments::AngleBracketed(ab) = &last.arguments { + return ab + .args + .iter() + .filter_map(|a| { + if let syn::GenericArgument::Type(t) = a { + Some(t.clone()) + } else { + None + } + }) + .collect(); + } + } + vec![] + } + syn::Type::Reference(r) => vec![(*r.elem).clone()], + syn::Type::Tuple(t) => t.elems.iter().cloned().collect(), + syn::Type::Array(a) => vec![(*a.elem).clone()], + syn::Type::Slice(s) => vec![(*s.elem).clone()], + syn::Type::Ptr(p) => vec![(*p.elem).clone()], + syn::Type::Group(g) => immediate_subtype_positions(&g.elem), + syn::Type::Paren(p) => immediate_subtype_positions(&p.elem), + syn::Type::ImplTrait(_) => extract_fn_trait_args(ty).unwrap_or_default(), + _ => vec![], + } +} + +/// The callback grammar, which the source language owns — re-exported here for +/// the existing call sites until they consume elements (stages L2–L4 of #229). +pub use crate::api::core::flat::extract_fn_trait_args; diff --git a/prebindgen/src/api/core/resolve.rs b/prebindgen/src/api/core/resolve.rs index 5155790a..9adb5d51 100644 --- a/prebindgen/src/api/core/resolve.rs +++ b/prebindgen/src/api/core/resolve.rs @@ -1,19 +1,14 @@ -//! Structural resolver and the post-resolution `required` propagation pass. +//! Completeness: which conversions a binding actually needs, and whether it has +//! them. //! -//! The resolver fills `Registry::input_types` / `output_types` cells by asking -//! the language adapter for each unresolved type's converter via -//! [`Prebindgen::on_input_type`] / [`Prebindgen::on_output_type`]. The adapter -//! peels the type's outermost structure itself and either returns a *terminal* -//! converter or a *wrapper* that looked up inner converters in the registry -//! (declaring those inners in [`ConverterImpl::subs`]); it returns `None` to -//! **defer** when an inner isn't resolved yet. +//! What survives here is the completeness check. The generator fills the cells +//! itself (`RegistryBuilder::crossings` → `convert_with`); this decides whether the +//! set it produced covers everything reachable from an exported root. //! -//! A fixed-point loop runs PASS A (read-only, build deltas) then PASS B (apply -//! deltas) until no entry advances. This handles inner-before-outer -//! dependencies (e.g. `Vec>` whose `Vec<_>` wrapper needs -//! `Option`'s wire) and the cross-direction `impl Fn` seam (a callback's -//! args resolve in the opposite direction). New slots only go `None → Some`, so -//! the loop terminates. +//! There is no loop. `Registry::crossings` hands the demand out inner-first, so +//! a generator answers each crossing once, with everything it composes from +//! already built — including across the `impl Fn` seam, whose args cross in the +//! opposite direction. //! //! After the loop, [`required_set`] performs a BFS from the **root** cells — the //! ones the binding asked for directly — through `subs` edges. It returns the @@ -25,10 +20,7 @@ use std::collections::{HashSet, VecDeque}; use crate::{ - api::core::{ - prebindgen::{ConverterImpl, Prebindgen}, - registry::{Direction, Registry, TypeEntry, TypeKey}, - }, + api::core::registry::{Direction, Registry, TypeKey}, SourceLocation, }; @@ -79,112 +71,6 @@ impl std::fmt::Display for ResolveError { impl std::error::Error for ResolveError {} -/// Top-level resolution entry point. -/// -/// Runs ONE fixed-point loop covering both directions. Each iteration sweeps -/// every unresolved entry (both input and output); deltas are collected without -/// mutating the registry, then applied at the end of the iteration. Loops until -/// a full sweep produces zero deltas. -/// -/// The single-loop design lets cross-direction dependencies converge: e.g. -/// `impl Fn(Sample)` is an INPUT entry whose callback wrapper needs `Sample`'s -/// OUTPUT converter (callback args flow Rust→foreign side). `Sample`'s output -/// resolves in one iteration, then `impl Fn(Sample)` succeeds in the next. -pub fn resolve( - registry: &mut Registry, - ext: &E, -) -> Result<(), ResolveError> { - loop { - // PASS A (read-only): sweep every unresolved entry once per direction, - // ask the adapter for a converter. Inner-before-outer ordering falls out - // of the fixed-point loop: a wrapper that needs an unresolved inner - // returns `None` and is retried next iteration. - let deltas_in = collect_deltas(registry, Direction::Input, ext); - let deltas_out = collect_deltas(registry, Direction::Output, ext); - if deltas_in.is_empty() && deltas_out.is_empty() { - break; - } - // PASS B: apply. - apply_deltas(registry, Direction::Input, deltas_in); - apply_deltas(registry, Direction::Output, deltas_out); - } - final_invariant_check(registry) -} - -/// PASS A — walk every unresolved entry in `dir`, ask the adapter, collect -/// successful results without mutating the registry. -fn collect_deltas( - registry: &Registry, - dir: Direction, - ext: &E, -) -> Vec<(TypeKey, TypeEntry)> { - let mut deltas: Vec<(TypeKey, TypeEntry)> = Vec::new(); - let table = registry.type_table(dir); - for (key, slot) in table { - if slot.entry.is_some() { - continue; - } - let key_ty = key.to_type(); - if let Some(entry) = resolve_one(ext, &key_ty, dir, registry) { - deltas.push((key.clone(), entry)); - } - } - deltas -} - -/// PASS B — apply collected deltas. Sole writer to the registry maps in -/// this iteration. Only fills empty (`None`) slots, so slots are monotonic -/// `None → Some` and the fixed-point loop terminates. -fn apply_deltas( - registry: &mut Registry, - dir: Direction, - deltas: Vec<(TypeKey, TypeEntry)>, -) { - let table = registry.type_table_mut(dir); - for (key, entry) in deltas { - if let Some(cell) = table.get_mut(&key) { - if cell.entry.is_none() { - cell.entry = Some(entry); - } - } - } -} - -/// Resolve one entry: ask the adapter for a converter (it inspects `key_ty` -/// structurally), then — for an `impl Fn(args...)` input that nothing else -/// claimed — fall back to `dispatch_fn_input`. The resulting `TypeEntry::subs` -/// are the inner types the converter declared it composed from. -fn resolve_one( - ext: &E, - key_ty: &syn::Type, - dir: Direction, - registry: &Registry, -) -> Option> { - let conv: Option> = match dir { - Direction::Input => ext.on_input_type(key_ty, registry), - Direction::Output => ext.on_output_type(key_ty, registry), - }; - // `impl Fn(args...) + Send + Sync + 'static` fallback (input only): callback - // args resolve in the OUTPUT direction, so this converter declares no - // same-direction `subs` — the callback-arg required-ness flows through the - // registry's direction-flipped `immediate_edges`, not through `subs`. - let conv = conv.or_else(|| { - if dir != Direction::Input { - return None; - } - let args = crate::api::core::registry::extract_fn_trait_args(key_ty)?; - ext.dispatch_fn_input(&args, registry) - }); - conv.map(|c| TypeEntry { - destination: c.destination, - function: c.function, - pre_stages: c.pre_stages, - subs: c.subs.iter().map(TypeKey::from_type).collect(), - niches: c.niches, - metadata: c.metadata, - }) -} - // ────────────────────────────────────────────────────────────────────── // Required-flag propagation (BFS from required entries through `subs`) // ────────────────────────────────────────────────────────────────────── @@ -209,7 +95,7 @@ fn required_set(registry: &Registry) -> HashSet<(Direction, TypeKey)> { while let Some((dir, key)) = queue.pop_front() { // Subs travel in the same direction as the parent — they are the inner // converters this body delegates to. An unresolved cell has none to give, - // which is why this cannot run before the fixed-point loop. + // which is why this cannot run before the conversions have filled them. let Some(entry) = registry .type_table(dir) .get(&key) @@ -283,7 +169,7 @@ fn collect_unresolved_descendants( } } -fn final_invariant_check(registry: &Registry) -> Result<(), ResolveError> { +pub(crate) fn check_complete(registry: &Registry) -> Result<(), ResolveError> { let required = required_set(registry); let mut entries: Vec = Vec::new(); let mut unresolved_required_roots: Vec<(Direction, TypeKey)> = Vec::new(); diff --git a/prebindgen/src/api/core/resolve/tests.rs b/prebindgen/src/api/core/resolve/tests.rs index 6df599d8..06fffd66 100644 --- a/prebindgen/src/api/core/resolve/tests.rs +++ b/prebindgen/src/api/core/resolve/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::api::test_util::cell; +use crate::api::{core::registry::TypeEntry, test_util::cell}; /// Regression: when a required type is itself unresolved AND has fields /// that are also unresolved, the diagnostic must list both. Previously @@ -16,13 +16,13 @@ fn final_invariant_reports_unresolved_field_of_unresolved_struct() { // catch. Driven through the real scan rather than simulated, so the state // under test is one the pipeline can actually produce. let mut reg: Registry<()> = - crate::api::test_util::reg_with(&["pub struct Outer { pub inner: ZKeyExpr }"]); + crate::api::test_util::scanned_with(&["pub struct Outer { pub inner: ZKeyExpr }"]); reg.require_input(&syn::parse_quote!(Outer)); let zke_key = TypeKey::parse("ZKeyExpr").expect("test type"); assert!(!reg.input_types[&zke_key].root, "the field is not a root"); - let err = final_invariant_check(®).expect_err("must surface unresolved"); + let err = check_complete(®).expect_err("must surface unresolved"); let ResolveError::Unresolved { entries } = err; let reported: std::collections::HashSet = entries.iter().map(|e| e.key.to_string()).collect(); @@ -48,7 +48,7 @@ fn final_invariant_stops_at_resolved_nodes() { // Through the real scan, so the state under test is one the pipeline can // actually produce: `Unrelated` is a field type nothing declares. - let mut reg: Registry<()> = crate::api::test_util::reg_with(&[ + let mut reg: Registry<()> = crate::api::test_util::scanned_with(&[ "pub struct Outer { pub inner: Inner }", "pub struct Inner { pub unused: Unrelated }", ]); @@ -59,13 +59,11 @@ fn final_invariant_stops_at_resolved_nodes() { let inner_key = TypeKey::parse("Inner").expect("test type"); let unrelated_key = TypeKey::parse("Unrelated").expect("test type"); - reg.input_types - .insert(outer_key.clone(), cell(&outer_key, true, None)); + reg.input_types.insert(outer_key.clone(), cell(true, None)); reg.input_types.insert( inner_key.clone(), cell( - &inner_key, false, Some(TypeEntry { destination: syn::parse_quote!(i64), @@ -81,9 +79,9 @@ fn final_invariant_stops_at_resolved_nodes() { ); reg.input_types - .insert(unrelated_key.clone(), cell(&unrelated_key, false, None)); + .insert(unrelated_key.clone(), cell(false, None)); - let err = final_invariant_check(®).expect_err("must surface Outer"); + let err = check_complete(®).expect_err("must surface Outer"); let ResolveError::Unresolved { entries } = err; let reported: std::collections::HashSet = entries.iter().map(|e| e.key.to_string()).collect(); @@ -121,7 +119,6 @@ fn a_type_reachable_only_through_subs_must_still_resolve() { reg.input_types.insert( outer.clone(), cell( - &outer, true, Some(TypeEntry { destination: syn::parse_quote!(i64), @@ -136,9 +133,9 @@ fn a_type_reachable_only_through_subs_must_still_resolve() { ), ); // `Mid` is present, unresolved, and NOT a root. - reg.input_types.insert(mid.clone(), cell(&mid, false, None)); + reg.input_types.insert(mid.clone(), cell(false, None)); - let err = final_invariant_check(®).expect_err("Mid must be reported"); + let err = check_complete(®).expect_err("Mid must be reported"); let ResolveError::Unresolved { entries } = err; let reported: std::collections::HashSet = entries.iter().map(|e| e.key.to_string()).collect(); diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index 5692bfeb..d354f91f 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use proc_macro2::Span; -use quote::ToTokens; use crate::SourceLocation; @@ -51,9 +50,9 @@ pub fn type_from_ident(ident: &syn::Ident) -> syn::Type { /// `Vec` ≡ `Bytes` turns a sequence into an extern — and no /// key-shape refinement fixes the category error. /// 5. Lifetimes are NOT normalized (`&'a T` ≠ `&T`, `Foo<'static>` ≠ `Foo`) -/// — [`match_pattern`] treats lifetimes as fixed structure and -/// foreign-type declarations (`ptr_class!(ZKeyExpr<'static>)`) rely on -/// the verbatim spelling. +/// — a lifetime is part of the spelling a foreign-type declaration relies +/// on (`ptr_class!(ZKeyExpr<'static>)`), so collapsing it would make two +/// distinct declarations collide. /// /// Idempotent; recurses through references, slices, tuples, pointers, /// generic arguments, and `impl Trait` bounds. Paths with a qualified self @@ -117,7 +116,7 @@ impl Normalization { /// Collect from a captured stream, before anything is normalized. /// - /// Both entry points — `FlatBuilder::build` and `Registry::from_items` — build + /// The single entry point — `FlatBuilder::build` — builds /// this, so they cannot normalize differently. Gathering every module and alias /// first is what makes reduction order-independent: a signature may name a type /// whose alias is declared later, or in another source. @@ -219,7 +218,7 @@ pub fn normalize_type(ty: &mut syn::Type, against: &Normalization) { /// Apply [`normalize_type`] to every type position inside an item — fn /// signatures, struct fields, enum variants, const types. The ingest-time -/// pass ([`crate::api::core::registry::Registry::from_items`]) that makes +/// pass ([`crate::api::core::flat::FlatBuilder::build`]) that makes /// captured spellings canonical before any key is formed, so every /// downstream `TypeKey::from_type` sees the flat spelling. pub fn normalize_item_types(item: &mut syn::Item, against: &Normalization) { @@ -274,186 +273,6 @@ fn reduce_flat_path(path: &mut syn::Path, against: &Normalization) { } } -/// Structurally match a concrete type `ty` against a wildcard `pattern` (a -/// `syn::Type` whose `_` placeholders are [`syn::Type::Infer`]). On success, -/// returns the subtrees of `ty` captured at each wildcard, in left-to-right -/// document order; `None` if the shapes don't unify. -/// -/// This is the inverse of pattern substitution: `match_pattern(ty, pat)` finds -/// the args `a` such that substituting them into `pat` reproduces `ty`. It -/// replaces the rank resolver's combinatorial wildcard *enumeration* with a -/// direct unify — an adapter (or a user-registered wrapper table) keeps full -/// expressive power (any depth) without the framework enumerating every -/// placement. Handles the type shapes that appear as wildcard patterns -/// (`Path<…>`, `&`/`&mut`, `[_]`, `(…)`, `*const`/`*mut`); other leaves compare -/// by token equality. -pub fn match_pattern(ty: &syn::Type, pattern: &syn::Type) -> Option> { - let mut out = Vec::new(); - if unify(ty, pattern, &mut out) { - Some(out) - } else { - None - } -} - -/// Count the wildcard (`_`) placeholders in a pattern — its "openness". Used to -/// order overlapping registered patterns most-specific-first (fewer wildcards -/// win, e.g. `Result<_, ConcreteErr>` over `Result<_, _>`). -pub fn wildcard_count(pattern: &syn::Type) -> usize { - if matches!(pattern, syn::Type::Infer(_)) { - return 1; - } - immediate_pattern_children(pattern) - .iter() - .map(wildcard_count) - .sum() -} - -/// Immediate substitutable child positions of a type (the generic type-args of -/// a path, the referent of a `&`/`*`, the element of a slice/array, the members -/// of a tuple). Mirrors the resolver's traversal so `match_pattern` / -/// `wildcard_count` descend the same positions wildcards can occupy. -fn immediate_pattern_children(ty: &syn::Type) -> Vec { - match ty { - syn::Type::Path(tp) => tp - .path - .segments - .last() - .and_then(|seg| match &seg.arguments { - syn::PathArguments::AngleBracketed(ab) => Some( - ab.args - .iter() - .filter_map(|a| match a { - syn::GenericArgument::Type(t) => Some(t.clone()), - _ => None, - }) - .collect(), - ), - _ => None, - }) - .unwrap_or_default(), - syn::Type::Reference(r) => vec![(*r.elem).clone()], - syn::Type::Ptr(p) => vec![(*p.elem).clone()], - syn::Type::Slice(s) => vec![(*s.elem).clone()], - syn::Type::Array(a) => vec![(*a.elem).clone()], - syn::Type::Tuple(t) => t.elems.iter().cloned().collect(), - syn::Type::Group(g) => immediate_pattern_children(&g.elem), - syn::Type::Paren(p) => immediate_pattern_children(&p.elem), - _ => Vec::new(), - } -} - -fn unify(ty: &syn::Type, pat: &syn::Type, out: &mut Vec) -> bool { - if matches!(pat, syn::Type::Infer(_)) { - out.push(ty.clone()); - return true; - } - match (ty, pat) { - (syn::Type::Path(t), syn::Type::Path(p)) => { - // Same path up to the last segment's generic args; unify those. - if t.qself.is_some() || p.qself.is_some() { - return token_eq(ty, pat); - } - let (ts, ps) = (&t.path.segments, &p.path.segments); - if ts.len() != ps.len() { - return false; - } - for (i, (tseg, pseg)) in ts.iter().zip(ps.iter()).enumerate() { - if tseg.ident != pseg.ident { - return false; - } - let is_last = i + 1 == ts.len(); - // Non-last segments (and non-angle-bracketed last segments) must - // match verbatim; the last segment's generic args unify. - match (&tseg.arguments, &pseg.arguments) { - ( - syn::PathArguments::AngleBracketed(ta), - syn::PathArguments::AngleBracketed(pa), - ) if is_last => { - // Compare ALL generic args positionally — lifetimes, - // const generics, and bindings are part of the fixed - // pattern structure and must match token-for-token; only - // a `_` in a type position captures. (Mirrors the old - // enumerator's exact `TypeKey` match, so e.g. - // `Foo<'static, _>` does NOT match `Foo<'a, T>`.) - if ta.args.len() != pa.args.len() { - return false; - } - for (a, b) in ta.args.iter().zip(pa.args.iter()) { - match (a, b) { - ( - syn::GenericArgument::Type(at), - syn::GenericArgument::Type(bt), - ) => { - if !unify(at, bt, out) { - return false; - } - } - (a, b) => { - if a.to_token_stream().to_string() - != b.to_token_stream().to_string() - { - return false; - } - } - } - } - } - (a, b) => { - if a.to_token_stream().to_string() != b.to_token_stream().to_string() { - return false; - } - } - } - } - true - } - (syn::Type::Reference(t), syn::Type::Reference(p)) => { - // Mutability and lifetime are fixed structure — `&'static _` must not - // match `&'a T`, and `&_` (no lifetime) must not match `&'a T`. - t.mutability.is_some() == p.mutability.is_some() - && lifetime_eq(&t.lifetime, &p.lifetime) - && unify(&t.elem, &p.elem, out) - } - (syn::Type::Ptr(t), syn::Type::Ptr(p)) => { - t.mutability.is_some() == p.mutability.is_some() - && t.const_token.is_some() == p.const_token.is_some() - && unify(&t.elem, &p.elem, out) - } - (syn::Type::Slice(t), syn::Type::Slice(p)) => unify(&t.elem, &p.elem, out), - (syn::Type::Array(t), syn::Type::Array(p)) => { - t.len.to_token_stream().to_string() == p.len.to_token_stream().to_string() - && unify(&t.elem, &p.elem, out) - } - (syn::Type::Tuple(t), syn::Type::Tuple(p)) => { - t.elems.len() == p.elems.len() - && t.elems - .iter() - .zip(p.elems.iter()) - .all(|(a, b)| unify(a, b, out)) - } - (syn::Type::Group(t), _) => unify(&t.elem, pat, out), - (_, syn::Type::Group(p)) => unify(ty, &p.elem, out), - (syn::Type::Paren(t), _) => unify(&t.elem, pat, out), - (_, syn::Type::Paren(p)) => unify(ty, &p.elem, out), - _ => token_eq(ty, pat), - } -} - -/// Two optional reference lifetimes are equal iff both are absent or name the -/// same lifetime. -fn lifetime_eq(a: &Option, b: &Option) -> bool { - match (a, b) { - (None, None) => true, - (Some(x), Some(y)) => x.ident == y.ident, - _ => false, - } -} - -fn token_eq(a: &syn::Type, b: &syn::Type) -> bool { - a.to_token_stream().to_string() == b.to_token_stream().to_string() -} - /// If `ty` is `Option` (by last path segment), return `Inner`. pub fn option_inner_type(ty: &syn::Type) -> Option { generic_inner(ty, "Option") @@ -665,7 +484,7 @@ pub fn first_payload_variant(e: &syn::ItemEnum) -> Option<&syn::Variant> { /// which alternative is live — plus one **leaf group per variant**. /// /// Core describes the sum; adapters decide what its leaves look like on the -/// wire (`JniGen` overlays the groups in the signature, `Cbindgen` overlays +/// wire (`JniGenBuilder` overlays the groups in the signature, `CbindgenBuilder` overlays /// them in memory as a `#[repr(C)]` union). Nothing here names a wire /// detail — in particular a payload enum carries no `repr`, so tags are /// declaration order and never an explicit discriminant. @@ -770,7 +589,7 @@ impl SumVariant { /// /// The single source of truth for every int↔variant mapping in the /// pipeline — the Kotlin `value(N)` constants, the generated `jint → -/// variant` decode, and the `#[repr(C)]` mirror `Cbindgen` emits — keeping +/// variant` decode, and the `#[repr(C)]` mirror `CbindgenBuilder` emits — keeping /// them from drifting and removing the need for a hand-written /// `TryFrom` on the source enum. Non-literal discriminants are /// rejected because prebindgen cannot reliably evaluate arbitrary diff --git a/prebindgen/src/api/core/types_util/tests.rs b/prebindgen/src/api/core/types_util/tests.rs index 23c3bd17..f41fa6d6 100644 --- a/prebindgen/src/api/core/types_util/tests.rs +++ b/prebindgen/src/api/core/types_util/tests.rs @@ -2,116 +2,9 @@ use quote::ToTokens; use super::*; -fn ty(s: &str) -> syn::Type { - syn::parse_str(s).unwrap() -} -fn caps(v: Option>) -> Option> { - v.map(|a| a.iter().map(|t| t.to_token_stream().to_string()).collect()) -} - -#[test] -fn match_pattern_outermost_and_deep() { - // Outermost single wildcard. - assert_eq!( - caps(match_pattern(&ty("Option"), &ty("Option<_>"))), - Some(vec!["u64".to_string()]) - ); - // Two wildcards (Result). - assert_eq!( - caps(match_pattern( - &ty("Result"), - &ty("Result<_, _>") - )), - Some(vec!["ZKeyExpr".to_string(), "ZError".to_string()]) - ); - // Deep single wildcard, intermediate level concrete (`Option<&_>`). - assert_eq!( - caps(match_pattern(&ty("Option<&ZKeyExpr>"), &ty("Option<&_>"))), - Some(vec!["ZKeyExpr".to_string()]) - ); - // The shallow pattern also matches, capturing the reference whole. - assert_eq!( - caps(match_pattern(&ty("Option<&ZKeyExpr>"), &ty("Option<_>"))), - Some(vec!["& ZKeyExpr".to_string()]) - ); - // `&mut _` vs `&_` mutability must agree. - assert!(match_pattern(&ty("&mut Foo"), &ty("&_")).is_none()); - assert_eq!( - caps(match_pattern(&ty("&mut Foo"), &ty("&mut _"))), - Some(vec!["Foo".to_string()]) - ); - // Slice element. - assert_eq!( - caps(match_pattern(&ty("&[u8]"), &ty("&[_]"))), - Some(vec!["u8".to_string()]) - ); - // Arbitrary depth (the framework never enumerated this, but a user - // pattern can name it). - assert_eq!( - caps(match_pattern( - &ty("Vec>"), - &ty("Vec>") - )), - Some(vec!["u64".to_string()]) - ); - // Head mismatch. - assert!(match_pattern(&ty("Vec"), &ty("Option<_>")).is_none()); - // Concrete non-wildcard pattern: matches only itself, no captures. - assert_eq!( - caps(match_pattern(&ty("MyType"), &ty("MyType"))), - Some(vec![]) - ); - assert!(match_pattern(&ty("Other"), &ty("MyType")).is_none()); -} - /// Lifetimes and const-generic args are fixed pattern structure — they must /// match token-for-token, not be silently dropped (restores the old /// enumerator's exact `TypeKey` semantics). -#[test] -fn match_pattern_respects_lifetimes_and_const_generics() { - // Reference lifetimes must match exactly. - assert_eq!( - caps(match_pattern(&ty("&'static Foo"), &ty("&'static _"))), - Some(vec!["Foo".to_string()]) - ); - assert!(match_pattern(&ty("&'a Foo"), &ty("&'static _")).is_none()); - // A no-lifetime pattern must not match a borrow that names a lifetime. - assert!(match_pattern(&ty("&'a Foo"), &ty("&_")).is_none()); - assert_eq!( - caps(match_pattern(&ty("&Foo"), &ty("&_"))), - Some(vec!["Foo".to_string()]) - ); - // A lifetime generic arg in a path is fixed structure. - assert_eq!( - caps(match_pattern( - &ty("Cow<'static, _>"), - &ty("Cow<'static, _>") - )), - Some(vec!["_".to_string()]) - ); - assert!(match_pattern(&ty("Cow<'a, str>"), &ty("Cow<'static, _>")).is_none()); - // Const-generic arg is fixed structure: arity must match exactly. - assert_eq!( - caps(match_pattern(&ty("Arr"), &ty("Arr<_, 4>"))), - Some(vec!["u8".to_string()]) - ); - assert!(match_pattern(&ty("Arr"), &ty("Arr<_, 4>")).is_none()); - // Array length is fixed structure. - assert!(match_pattern(&ty("[u8; 8]"), &ty("[_; 4]")).is_none()); - assert_eq!( - caps(match_pattern(&ty("[u8; 4]"), &ty("[_; 4]"))), - Some(vec!["u8".to_string()]) - ); -} - -#[test] -fn wildcard_count_specificity() { - assert_eq!(wildcard_count(&ty("Result<_, _>")), 2); - assert_eq!(wildcard_count(&ty("Result<_, ConcreteErr>")), 1); - assert_eq!(wildcard_count(&ty("Option<&_>")), 1); - assert_eq!(wildcard_count(&ty("ZKeyExpr")), 0); -} - // ── Enum shape / sum model ───────────────────────────────────────────── #[test] diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index daf80029..e83d0a44 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -277,7 +277,7 @@ pub fn apply( validate_declarations(acc)?; // Binding-local accessors (`LocalAcc` records) resolve through registry // entries synthesized by the adapter's `local_functions()` pre-pass in - // `Registry::resolve` — by this point they read exactly like + // the builder's scan — by this point they read exactly like // `#[prebindgen]` accessors. // Gate: every accessor-function record of every declared deconstructor must @@ -656,7 +656,7 @@ fn wire_fixed_returns( // All THREE peeled layers, the `Vec` element included. The shape // fold peels here, so the matching unrequire belongs here; leaving // the element out made the invariant depend on the adapter's - // `boundary_only_types` covering it — true for JniGen today, and + // `boundary_only_types` covering it — true for JniGenBuilder today, and // the only reason a `Vec`-only declaration resolves. registry.unrequire_output(&ret); registry.unrequire_output(&after_opt); diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index 31d8d238..6f585f5e 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -1,7 +1,10 @@ use quote::ToTokens; use super::*; -use crate::api::{core::types_util::ident, test_util::reg_with}; +use crate::api::{ + core::{registry::Registry, types_util::ident}, + test_util::scanned_with as reg_with, +}; /// A generous `.fun_accessor` set covering every function used as a /// deconstructor record across these tests (a superset is fine — `apply` @@ -52,7 +55,7 @@ fn accessor_optional_primitive() { // M2: `z_sample_timestamp(&ZSample) -> Option<&ZTimestamp>` decomposed // into a single primitive leaf `z_timestamp_ntp64(&ZTimestamp) -> i64` // (no identity). Outer shape is `Optional(Decompose)`. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_sample_timestamp(s: &ZSample) -> Option<&ZTimestamp> { todo!() }", "fn z_timestamp_ntp64(t: &ZTimestamp) -> i64 { todo!() }", ]); @@ -103,7 +106,7 @@ fn accessor_optional_primitive() { fn accessor_plan_byref() { // `z_sample_key_expr(&ZSample) -> &ZKeyExpr` decomposed into the keyexpr // handle (identity) + its string form (`z_keyexpr_as_str`). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_sample_key_expr(s: &ZSample) -> &ZKeyExpr { todo!() }", "fn z_keyexpr_as_str(ke: &ZKeyExpr) -> &str { todo!() }", ]); @@ -170,7 +173,7 @@ fn root_identity_before_nested_identity_errors() { // Owned return: the root `.field_self()` MOVES the value, a nested // identity (spliced ZKeyExpr handle) borrows it — id-first is the // order that would generate non-compiling Rust, caught at apply time. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_take_query(q: &ZQuery) -> ZQuery { todo!() }", "fn z_query_key_expr(q: &ZQuery) -> &ZKeyExpr { todo!() }", ]); @@ -204,7 +207,7 @@ fn root_identity_before_nested_identity_errors() { assert!(matches!(err, UnfoldError::RootIdentityBeforeNested { .. })); // Root identity LAST (the zenoh `Query` shape) is accepted. - let mut reg2 = reg_with(&[ + let mut reg2: Registry<()> = reg_with(&[ "fn z_take_query(q: &ZQuery) -> ZQuery { todo!() }", "fn z_query_key_expr(q: &ZQuery) -> &ZKeyExpr { todo!() }", ]); @@ -237,7 +240,7 @@ fn root_identity_before_nested_identity_errors() { #[test] fn accessor_target_mismatch_errors() { // Accessor takes a different type than the accessor's target. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_foo() -> ZKeyExpr { todo!() }", "fn wrong(x: &ZSample) -> &str { todo!() }", ]); @@ -262,7 +265,7 @@ fn accessor_target_mismatch_errors() { #[test] fn multiple_identity_errors() { - let mut reg = reg_with(&["fn z_foo() -> ZKeyExpr { todo!() }"]); + let mut reg: Registry<()> = reg_with(&["fn z_foo() -> ZKeyExpr { todo!() }"]); let mut acc = Deconstructors::default(); acc.deconstructors.push(DeconstructorDecl { target: syn::parse_quote!(ZKeyExpr), @@ -283,7 +286,7 @@ fn multiple_identity_errors() { #[test] fn record_must_be_fun_accessor() { // A deconstructor record referencing a non-`.fun_accessor` fn errors. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_foo(s: &ZSample) -> &ZKeyExpr { todo!() }", "fn z_keyexpr_as_str(ke: &ZKeyExpr) -> &str { todo!() }", ]); @@ -322,7 +325,7 @@ fn record_must_be_fun_accessor() { fn duplicate_leaf_name_errors() { // Two records of one deconstructor given the same literal name ⇒ hard // error (names are emitted verbatim; never auto-disambiguated). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_foo() -> ZSample { todo!() }", "fn z_sample_key_expr(s: &ZSample) -> &str { todo!() }", "fn z_sample_payload(s: &ZSample) -> Vec { todo!() }", @@ -359,7 +362,7 @@ fn duplicate_leaf_name_errors() { #[test] fn reserved_separator_in_name_errors() { // A record name containing the reserved `"__"` chain separator ⇒ error. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_foo() -> ZSample { todo!() }", "fn z_sample_key_expr(s: &ZSample) -> &str { todo!() }", ]); @@ -396,7 +399,7 @@ fn nested_accessor_flatten() { // accessor nests ZKeyExpr (handle+string), ZZBytes (bytes), and a // nullable ZTimestamp (Option<&ZTimestamp> → ntp64), plus a direct enum // leaf. Verifies path prefixes + nullable propagation. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_reply_sample(r: &ZReply) -> Option<&ZSample> { todo!() }", "fn z_sample_key_expr(s: &ZSample) -> &ZKeyExpr { todo!() }", "fn z_sample_payload(s: &ZSample) -> &ZZBytes { todo!() }", @@ -524,7 +527,7 @@ fn reply_product_double_option_flatten() { // `Option` Acc record with NO default child, which keeps // the full `Option<…>` as its leaf `out_ty` (its own `Option` is the // converter's business, not a nesting step ⇒ NOT nullable). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_recv_reply(q: &ZQuery) -> ZReply { todo!() }", "fn z_reply_replier_zid(r: &ZReply) -> Option { todo!() }", "fn z_reply_is_ok(r: &ZReply) -> bool { todo!() }", @@ -673,7 +676,7 @@ fn reply_product_double_option_flatten() { #[test] fn nested_cycle_errors() { // A → B → A nesting is rejected. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_foo() -> ZA { todo!() }", "fn a_to_b(a: &ZA) -> &ZB { todo!() }", "fn b_to_a(b: &ZB) -> &ZA { todo!() }", @@ -715,7 +718,8 @@ fn iterable_whole_element_plan() { // each element delivered WHOLE (no accessor, no leaves): a per-fn // flatten with an empty record list on an element type that has no // deconstructor of its own. - let mut reg = reg_with(&["fn z_session_peers_zid(s: &ZSession) -> Vec { todo!() }"]); + let mut reg: Registry<()> = + reg_with(&["fn z_session_peers_zid(s: &ZSession) -> Vec { todo!() }"]); let mut acc = Deconstructors::default(); acc.outputs.push(OutputDecl { func: ident("z_session_peers_zid"), @@ -764,7 +768,7 @@ fn iterable_decomposed_plan() { // accessor → Iterable with per-element leaves: the string form + the // value itself via `record_id` (an identity leaf, owned at the // root since `Vec` owns its elements). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_session_peers_zid(s: &ZSession) -> Vec { todo!() }", "fn z_zenoh_id_to_string(z: &ZZenohId) -> String { todo!() }", ]); @@ -823,7 +827,7 @@ fn convert_output_single_value() { // `.converter(ZTimestamp, z_timestamp_ntp64)` + `.convert_output()` on // `z_sample_timestamp -> Option<&ZTimestamp>` ⇒ Return delivery, single // leaf, convert_out_ty = Option. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_sample_timestamp(s: &ZSample) -> Option<&ZTimestamp> { todo!() }", "fn z_timestamp_ntp64(t: &ZTimestamp) -> i64 { todo!() }", ]); @@ -866,7 +870,7 @@ fn convert_output_single_value() { #[test] fn multi_leaf_output_is_callback() { // A two-record deconstructor (handle + string) ⇒ Callback delivery (>1 leaf). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_sample_key_expr(s: &ZSample) -> &ZKeyExpr { todo!() }", "fn z_keyexpr_as_str(ke: &ZKeyExpr) -> &str { todo!() }", ]); @@ -902,7 +906,7 @@ fn multi_leaf_output_is_callback() { #[test] fn vec_output_is_iterable_callback() { // A `Vec` return ⇒ Iterable + Callback (a fold), never a single Return. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_session_peers_zid(s: &ZSession) -> Vec { todo!() }", "fn z_zenoh_id_to_string(z: &ZZenohId) -> String { todo!() }", ]); @@ -950,7 +954,7 @@ fn option_vec_output_is_optional_iterable_callback() { // a RECORD-BUILT `Optional(Iterable)` fold (issue #105): the auto-apply // peels the `Option` before probing the `Vec`, the elements decompose // into leaves (M5), and `None` skips the fold to deliver a null result. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_routers_zid(s: &ZSession) -> Option> { todo!() }", "fn z_zenoh_id_to_string(z: &ZZenohId) -> String { todo!() }", ]); @@ -993,7 +997,7 @@ fn option_vec_single_leaf_stays_callback() { // single-Return reclassification is gated on "no Iterable at any layer", // not just a top-level `Iterable` (an `Option>` fold has no single // value to return through `convert_out_ty`). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_routers_zid(s: &ZSession) -> Option> { todo!() }", "fn z_zenoh_id_to_string(z: &ZZenohId) -> String { todo!() }", ]); @@ -1024,7 +1028,7 @@ fn option_vec_whole_element_plan() { // M4 dual of the decomposed case: an `Option>` return with an // inline EMPTY record list delivers each element whole through its own // output converter, wrapped in the `Optional` layer. - let mut reg = + let mut reg: Registry<()> = reg_with(&["fn z_routers_zid(s: &ZSession) -> Option> { todo!() }"]); let mut acc = Deconstructors::default(); acc.outputs.push(OutputDecl { @@ -1067,7 +1071,7 @@ fn value_struct_vec_is_fixed_iterable_fold() { // an Optional layer: the field leaves cross raw per element and the // foreign folder rebuilds + appends them (no Java object is built on the // Rust side); `None` ⇒ a null list. Closes the data_class→Vec milestone. - let mut reg = + let mut reg: Registry<()> = reg_with(&["fn storage_get_vec(s: &Storage) -> Option> { todo!() }"]); let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { name: name.to_string(), @@ -1119,7 +1123,7 @@ fn value_struct_slice_callback_is_fixed_iterable_fold() { // `callback_arg_plans` entry keyed by the `&[Payload]` arg: the // trampoline folds each element's field leaves into a foreign list, the // user callback still sees the whole `List`. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn storage_callback_vec(f: impl Fn(&[Payload]) + Send + Sync + 'static) { todo!() }", ]); let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { @@ -1159,7 +1163,7 @@ fn value_struct_slice_callback_is_fixed_iterable_fold() { assert_eq!(plan.leaves.len(), 2); assert!(plan.leaves.iter().all(|l| l.source == LeafSource::Field)); // A scalar `&Payload` callback arg must stay a Base fixed builder. - let mut reg2 = reg_with(&[ + let mut reg2: Registry<()> = reg_with(&[ "fn storage_callback(f: impl Fn(&Payload) + Send + Sync + 'static) { todo!() }", ]); let vd2 = ValueDecon { @@ -1182,7 +1186,7 @@ fn convert_error_decomposes_result_e() { // The ZError deconstructor (`z_error_message`) auto-applies to every fn // returning `Result<_, ZError>`, storing the plan in `error_plans`. Error // delivery is always Callback (its leaves are the `ze` callback args). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", "fn z_error_message(e: &ZError) -> String { todo!() }", "fn z_infallible(s: &ZSample) -> bool { todo!() }", @@ -1233,7 +1237,7 @@ fn default_output_applies_to_owned_and_borrow_returns() { // Default-everywhere: the ZKeyExpr deconstructor auto-applies to BOTH a // `&ZKeyExpr` (borrow) and an owned `ZKeyExpr` return. (`Result<…>` returns // are excluded — they keep a handle — and `fun_accessor`s are skipped.) - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_borrow_keyexpr(s: &ZSession) -> &ZKeyExpr { todo!() }", "fn z_make_keyexpr(s: &ZSession) -> ZKeyExpr { todo!() }", "fn z_keyexpr_as_str(k: &ZKeyExpr) -> &str { todo!() }", @@ -1278,7 +1282,7 @@ fn callback_arg_plan_derived() { // An `impl Fn(ZSample)` parameter of a declared fn gets a type-level // plan from ZSample's default deconstructor — same leaves a return of // ZSample would produce, but owned (`by_ref = false`). - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_declare_sub(cb: impl Fn(ZSample) + Send + Sync + 'static) { todo!() }", "fn z_sample_key_expr(s: &ZSample) -> &ZKeyExpr { todo!() }", "fn z_sample_kind(s: &ZSample) -> SampleKind { todo!() }", @@ -1361,7 +1365,7 @@ fn callback_arg_borrowed_decomposed() { // deconstructor as the by-value case, but with `by_ref = true` (leaves // read through the reference) and keyed under the actual `&ZSample` arg // type — so `callback_input`/`callback_iface_spec` find it. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_declare_sub(cb: impl Fn(&ZSample) + Send + Sync + 'static) { todo!() }", "fn z_sample_key_expr(s: &ZSample) -> &ZKeyExpr { todo!() }", "fn z_sample_kind(s: &ZSample) -> SampleKind { todo!() }", @@ -1428,7 +1432,7 @@ fn callback_arg_borrowed_decomposed() { #[test] fn callback_arg_identity_fallback() { // No deconstructor for ZQuery ⇒ no plan: the arg is delivered whole. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_declare_queryable(cb: impl Fn(ZQuery) + Send + Sync + 'static) { todo!() }", ]); let acc = Deconstructors::default(); @@ -1440,7 +1444,7 @@ fn callback_arg_identity_fallback() { #[test] fn callback_zero_arg_no_plan() { - let mut reg = + let mut reg: Registry<()> = reg_with(&["fn z_with_close(on_close: impl Fn() + Send + Sync + 'static) { todo!() }"]); let acc = Deconstructors::default(); let declared: std::collections::HashSet = @@ -1453,7 +1457,7 @@ fn callback_zero_arg_no_plan() { fn callback_arg_nonbare_skipped() { // `impl Fn(Vec)`: the arg type key (`Vec`) matches no // deconstructor target ⇒ whole-value fallback, no plan. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_batched(cb: impl Fn(Vec) + Send + Sync + 'static) { todo!() }", "fn z_sample_kind(s: &ZSample) -> SampleKind { todo!() }", ]); @@ -1492,7 +1496,7 @@ fn leaf_vec_fold_synthesizes_whole_element_plans() { // `Vec` / `Option>` returns and an `impl Fn(&[String])` // callback arg synthesize FIXED **whole-element** folds (no decon, element // set, no leaves) — the single-leaf dual of the `data_class` Vec fold. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn hello_get_locators(h: &Hello) -> Vec { todo!() }", "fn session_peers(s: &Session) -> Option> { todo!() }", "fn on_strings(f: impl Fn(&[String]) + Send + Sync + 'static) { todo!() }", @@ -1550,7 +1554,7 @@ fn leaf_vec_fold_synthesizes_whole_element_plans() { fn leaf_vec_fold_skips_unnominated_and_preexisting() { // An un-nominated element is left on the ArrayList path (no plan); a fn // that already has a plan is never overwritten. - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn other(x: &X) -> Vec { todo!() }", "fn strings() -> Vec { todo!() }", ]); @@ -1591,7 +1595,7 @@ fn leaf_vec_fold_skips_unnominated_and_preexisting() { /// that stands on its own for any adapter.) #[test] fn unknown_accessor_errors() { - let mut reg = reg_with(&["fn z_foo() -> ZKeyExpr { todo!() }"]); + let mut reg: Registry<()> = reg_with(&["fn z_foo() -> ZKeyExpr { todo!() }"]); let mut acc = Deconstructors::default(); acc.deconstructors.push(DeconstructorDecl { target: syn::parse_quote!(ZKeyExpr), @@ -1617,7 +1621,7 @@ fn unknown_accessor_errors() { /// delivery form.) #[test] fn duplicate_declarations_collected() { - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn z_keyexpr_as_str(ke: &ZKeyExpr) -> &str { todo!() }", "fn z_session_key(s: &ZSession) -> ZKeyExpr { todo!() }", ]); @@ -1704,7 +1708,7 @@ fn reading_sum_decon() -> SumDecon { /// would fail the resolve on a converter that must not exist. #[test] fn sum_return_is_a_fixed_builder_plan() { - let mut reg = reg_with(&["fn read_one(which: i32) -> Reading { todo!() }"]); + let mut reg: Registry<()> = reg_with(&["fn read_one(which: i32) -> Reading { todo!() }"]); let declared: std::collections::HashSet = ["read_one"].iter().map(|s| ident(s)).collect(); apply_sum_returns(&mut reg, vec![reading_sum_decon()], &declared).expect("apply_sum_returns"); @@ -1743,7 +1747,7 @@ fn sum_return_is_a_fixed_builder_plan() { /// dropped along with the bare type's. #[test] fn sum_return_layers_ride_the_shape_fold() { - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn read_maybe(w: i32) -> Option { todo!() }", "fn read_all(n: i32) -> Vec { todo!() }", ]); @@ -1785,7 +1789,7 @@ fn sum_return_layers_ride_the_shape_fold() { /// unrequire exists for. #[test] fn a_vec_only_sum_return_drops_the_bare_requirement() { - let mut reg = reg_with(&["fn read_all(n: i32) -> Vec { todo!() }"]); + let mut reg: Registry<()> = reg_with(&["fn read_all(n: i32) -> Vec { todo!() }"]); let bare: syn::Type = syn::parse_quote!(Reading); reg.require_output(&bare); assert!( @@ -1814,7 +1818,7 @@ fn a_vec_only_sum_return_drops_the_bare_requirement() { /// groups instead of a whole value built on the Rust side. #[test] fn sum_callback_arg_is_a_fixed_builder_plan() { - let mut reg = reg_with(&[ + let mut reg: Registry<()> = reg_with(&[ "fn read_each(n: i32, f: impl Fn(Reading) + Send + Sync + 'static) { todo!() }", ]); let declared: std::collections::HashSet = diff --git a/prebindgen/src/api/core/write.rs b/prebindgen/src/api/core/write.rs index cbbe747c..1c49ccfa 100644 --- a/prebindgen/src/api/core/write.rs +++ b/prebindgen/src/api/core/write.rs @@ -23,9 +23,9 @@ use crate::api::{ /// Errors surfaced by the file-emission phase. /// -/// Binding validation is NOT here — it runs once in [`Registry::resolve`] +/// Binding validation is NOT here — it runs once in [`Registry::finish`] /// (see [`Prebindgen::validate_resolved`]), so an invalid binding fails -/// before a `Generation` exists and never reaches a writer. +/// before a built generator exists and never reaches a writer. #[derive(Debug)] pub enum WriteError { /// A `TokenStream` produced by an `on_*` trait method failed to parse @@ -61,7 +61,7 @@ pub fn write_rust, E: Prebindgen>( ext: &E, out_path: P, ) -> Result { - // Validation already ran ONCE in `Registry::resolve` — a `Generation` + // Validation already ran ONCE in the generator's `build` — a built generator // (the only source of a resolved registry) is valid by construction, so // this writer is a pure emission. let mut items: Vec = Vec::new(); @@ -78,9 +78,10 @@ pub fn write_rust, E: Prebindgen>( // 2. Per-item Rust output from the adapter — only for items the adapter // explicitly declared. Undeclared items were already announced - // via `cargo:warning=` in `Registry::scan_declared`. - let declared_fns = ext.declared_functions(); - let declared_types = ext.declared_types(); + // via `cargo:warning=` by the generator's own unclaimed-item report. + let declared = registry.declared(); + let declared_fns = &declared.functions; + let declared_types = &declared.types; let flat = registry.flat(); items.extend(parse_items_from_tokens( "on_function", @@ -118,7 +119,7 @@ pub fn write_rust, E: Prebindgen>( // symmetric with functions; an adapter without one (`None`) gets every // const passed through verbatim via the default `on_const`. Prebindgen's // own injected feature guards are not consts at all — see the guards loop. - let declared_consts = ext.declared_consts(); + let declared_consts = &declared.consts; items.extend(parse_items_from_tokens( "on_const", sorted_by_name(flat.constants().map(|c| (&c.name, &c.origin.syntax))) diff --git a/prebindgen/src/api/core/write/tests.rs b/prebindgen/src/api/core/write/tests.rs index 44f2c057..e0b509c9 100644 --- a/prebindgen/src/api/core/write/tests.rs +++ b/prebindgen/src/api/core/write/tests.rs @@ -1,31 +1,30 @@ -use std::{ - collections::HashSet, - time::{SystemTime, UNIX_EPOCH}, -}; +use std::time::{SystemTime, UNIX_EPOCH}; use proc_macro2::TokenStream; use quote::ToTokens; use super::*; -use crate::{api::test_util::cell, SourceLocation}; +use crate::{ + api::{core::registry::RegistryBuilder, test_util::cell}, + SourceLocation, +}; struct IdentityExt; -impl Prebindgen for IdentityExt { - type Metadata = (); - - fn declared_functions(&self) -> HashSet { - [syn::parse_quote!(a_fn), syn::parse_quote!(b_fn)] - .into_iter() - .collect() +impl IdentityExt { + fn declare_into(&self, mut reg: RegistryBuilder<()>) -> RegistryBuilder<()> { + for f in [syn::parse_quote!(a_fn), syn::parse_quote!(b_fn)] { + reg = reg.export(&f); + } + for t in ["AEnum", "AStruct", "BEnum", "BStruct"] { + reg = reg.export_type(TypeKey::parse(t).expect("test type")); + } + reg } +} - fn declared_types(&self) -> HashSet { - ["AEnum", "AStruct", "BEnum", "BStruct"] - .into_iter() - .map(|s| TypeKey::parse(s).expect("test type")) - .collect() - } +impl Prebindgen for IdentityExt { + type Metadata = (); fn on_function(&self, f: &syn::ItemFn, _registry: &Registry) -> TokenStream { f.to_token_stream() @@ -38,22 +37,6 @@ impl Prebindgen for IdentityExt { fn on_enum(&self, e: &syn::ItemEnum, _registry: &Registry) -> TokenStream { e.to_token_stream() } - - fn on_input_type( - &self, - _ty: &syn::Type, - _registry: &Registry, - ) -> Option> { - None - } - - fn on_output_type( - &self, - _ty: &syn::Type, - _registry: &Registry, - ) -> Option> { - None - } } #[test] @@ -67,7 +50,6 @@ fn dedup_and_sort() { reg.input_types.insert( key_a.clone(), cell( - &key_a, true, Some(TypeEntry { destination: wire.clone(), @@ -86,7 +68,6 @@ fn dedup_and_sort() { reg.input_types.insert( key_b.clone(), cell( - &key_b, true, Some(TypeEntry { destination: wire2.clone(), @@ -170,7 +151,10 @@ fn write_rust_sorts_declared_items_by_ident() { loc, ), ]; - let reg: Registry<()> = Registry::from_items(items).expect("index"); + let reg: Registry<()> = IdentityExt + .declare_into(crate::api::test_util::reg_from_items(items).expect("index")) + .scanned() + .expect("scan"); let unique = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -211,16 +195,30 @@ fn bad_generated_tokens_report_emission_phase() { /// on the way out. #[test] fn guards_emit_ungated_and_in_stream_order() { - /// Declares a const mechanism (`Some`) and declares nothing through it. + /// Declares a const mechanism and declares nothing through it, so + /// `KEPT_OUT` must not emit. struct ConstGatingExt; + trait ResolveGating { + fn resolve_gating( + self, + ext: ConstGatingExt, + ) -> Result, crate::core::WriteRustError>; + } + impl ResolveGating for RegistryBuilder<()> { + fn resolve_gating( + self, + ext: ConstGatingExt, + ) -> Result, crate::core::WriteRustError> { + let registry = self.declares_consts().build()?; + let _ = &ext; + Ok(registry) + } + } + impl Prebindgen for ConstGatingExt { type Metadata = (); - fn declared_consts(&self) -> Option> { - // The gate exists and is empty: `KEPT_OUT` must not emit. - Some(HashSet::new()) - } fn on_function(&self, f: &syn::ItemFn, _r: &Registry<()>) -> TokenStream { f.to_token_stream() } @@ -230,20 +228,6 @@ fn guards_emit_ungated_and_in_stream_order() { fn on_enum(&self, e: &syn::ItemEnum, _r: &Registry<()>) -> TokenStream { e.to_token_stream() } - fn on_input_type( - &self, - _ty: &syn::Type, - _r: &Registry<()>, - ) -> Option> { - None - } - fn on_output_type( - &self, - _ty: &syn::Type, - _r: &Registry<()>, - ) -> Option> { - None - } } let loc = SourceLocation::default(); @@ -273,15 +257,14 @@ fn guards_emit_ungated_and_in_stream_order() { loc.clone(), ), ]; - let registry: Registry<()> = Registry::from_items(items).expect("index"); + let registry: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(items).expect("index"); assert_eq!(registry.flat().guards().count(), 2); let dir = crate::api::test_util::unique_test_dir("write_guards"); std::fs::create_dir_all(&dir).unwrap(); - let path = registry - .resolve(ConstGatingExt) - .expect("resolve") - .write_rust(dir.join("gen.rs")) + let registry = registry.resolve_gating(ConstGatingExt).expect("resolve"); + let path = crate::api::core::write::write_rust(®istry, &ConstGatingExt, dir.join("gen.rs")) .expect("write_rust"); let src = std::fs::read_to_string(&path).unwrap(); diff --git a/prebindgen/src/api/gen/kotlin/expr/tests.rs b/prebindgen/src/api/gen/kotlin/expr/tests.rs index 2f1fecd2..22aac3c8 100644 --- a/prebindgen/src/api/gen/kotlin/expr/tests.rs +++ b/prebindgen/src/api/gen/kotlin/expr/tests.rs @@ -1266,7 +1266,7 @@ fn legacy_annotation_bridge_has_exactly_one_caller() { /// a module. /// /// The constructors below are crate-visible, so a hand-listed set of files is -/// not an audit — a direct call from `expr.rs`, `file.rs` or any JniGen module +/// not an audit — a direct call from `expr.rs`, `file.rs` or any JniGenBuilder module /// would simply not be looked at. This walks `src/` instead. fn crate_sources() -> Vec<(String, String)> { // The auditing file is skipped: it necessarily *spells* the constructors it @@ -1345,7 +1345,7 @@ fn static_annotation_text_constructors_are_pinned_crate_wide() { /// producers is the mechanical check** behind #199's global exit. /// /// Scanned **crate-wide**, not over a hand-listed set of files: `KtExpr` is -/// re-exported for JniGen and visible throughout it, so a +/// re-exported for JniGenBuilder and visible throughout it, so a /// `kt::KtExpr::Raw(...)` in `api/lang/jnigen/…` would pass a `gen/kotlin`-only /// audit and the asserted exit would drift silently. Same visibility-boundary /// problem `crate_sources` already solves for `StaticAnnotationText`. diff --git a/prebindgen/src/api/gen/kotlin/model.rs b/prebindgen/src/api/gen/kotlin/model.rs index a86139d8..a0725906 100644 --- a/prebindgen/src/api/gen/kotlin/model.rs +++ b/prebindgen/src/api/gen/kotlin/model.rs @@ -1,6 +1,6 @@ //! Declaration model: [`KtFile`] → [`KtDecl`] (classes, functions, //! properties, type aliases, raw blocks). Chained builders in the same -//! style as the JniGen config builder. Rendering lives in +//! style as the JniGenBuilder config builder. Rendering lives in //! [`super::render`]; this module is pure data. use super::{ diff --git a/prebindgen/src/api/lang/cbindgen/builder.rs b/prebindgen/src/api/lang/cbindgen/builder.rs index 3f215a12..b774de26 100644 --- a/prebindgen/src/api/lang/cbindgen/builder.rs +++ b/prebindgen/src/api/lang/cbindgen/builder.rs @@ -1,6 +1,6 @@ use super::*; -impl Cbindgen { +impl CbindgenBuilder { fn clear_current(&mut self) { self.current = None; } @@ -93,6 +93,33 @@ impl Cbindgen { } /// Declare a `#[prebindgen]` function to convert into the C layer. + /// Every `#[prebindgen]` item captured in `dir` — see + /// `JniGenBuilder::source`. + pub fn source>(mut self, dir: P) -> Self { + self.sources = std::mem::take(&mut self.sources).source(dir); + self + } + + /// The same, for a dependency this crate **renames** in `Cargo.toml`. + pub fn source_named>( + mut self, + dir: P, + crate_name: impl Into, + ) -> Self { + self.sources = std::mem::take(&mut self.sources).source_named(dir, crate_name); + self + } + + /// Add a captured item stream. Accumulates, so it mixes with + /// [`Self::source`]. + pub fn items(mut self, items: I) -> Self + where + I: IntoIterator, + { + self.sources = std::mem::take(&mut self.sources).items(items); + self + } + pub fn function(mut self, ident: syn::Ident) -> Self { assert!( !self.ignored_functions.contains(&ident), @@ -104,7 +131,7 @@ impl Cbindgen { self } - /// Declare a canonical scalar conversion shared with JniGen. A domain on + /// Declare a canonical scalar conversion shared with JniGenBuilder. A domain on /// the [`ConvertDecl`] is validated in both directions; invalid scalar /// values become by-value niches for `Option`/`Result`, with public C /// constants derived from the conversion's naming base. diff --git a/prebindgen/src/api/lang/cbindgen/convert.rs b/prebindgen/src/api/lang/cbindgen/convert.rs index 9732e57e..4b741a8b 100644 --- a/prebindgen/src/api/lang/cbindgen/convert.rs +++ b/prebindgen/src/api/lang/cbindgen/convert.rs @@ -1,6 +1,7 @@ use super::*; +use crate::api::core::registry::Conversions; -impl Cbindgen { +impl CbindgenBuilder { pub(crate) fn prereq_domain_constants(&self, registry: &Registry<()>) -> Vec { let mut items = Vec::new(); for decl in &self.convert_decls { @@ -51,7 +52,7 @@ impl Cbindgen { pub(crate) fn in_custom( &self, ty: &syn::Type, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> Option> { let key = TypeKey::from_type(ty); let decl = self.convert_decls.iter().find(|d| d.key == key)?; @@ -118,7 +119,7 @@ impl Cbindgen { pub(crate) fn out_custom( &self, ty: &syn::Type, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> Option> { let key = TypeKey::from_type(ty); let decl = self.convert_decls.iter().find(|d| d.key == key)?; @@ -187,7 +188,7 @@ impl Cbindgen { &self, decl: &ConvertDecl, spec: &ConvertSpec, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> (syn::Type, syn::Expr, bool) { let target = self.src_ty(&decl.key.to_type()); match spec { @@ -231,7 +232,7 @@ impl Cbindgen { &self, decl: &ConvertDecl, spec: &ConvertSpec, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> (syn::Type, syn::Expr, bool) { let target = self.src_ty(&decl.key.to_type()); match spec { @@ -274,15 +275,15 @@ impl Cbindgen { fn c_domain_niches( &self, decl: &ConvertDecl, - registry: &Registry<()>, + registry: &impl Conversions<()>, direction: Direction, ) -> Niches { let Some(domain) = &decl.domain else { return Niches::empty(); }; let demand = registry - .type_table(direction) - .keys() + .crossing_keys(direction) + .iter() .map(|candidate| option_depth(candidate, &decl.key)) .max() .unwrap_or(0); @@ -310,7 +311,7 @@ impl Cbindgen { ) } - fn conversion_fn_path(&self, registry: &Registry<()>, ident: &syn::Ident) -> syn::Path { + fn conversion_fn_path(&self, registry: &impl Conversions<()>, ident: &syn::Ident) -> syn::Path { let Some(mut module) = registry.origin_module(ident) else { return self.src_fn(ident); }; diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index 5bb74f83..bb61d8ec 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -1,6 +1,7 @@ use super::*; +use crate::api::core::registry::Conversions; -impl Cbindgen { +impl CbindgenBuilder { /// Whether the generated layer hands `char*` data memory to C — a `String` /// return value, or a declared data struct that is produced as output and has /// a `String` field. When true, a `free_memory_function` must be declared. @@ -75,7 +76,7 @@ impl Cbindgen { /// — a declared `enum_type` (a discriminant no variant has) and `bool` /// (anything but `0`/`1`) — are wrapped in [`::core::mem::MaybeUninit`] so /// they do too. That is what makes the mirror's tag the *only* thing - /// [`Cbindgen::in_tagged_union`] has to validate before `assume_init`. + /// [`CbindgenBuilder::in_tagged_union`] has to validate before `assume_init`. /// /// A `bool` reached through a nested `data_struct` payload is covered by /// the same [`bool_wire`] policy, which [`c_field_wire`] and the plain @@ -163,7 +164,7 @@ impl Cbindgen { /// Wire type of a `data_struct` field: the free [`c_field_wire`] policy /// (`String` → `char *`, scalar → itself) plus a declared - /// [`Cbindgen::tagged_union`] field, which crosses **by value** as its + /// [`CbindgenBuilder::tagged_union`] field, which crosses **by value** as its /// `#[repr(C)]` mirror — the same way it crosses as a parameter or a /// return. `None` ⇒ the field type is unsupported in a data struct. /// @@ -190,7 +191,7 @@ impl Cbindgen { /// A nested `data_struct` payload crosses BY VALUE, so the wire itself is /// not a pointer, but its mirror's own fields may be: the union's drop /// then has to reach through and release each of them (see - /// [`Cbindgen::payload_free_stmt`]). Without this a `String` or handle + /// [`CbindgenBuilder::payload_free_stmt`]). Without this a `String` or handle /// inside a struct payload would leak, silently, for exactly the shape /// zenoh-flat#30 needs. pub(super) fn payload_wire_owns( @@ -244,7 +245,7 @@ impl Cbindgen { /// Whether one `data_struct` **field** hands owned memory to C: its own wire /// is a pointer (`String` → `char *`), or it is a declared - /// [`Cbindgen::tagged_union`] with an owning arm — which crosses by value, + /// [`CbindgenBuilder::tagged_union`] with an owning arm — which crosses by value, /// so the pointer it owns is one level further down. fn data_field_owns(&self, fty: &syn::Type, registry: &Registry<()>) -> bool { if matches!(self.data_field_wire(fty), Some(syn::Type::Ptr(_))) { @@ -257,7 +258,7 @@ impl Cbindgen { /// produced at all, and some arm's payload owns memory. /// /// This is the emission condition of that drop - /// ([`Cbindgen::prereq_tagged_unions`]) *and* the test for whether a + /// ([`CbindgenBuilder::prereq_tagged_unions`]) *and* the test for whether a /// containing struct has to call it, so a union nested inside a payload /// cannot be freed through a symbol that was never emitted. `false` for /// anything that is not a declared tagged union. @@ -298,7 +299,7 @@ impl Cbindgen { /// struct. pub(super) fn struct_fields( &self, - registry: &Registry<()>, + registry: &impl Conversions<()>, ty: &syn::Type, ) -> Option> { let ident = type_path_tail(ty)?; @@ -320,9 +321,9 @@ impl Cbindgen { } /// Wire type of a `repr_c_struct` field in the generated **visible** mirror: a - /// scalar passes through; a declared [`Cbindgen::enum_type`] becomes its C enum; + /// scalar passes through; a declared [`CbindgenBuilder::enum_type`] becomes its C enum; /// an opaque pointer `Option>` / `Box` (with `T` a declared - /// [`Cbindgen::opaque_ptr`]) becomes `*mut t_t`. The whole-struct `Transmute` + /// [`CbindgenBuilder::opaque_ptr`]) becomes `*mut t_t`. The whole-struct `Transmute` /// (size/align-equal, asserted) then reinterprets each source field's bits into /// this wire. `None` ⇒ the field type is unsupported in a `repr_c_struct`. /// diff --git a/prebindgen/src/api/lang/cbindgen/mod.rs b/prebindgen/src/api/lang/cbindgen/mod.rs index 899bb402..7b68ad30 100644 --- a/prebindgen/src/api/lang/cbindgen/mod.rs +++ b/prebindgen/src/api/lang/cbindgen/mod.rs @@ -1,4 +1,4 @@ -//! `Cbindgen` — the C / cbindgen language adapter. +//! `CbindgenBuilder` — the C / cbindgen language adapter. //! //! # Experimental API //! @@ -11,23 +11,23 @@ //! parse into a C header plus a static / dynamic library. //! //! Items are **opt-in**: nothing is converted unless it is explicitly declared -//! with [`Cbindgen::function`] / [`Cbindgen::opaque_ptr`] / -//! [`Cbindgen::data_struct`] / [`Cbindgen::enum_type`] / -//! [`Cbindgen::tagged_union`]. The C name of a declared -//! type's generated destructor can be pinned by chaining [`Cbindgen::name`]. +//! with [`CbindgenBuilder::function`] / [`CbindgenBuilder::opaque_ptr`] / +//! [`CbindgenBuilder::data_struct`] / [`CbindgenBuilder::enum_type`] / +//! [`CbindgenBuilder::tagged_union`]. The C name of a declared +//! type's generated destructor can be pinned by chaining [`CbindgenBuilder::name`]. //! //! ## C ABI conventions //! -//! * **Pointer struct** (declared with [`Cbindgen::opaque_ptr`]): a `Box`-owned +//! * **Pointer struct** (declared with [`CbindgenBuilder::opaque_ptr`]): a `Box`-owned //! Rust value whose lifecycle is owned by the C side. The C type `T` is //! **opaque/incomplete** and the handle is a bare `T *` = `Box::into_raw`. A //! typed `_drop(T *)` destructor (running the Rust `Drop`) is generated //! per handle. -//! * **Data struct** (declared with [`Cbindgen::data_struct`]): a by-value +//! * **Data struct** (declared with [`CbindgenBuilder::data_struct`]): a by-value //! `#[repr(C)]` struct whose fields are mapped to C-ABI wire types //! (`String` → `*mut c_char`). No per-struct destructor — each `char*` field -//! is released individually via the [`Cbindgen::free_memory_function`]. -//! * **Enum type** (declared with [`Cbindgen::enum_type`]): a fieldless enum, +//! is released individually via the [`CbindgenBuilder::free_memory_function`]. +//! * **Enum type** (declared with [`CbindgenBuilder::enum_type`]): a fieldless enum, //! mirrored as a `#[repr(C)]` enum that cbindgen renders as the C enum. //! Rust → C hands over the mirror directly (Rust only ever builds declared //! variants). C → Rust must **not** do the reverse: a C `enum` is an `int` at @@ -39,9 +39,9 @@ //! its raw `c_int` is validated against the mirror's variants before the Rust //! value is built. An unmatched value is a fallible-input error (see below), //! so a function taking an enum by value needs either a `Result` return or -//! [`Cbindgen::panic`]. This relies on cbindgen's C rendering; the `C++` +//! [`CbindgenBuilder::panic`]. This relies on cbindgen's C rendering; the `C++` //! language mode is not supported. -//! * **Tagged union** (declared with [`Cbindgen::tagged_union`]): a +//! * **Tagged union** (declared with [`CbindgenBuilder::tagged_union`]): a //! data-carrying enum crossing by value as a `#[repr(C)]` enum with payload //! variants, which cbindgen renders as a tag enum plus a `union` of the //! variant bodies. When any variant's payload wire owns memory, a typed @@ -55,7 +55,7 @@ //! out-of-range one as nothing to release. //! * **Direct `String` output**: a bare `char *` — a `malloc`'d, null-terminated //! raw block (no wrapper struct), freed via the `free_memory_function`. -//! * **[`Cbindgen::free_memory_function`]**: the single, type-agnostic raw memory +//! * **[`CbindgenBuilder::free_memory_function`]**: the single, type-agnostic raw memory //! freer (C `free`) for every `char*` the layer hands out (string returns and //! data-struct `String` fields). It runs no destructor and needs no length. //! Required whenever such string memory is produced. @@ -72,7 +72,7 @@ //! ## Error handling (multiple error types) //! //! Any type used as the `E` of a `Result` return **must be declared** as an -//! error type via [`Cbindgen::data_struct`] + [`Cbindgen::error`] — otherwise the +//! error type via [`CbindgenBuilder::data_struct`] + [`CbindgenBuilder::error`] — otherwise the //! build fails. Error types are ordinary data structs (marshalled by value) and //! must additionally implement `From`. //! @@ -85,12 +85,12 @@ //! directly through `E`'s output converter. //! //! If a function can produce such an internal message but does **not** return -//! `Result`, that is a build error — suppress it by chaining [`Cbindgen::panic`] +//! `Result`, that is a build error — suppress it by chaining [`CbindgenBuilder::panic`] //! after the function declaration, which makes the wrapper `panic!` on the //! internal error instead. //! //! References to the original Rust types in generated bodies are written -//! fully-qualified against [`Cbindgen::source_module`] so the generated file can +//! fully-qualified against [`CbindgenBuilder::source_module`] so the generated file can //! define its own identically-named `#[repr(C)]` wrapper structs without //! colliding with the source crate's types. @@ -110,7 +110,7 @@ use crate::api::{ core::{ niches::{NicheSlot, Niches}, prebindgen::{ConverterImpl, Prebindgen}, - registry::{extract_fn_trait_args, Direction, Registry, TypeKey}, + registry::{extract_fn_trait_args, Conversions, Direction, Registry, TypeKey}, }, lang::jnigen::{ConvertDecl, ConvertSpec}, }; @@ -125,14 +125,14 @@ type CallbackKey = Vec; struct TypeCfg { /// Per-declaration **base** token override, fed to the name manglers /// (`mangle_type_name` / `mangle_destructor` / `mangle_take`) in place of the - /// `mangle_rust_type`-derived base. Set by [`Cbindgen::base_name`]. `None` ⇒ + /// `mangle_rust_type`-derived base. Set by [`CbindgenBuilder::base_name`]. `None` ⇒ /// the base comes from `mangle_rust_type(short)` (or the short name). base: Option, } /// What an inline-opaque by-value type holds, which decides whether its consume /// path needs a gravestone write-back (and thus a [`crate::core::Gravestone`] -/// impl). See [`Cbindgen::opaque_data_struct`] / [`Cbindgen::opaque_owned_struct`]. +/// impl). See [`CbindgenBuilder::opaque_data_struct`] / [`CbindgenBuilder::opaque_owned_struct`]. #[derive(Clone, Copy, PartialEq, Eq)] enum OpaqueKind { /// **Plain data** — holds no external resource (typically `Copy`, e.g. a @@ -162,13 +162,13 @@ struct ValueOpaqueCfg { kind: OpaqueKind, /// When `true`, the `opaque` counterpart is **not** supplied externally but is /// an auto-generated **visible-field** `#[repr(C)]` mirror of the source struct, - /// emitted by [`Cbindgen::prereq_value_opaque`]. Set by - /// [`Cbindgen::repr_c_struct`]; `false` for `opaque_data_struct` / + /// emitted by [`CbindgenBuilder::prereq_value_opaque`]. Set by + /// [`CbindgenBuilder::repr_c_struct`]; `false` for `opaque_data_struct` / /// `opaque_owned_struct` (counterpart defined elsewhere). generate_mirror: bool, /// Opt-out of the restricted-validity field audit (#170 instance 3, #158 - /// instance 3). Set by [`Cbindgen::assume_c_field_validity`]. See - /// [`Cbindgen::restricted_validity_field`] for what the audit rejects and + /// instance 3). Set by [`CbindgenBuilder::assume_c_field_validity`]. See + /// [`CbindgenBuilder::restricted_validity_field`] for what the audit rejects and /// why the escape hatch exists. assume_c_field_validity: bool, /// Name config (`.base_name()` override; default naming via the manglers). @@ -180,14 +180,14 @@ struct ValueOpaqueCfg { struct CbCfg { /// Per-declaration **base** token override fed to `mangle_callback` (as the /// sole base, replacing the args' derived bases). Set by - /// [`Cbindgen::base_name`]. `None` ⇒ bases come from the arguments. + /// [`CbindgenBuilder::base_name`]. `None` ⇒ bases come from the arguments. base: Option, /// Argument indices delivered to the C `call` as a **takeable owned pointer** /// (`*mut z_x_t`) instead of by value: the callee may take the value (move it /// out via `z_x_take`, leaving a gravestone) or just read it, and the /// trampoline drops it after the call (no-op if taken). Set by - /// [`Cbindgen::takeable_param`]; each such arg type must be an inline-opaque - /// type ([`Cbindgen::opaque_owned_struct`] / [`Cbindgen::opaque_data_struct`]). + /// [`CbindgenBuilder::takeable_param`]; each such arg type must be an inline-opaque + /// type ([`CbindgenBuilder::opaque_owned_struct`] / [`CbindgenBuilder::opaque_data_struct`]). takeable: std::collections::BTreeSet, } @@ -195,17 +195,17 @@ struct CbCfg { #[derive(Clone, Default)] struct FnCfg { /// Per-declaration **base** token override fed to `mangle_function` in place of - /// the Rust fn ident. Set by [`Cbindgen::base_name`]. `None` ⇒ the fn ident. + /// the Rust fn ident. Set by [`CbindgenBuilder::base_name`]. `None` ⇒ the fn ident. base: Option, /// Allow the generated wrapper to `panic!` on an internal error message - /// (set by [`Cbindgen::panic`]). Only meaningful for non-`Result` functions + /// (set by [`CbindgenBuilder::panic`]). Only meaningful for non-`Result` functions /// that have a fallible input. panic: bool, } -/// The declaration a chained modifier ([`Cbindgen::name`] / [`Cbindgen::error`] -/// / [`Cbindgen::panic`]) applies to. Set by each declaration method, reset to -/// `None` by root-level modifiers (e.g. [`Cbindgen::source_module`]). +/// The declaration a chained modifier ([`CbindgenBuilder::name`] / [`CbindgenBuilder::error`] +/// / [`CbindgenBuilder::panic`]) applies to. Set by each declaration method, reset to +/// `None` by root-level modifiers (e.g. [`CbindgenBuilder::source_module`]). #[derive(Clone)] enum CurrentDecl { Ptr(TypeKey), @@ -257,7 +257,7 @@ fn route_message(route: &ErrRoute<'_>) -> TokenStream { } /// How a parameter uses the resource it names — the axis -/// [`Cbindgen::alias_preflight`] states its rule on. +/// [`CbindgenBuilder::alias_preflight`] states its rule on. #[derive(Clone, Copy, PartialEq, Eq)] enum AliasAccess { /// Taken by value: the callee owns it afterwards, and the C-side handle is @@ -281,18 +281,65 @@ impl AliasAccess { } } -/// C / cbindgen language adapter. Build it with [`Cbindgen::new`], declare the +/// C / cbindgen language adapter. Build it with [`CbindgenBuilder::new`], declare the /// items to convert with the fluent methods, then drive it through -/// [`Registry::resolve`](crate::core::Registry::resolve) → -/// [`Generation::write_rust`](crate::core::Generation::write_rust). -#[derive(Default)] +/// [`CbindgenBuilder::build`] → [`Cbindgen::write_rust`]. +/// +/// A resolved C binding: every crossing has a conversion, and the header-facing +/// Rust file can be written. +/// +/// Built by [`CbindgenBuilder::build`]. Read-only, so `write_rust` is a pure +/// emission over a complete registry. pub struct Cbindgen { + pub(crate) gen: CbindgenBuilder, + pub(crate) registry: crate::core::Registry<()>, +} + +// Opaque — exists so `Result::expect_err` works in tests. +impl std::fmt::Debug for Cbindgen { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("Cbindgen(..)") + } +} + +impl Cbindgen { + /// Describe a C binding. + pub fn builder() -> CbindgenBuilder { + CbindgenBuilder::new() + } + + /// Write the generated Rust file — the `extern "C"` wrappers and their + /// converters, which `cbindgen` then reads to emit the header. + pub fn write_rust( + &self, + out_path: impl AsRef, + ) -> Result { + Ok(crate::api::core::write::write_rust( + &self.registry, + &self.gen, + out_path, + )?) + } + + /// The resolved registry — conversions, decompositions, and the model. + pub fn registry(&self) -> &crate::core::Registry<()> { + &self.registry + } + + /// What the binding declared. + pub fn declarations(&self) -> &CbindgenBuilder { + &self.gen + } +} + +#[derive(Default)] +pub struct CbindgenBuilder { /// Module path the original `#[prebindgen]` items live under. Used to /// fully-qualify bare references to source types in generated bodies. source_module: Option, /// `#[prebindgen]` functions explicitly declared for conversion. functions: HashMap, - /// Canonical scalar conversions shared with JniGen. + /// Canonical scalar conversions shared with JniGenBuilder. convert_decls: Vec, /// Per-conversion C naming base used for generated niche constants. convert_bases: HashMap, @@ -310,7 +357,7 @@ pub struct Cbindgen { enums: HashMap, /// Data-carrying enum types crossing by value as a `#[repr(C)]` enum with /// payload variants, which cbindgen renders as the idiomatic C tag + - /// `union`. Declared with [`Cbindgen::tagged_union`]. + /// `union`. Declared with [`CbindgenBuilder::tagged_union`]. tagged_unions: HashMap, /// Declared callback signatures (`impl Fn(...) + Send + Sync + 'static`), /// keyed by their argument-type list. Each emits one `#[repr(C)]` closure @@ -351,6 +398,9 @@ pub struct Cbindgen { mangle_callback: Option, /// Rust function ident → exported `#[no_mangle]` symbol. mangle_function: Option, + /// Where the `#[prebindgen]` items come from — see + /// `JniGenBuilder::source`. + pub(crate) sources: crate::api::core::flat::FlatBuilder, } /// A mangler over a single name component (Rust short name, base, or fn ident). @@ -391,7 +441,7 @@ fn type_short(ty: &syn::Type) -> String { } /// The indexed `syn::ItemEnum` for a declared enum type, by tail ident. -fn enum_item<'r>(registry: &'r Registry<()>, ty: &syn::Type) -> Option<&'r syn::ItemEnum> { +fn enum_item<'r>(registry: &'r impl Conversions<()>, ty: &syn::Type) -> Option<&'r syn::ItemEnum> { let ident = type_path_tail(ty)?; registry.flat().enum_item(&ident) } @@ -414,7 +464,7 @@ fn assert_payload_enum(e: &syn::ItemEnum) { /// If `fty` is an opaque-pointer payload — `Box` or `Option>` with /// `T` a path type — return `T`. The shape check only; whether `T` is a -/// declared `opaque_ptr` is [`Cbindgen::mirror_field_wire`]'s call, and this +/// declared `opaque_ptr` is [`CbindgenBuilder::mirror_field_wire`]'s call, and this /// is only reached for a field that already passed it. fn opaque_ptr_payload_inner(fty: &syn::Type) -> Option { if is_option(fty) { @@ -498,7 +548,7 @@ fn assert_unit_enum(e: &syn::ItemEnum) { /// PascalCase → snake_case (`ZKeyExpr` → `z_key_expr`). /// Convert a `PascalCase` / `camelCase` identifier to `snake_case` (a /// convention-free helper, re-exported as `prebindgen::lang::snake_case` for -/// consumers composing their own [`Cbindgen::mangle_rust_type`] rules). +/// consumers composing their own [`CbindgenBuilder::mangle_rust_type`] rules). /// Thin alias for the core spelling, which sum-variant leaf naming shares. pub fn snake_case(s: &str) -> String { crate::api::core::types_util::pascal_to_snake(s) diff --git a/prebindgen/src/api/lang/cbindgen/selector.rs b/prebindgen/src/api/lang/cbindgen/selector.rs index d6dbe695..6334ddab 100644 --- a/prebindgen/src/api/lang/cbindgen/selector.rs +++ b/prebindgen/src/api/lang/cbindgen/selector.rs @@ -1,14 +1,15 @@ -//! Structural converter-selection policy for [`Cbindgen`]. +//! Structural converter-selection policy for [`CbindgenBuilder`]. use super::*; +use crate::api::core::registry::Conversions; -impl Cbindgen { +impl CbindgenBuilder { /// Select the input converter for `ty`: terminal categories, then built-in /// C structural wrappers. pub(crate) fn select_input_type( &self, ty: &syn::Type, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> Option> { self.in_custom(ty, registry) .or_else(|| self.in_opaque_handle(ty)) @@ -28,7 +29,7 @@ impl Cbindgen { pub(crate) fn select_output_type( &self, ty: &syn::Type, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> Option> { self.out_custom(ty, registry) .or_else(|| self.out_terminal(ty, registry)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs b/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs index f11ff2ea..88ad4f2c 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/aliasing.rs @@ -38,9 +38,10 @@ fn build(fns: &[&str]) -> String { idents.push(f.sig.ident.clone()); items.push((syn::Item::Fn(f), loc.clone())); } - let registry = Registry::<()>::from_items(declare_referenced(items)).expect("index items"); + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let mut cbindgen = Cbindgen::new() + let mut cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(myflat)) .free_memory_function("my_free") .mangle_type_name(|base| format!("{base}_t")) diff --git a/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs b/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs index 95aa3557..745bf13e 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/boundary_invariants.rs @@ -141,12 +141,12 @@ fn every_input_category() -> String { ), ]; - let registry = Registry::<()>::from_items(declare_referenced( + let registry = crate::api::test_util::reg_from_items(declare_referenced( items.into_iter().map(|i| (i, loc.clone())), )) .expect("index items"); - let mut cbindgen = Cbindgen::new() + let mut cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) diff --git a/prebindgen/src/api/lang/cbindgen/tests/builder.rs b/prebindgen/src/api/lang/cbindgen/tests/builder.rs index 37e37764..49c6d16a 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/builder.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/builder.rs @@ -10,9 +10,12 @@ fn function_name_renames_symbol() { unimplemented!() } ); - let reg = Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); - let cb = Cbindgen::new() + let reg = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); + let cb = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(rust_init)) .base_name("z_init"); @@ -27,7 +30,7 @@ fn function_name_renames_symbol() { #[test] fn error_after_ptr_struct_panics() { assert!(catch(|| { - let _ = Cbindgen::new() + let _ = CbindgenBuilder::new() .opaque_ptr(syn::parse_quote!(ZKeyExpr)) .error(); })); @@ -36,7 +39,7 @@ fn error_after_ptr_struct_panics() { #[test] fn panic_after_data_struct_panics() { assert!(catch(|| { - let _ = Cbindgen::new() + let _ = CbindgenBuilder::new() .data_struct(syn::parse_quote!(Error)) .panic(); })); @@ -47,7 +50,7 @@ fn name_with_no_declaration_panics() { // `source_module` is a root modifier — it resets the current declaration, // so a trailing `.base_name()` has nothing to apply to. assert!(catch(|| { - let _ = Cbindgen::new() + let _ = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .base_name("x"); })); @@ -56,7 +59,7 @@ fn name_with_no_declaration_panics() { #[test] fn function_and_ignore_function_conflict_panics() { assert!(catch(|| { - let _ = Cbindgen::new() + let _ = CbindgenBuilder::new() .function(syn::parse_quote!(z_open)) .ignore_function(syn::parse_quote!(z_open)); })); @@ -65,7 +68,7 @@ fn function_and_ignore_function_conflict_panics() { #[test] fn data_struct_and_ignore_type_conflict_panics() { assert!(catch(|| { - let _ = Cbindgen::new() + let _ = CbindgenBuilder::new() .data_struct(syn::parse_quote!(Error)) .ignore_type(syn::parse_quote!(Error)); })); @@ -81,14 +84,14 @@ fn free_memory_function_required() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); // String output (and an Error with a String field) but no free fn declared. - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .data_struct(syn::parse_quote!(Error)) .base_name("z_error") @@ -96,8 +99,8 @@ fn free_memory_function_required() { .function(syn::parse_quote!(z_describe)); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let _ = registry - .resolve(cbindgen) + let _ = cbindgen + .build_with(registry) .and_then(|gen| gen.write_rust(std::env::temp_dir().join("nofree.rs"))); })); assert!( @@ -122,13 +125,13 @@ fn manglers_generate_all_names() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") // One base rule fixes the `KeyExpr`→`keyexpr` irregular in a single @@ -211,9 +214,12 @@ fn qualified_signature_spelling_matches_bare_opaque_ptr() { unimplemented!() } ); - let reg = Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); - let cb = Cbindgen::new() + let reg = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); + let cb = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_ptr(syn::parse_quote!(ZKeyExpr)) .function(syn::parse_quote!(z_keyexpr_len)) @@ -244,13 +250,13 @@ fn enum_mirror_preserves_the_source_discriminant_domain() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(f), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(myflat)) .mangle_type_name(|base| format!("{base}_t")) .enum_type(syn::parse_quote!(Wide)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs b/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs index d35b8b42..1afa6041 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/callbacks.rs @@ -17,13 +17,13 @@ fn takeable_callback_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(func), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_owned_struct(syn::parse_quote!(Sample), syn::parse_quote!(z_sample_t)) .callback(syn::parse_quote!(impl Fn(Sample) + Send + Sync + 'static)) @@ -77,13 +77,13 @@ fn callback_subscriber_emits_closure_structs() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZSession)) @@ -173,13 +173,13 @@ fn callback_scalar_arg_not_module_qualified() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .data_struct(syn::parse_quote!(Error)) @@ -206,7 +206,7 @@ fn callback_scalar_arg_not_module_qualified() { /// Without a `.name(...)` override the closure-struct C name is composed /// generically from the args' configured C type names (`closure_`) -/// — `lang::Cbindgen` invents no target-language convention of its own. +/// — `lang::CbindgenBuilder` invents no target-language convention of its own. #[test] fn callback_struct_name_defaults_generically() { let loc = SourceLocation::default(); @@ -218,13 +218,13 @@ fn callback_struct_name_defaults_generically() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZSession)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/errors.rs b/prebindgen/src/api/lang/cbindgen/tests/errors.rs index 3df6f4cc..e4300d41 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/errors.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/errors.rs @@ -10,14 +10,14 @@ fn result_error_not_declared_is_build_error() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); // Error declared as data_struct but NOT marked `.error()`. - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZKeyExpr)) @@ -27,8 +27,8 @@ fn result_error_not_declared_is_build_error() { .function(syn::parse_quote!(z_keyexpr_try_from)); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let _ = registry - .resolve(cbindgen) + let _ = cbindgen + .build_with(registry) .and_then(|gen| gen.write_rust(std::env::temp_dir().join("nope.rs"))); })); assert!( @@ -49,25 +49,28 @@ fn fallible_input_without_result_needs_panic() { ); // No `.panic()` → build error. - let reg1 = Registry::<()>::from_items(declare_referenced([( + let reg1 = crate::api::test_util::reg_from_items(declare_referenced([( syn::Item::Fn(func.clone()), loc.clone(), )])) .expect("index items"); - let cb1 = Cbindgen::new() + let cb1 = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(z_log)); let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let _ = reg1 - .resolve(cb1) + let _ = cb1 + .build_with(reg1) .and_then(|gen| gen.write_rust(std::env::temp_dir().join("nope2.rs"))); })); assert!(err.is_err(), "expected a build error without .panic()"); // With `.panic()` → wrapper aborts on decode failure. - let reg2 = Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); - let cb2 = Cbindgen::new() + let reg2 = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); + let cb2 = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(z_log)) .panic(); @@ -98,14 +101,14 @@ fn error_out_param_is_null_guarded() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(ptr_fn), loc.clone()), (syn::Item::Fn(unit_fn), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZKeyExpr)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/inputs.rs b/prebindgen/src/api/lang/cbindgen/tests/inputs.rs index 02c6a6bb..510b07f0 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/inputs.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/inputs.rs @@ -10,11 +10,13 @@ fn slice_u8_input_two_params() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_ptr(syn::parse_quote!(ZZBytes)) .base_name("z_zbytes") @@ -47,13 +49,13 @@ fn option_opaque_input_reuses_pointer() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZZBytes)) @@ -91,11 +93,13 @@ fn option_scalar_input_boxed_pointer() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(z_op)); @@ -123,11 +127,13 @@ fn str_borrow_input_lowering() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .function(syn::parse_quote!(z_init_logs)) .panic(); @@ -164,13 +170,13 @@ fn relation_to_lowering() { } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Enum(enum_item), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_ptr(syn::parse_quote!(ZKeyExpr)) .base_name("z_keyexpr") @@ -224,14 +230,14 @@ fn enum_input_validates_the_discriminant() { } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Enum(enum_item), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .data_struct(syn::parse_quote!(Error)) @@ -293,12 +299,12 @@ fn enum_input_without_error_channel_requires_panic() { ); let build = |allow_panic: bool| { let loc = SourceLocation::default(); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func.clone()), loc.clone()), (syn::Item::Enum(enum_item.clone()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .enum_type(syn::parse_quote!(SetIntersectionLevel)) .base_name("z_intersection") @@ -334,13 +340,13 @@ fn mutable_opaque_borrow_input_lowering() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZConfig)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/lowering.rs b/prebindgen/src/api/lang/cbindgen/tests/lowering.rs index a4dd7fa9..01a96b26 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/lowering.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/lowering.rs @@ -17,8 +17,8 @@ fn bounded_duration_option_is_one_scalar_with_named_niche() { (item, loc.clone()) }) .collect(); - let registry = Registry::<()>::from_items(declare_referenced(items)).unwrap(); - let cbindgen = Cbindgen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).unwrap(); + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(myflat)) .convert( crate::convert!(Duration) @@ -75,8 +75,8 @@ fn bounded_float_option_uses_a_finite_bit_exact_niche() { (item, loc.clone()) }) .collect(); - let registry = Registry::<()>::from_items(declare_referenced(items)).unwrap(); - let cbindgen = Cbindgen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).unwrap(); + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(myflat)) .convert( crate::convert!(Ratio) @@ -121,8 +121,8 @@ fn custom_conversion_without_domain_stays_infallible() { (item, loc.clone()) }) .collect(); - let registry = Registry::<()>::from_items(declare_referenced(items)).unwrap(); - let cbindgen = Cbindgen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).unwrap(); + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(myflat)) .convert( crate::convert!(Ratio) @@ -151,8 +151,9 @@ fn custom_conversion_without_domain_stays_infallible() { /// An adapter with no declarations writes an empty (whitespace-only) file. #[test] fn empty_adapter_writes_empty_file() { - let cbindgen = Cbindgen::new(); - let registry: Registry<()> = Registry::empty(); + let cbindgen = CbindgenBuilder::new(); + let registry: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(Vec::new()).expect("empty"); let src = write(cbindgen, registry, "empty"); assert!(src.trim().is_empty(), "expected empty output, got:\n{src}"); } @@ -169,13 +170,13 @@ fn keyexpr_try_from_lowering() { } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZKeyExpr)) @@ -241,11 +242,13 @@ fn opaque_error_lowering() { } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZKeyExpr)) diff --git a/prebindgen/src/api/lang/cbindgen/tests/mod.rs b/prebindgen/src/api/lang/cbindgen/tests/mod.rs index ba9a675e..278d968f 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/mod.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/mod.rs @@ -1,6 +1,9 @@ use super::*; pub(crate) use crate::api::test_util::declare_referenced; -use crate::{api::test_util::unique_test_dir, SourceLocation}; +use crate::{ + api::{core::registry::RegistryBuilder, test_util::unique_test_dir}, + SourceLocation, +}; mod aliasing; mod boundary_invariants; @@ -13,11 +16,11 @@ mod returns; mod structs; mod tagged_unions; -fn write(cbindgen: Cbindgen, registry: Registry<()>, tag: &str) -> String { +fn write(cbindgen: CbindgenBuilder, registry: RegistryBuilder<()>, tag: &str) -> String { let dir = unique_test_dir(&format!("cbindgen_{tag}")); std::fs::create_dir_all(&dir).unwrap(); let out = dir.join(format!("{tag}.rs")); - let gen = registry.resolve(cbindgen).expect("resolve"); + let gen = cbindgen.build_with(registry).expect("resolve"); let path = gen.write_rust(&out).expect("write_rust"); std::fs::read_to_string(&path).unwrap() } diff --git a/prebindgen/src/api/lang/cbindgen/tests/returns.rs b/prebindgen/src/api/lang/cbindgen/tests/returns.rs index 1245b856..6d52dde7 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/returns.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/returns.rs @@ -10,13 +10,13 @@ fn result_unit_omits_out_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .data_struct(syn::parse_quote!(Error)) @@ -47,13 +47,13 @@ fn result_string_uses_owned_string_wire() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .data_struct(syn::parse_quote!(Error)) @@ -93,11 +93,13 @@ fn option_string_returns_pointer_null_for_none() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZEncoding)) @@ -137,13 +139,13 @@ fn result_option_uses_out_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZThing)) @@ -184,11 +186,13 @@ fn vec_string_returns_ptr_and_len() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZHello)) @@ -233,11 +237,13 @@ fn vec_u8_returns_scalar_array() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZZBytes)) @@ -262,11 +268,13 @@ fn cow_u8_returns_scalar_array() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZZBytes)) @@ -296,13 +304,13 @@ fn result_vec_uses_out_params() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZThing)) @@ -337,11 +345,13 @@ fn option_vec_uses_present_and_out() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZHello)) @@ -376,13 +386,13 @@ fn result_option_vec_full() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZThing)) @@ -417,13 +427,13 @@ fn result_pointer_returns_null_on_error() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Fn(func), loc.clone()), (syn::Item::Struct(error_struct()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .free_memory_function("z_free") .opaque_ptr(syn::parse_quote!(ZKeyExpr)) @@ -455,11 +465,13 @@ fn borrowed_ref_output_is_const_non_owning() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_ptr(syn::parse_quote!(ZSample)) .base_name("z_sample_t") @@ -494,11 +506,13 @@ fn borrowed_option_ref_output_nullable() { unimplemented!() } ); - let registry = - Registry::<()>::from_items(declare_referenced([(syn::Item::Fn(func), loc.clone())])) - .expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced([( + syn::Item::Fn(func), + loc.clone(), + )])) + .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_ptr(syn::parse_quote!(ZSample)) .base_name("z_sample_t") diff --git a/prebindgen/src/api/lang/cbindgen/tests/structs.rs b/prebindgen/src/api/lang/cbindgen/tests/structs.rs index 715b4fb2..507244b0 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/structs.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/structs.rs @@ -22,14 +22,14 @@ fn opaque_owned_transmute_by_value() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(out_fn), loc.clone()), (syn::Item::Fn(in_fn), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_owned_struct(syn::parse_quote!(Payload), syn::parse_quote!(OpaquePayload)) .base_name("z_payload_t") @@ -117,14 +117,14 @@ fn opaque_data_no_gravestone_writeback() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(out_fn), loc.clone()), (syn::Item::Fn(in_fn), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .opaque_data_struct(syn::parse_quote!(Stamp), syn::parse_quote!(z_stamp_t)) .base_name("z_stamp_t") @@ -196,7 +196,7 @@ fn repr_c_struct_visible_mirror_and_zero_copy_borrow() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(make_fn), loc.clone()), (syn::Item::Fn(put_fn), loc.clone()), @@ -205,7 +205,7 @@ fn repr_c_struct_visible_mirror_and_zero_copy_borrow() { ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -284,14 +284,14 @@ fn repr_c_struct_owned_inferred_field_nulls_without_default() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(put_fn), loc.clone()), (syn::Item::Fn(string_fn), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -337,13 +337,13 @@ fn repr_c_struct_plain_data_has_no_writeback() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(take_fn), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -385,14 +385,14 @@ fn repr_c_struct_bare_box_field_keeps_full_gravestone() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(put_fn), loc.clone()), (syn::Item::Fn(string_fn), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -450,7 +450,7 @@ fn repr_c_struct_mut_ref_and_maybe_uninit_out_param() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(upd_fn), loc.clone()), (syn::Item::Fn(into_fn), loc.clone()), @@ -458,7 +458,7 @@ fn repr_c_struct_mut_ref_and_maybe_uninit_out_param() { ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -517,7 +517,7 @@ fn repr_c_struct_restricted_validity_field_is_rejected() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), ( syn::Item::Enum(syn::parse_quote!( @@ -532,7 +532,7 @@ fn repr_c_struct_restricted_validity_field_is_rejected() { ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -569,13 +569,13 @@ fn repr_c_struct_restricted_validity_field_accepted_when_acknowledged() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(take), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -612,7 +612,7 @@ fn repr_c_struct_restricted_validity_field_audited_even_when_output_only() { } ); let registry = || { - Registry::<()>::from_items(declare_referenced([ + crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st.clone()), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), ])) @@ -620,7 +620,7 @@ fn repr_c_struct_restricted_validity_field_audited_even_when_output_only() { }; let declare = |acknowledged: bool| { - let mut c = Cbindgen::new() + let mut c = CbindgenBuilder::new() .source_module(syn::parse_quote!(zenoh_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) diff --git a/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs b/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs index 798578d9..80e7bab5 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs @@ -39,7 +39,7 @@ fn tagged_union_mirror_and_converters() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(make), loc.clone()), @@ -47,7 +47,7 @@ fn tagged_union_mirror_and_converters() { ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -133,14 +133,14 @@ fn owning_payload_gets_typed_drop() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(make), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -189,13 +189,13 @@ fn plain_data_union_has_no_drop() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(make), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -223,7 +223,7 @@ fn tagged_union_as_data_struct_field() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Struct(st), loc.clone()), @@ -231,7 +231,7 @@ fn tagged_union_as_data_struct_field() { ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -292,7 +292,7 @@ fn a_union_nested_in_a_struct_payload_is_freed() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Struct(drawing), loc.clone()), @@ -302,7 +302,7 @@ fn a_union_nested_in_a_struct_payload_is_freed() { ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -345,13 +345,13 @@ fn plain_data_struct_decode_stays_infallible() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(f), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -381,13 +381,13 @@ fn declarators_do_not_accept_each_others_shape() { // Payload enum handed to `.enum_type()`. let payload_as_enum = || { - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(shape_enum()), loc.clone()), (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -404,12 +404,12 @@ fn declarators_do_not_accept_each_others_shape() { } ); let unit_as_union = || { - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(operation_enum()), loc.clone()), (syn::Item::Fn(unit_fn.clone()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .mangle_type_name(|base| format!("{base}_t")) .tagged_union(syn::parse_quote!(Operation)) @@ -442,12 +442,12 @@ fn each_payload_rejection_names_its_own_reason() { Many(Vec), } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -492,12 +492,12 @@ fn unsupported_payload_is_a_generation_error() { } ); let boom = || { - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(e.clone()), loc.clone()), (syn::Item::Fn(make.clone()), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .mangle_type_name(|base| format!("{base}_t")) .tagged_union(syn::parse_quote!(Weird)) @@ -533,14 +533,14 @@ fn null_opaque_payload_is_reported_not_materialised() { unimplemented!() } ); - let registry = Registry::<()>::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Enum(e), loc.clone()), (syn::Item::Fn(make), loc.clone()), (syn::Item::Fn(take), loc.clone()), ])) .expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .mangle_type_name(|base| format!("{base}_t")) .mangle_destructor(|base| format!("{base}_drop")) @@ -633,9 +633,10 @@ fn payload_wires_come_from_the_converter_destination() { loc.clone(), ), ]; - let registry = Registry::<()>::from_items(declare_referenced(items)).expect("index items"); + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) @@ -726,9 +727,10 @@ fn bool_payload_is_normalised_not_materialised() { loc.clone(), ), ]; - let registry = Registry::<()>::from_items(declare_referenced(items)).expect("index items"); + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let cbindgen = Cbindgen::new() + let cbindgen = CbindgenBuilder::new() .source_module(syn::parse_quote!(example_flat)) .free_memory_function("example_free") .mangle_type_name(|base| format!("{base}_t")) diff --git a/prebindgen/src/api/lang/cbindgen/trait_impl.rs b/prebindgen/src/api/lang/cbindgen/trait_impl.rs index e9439d50..7cef477b 100644 --- a/prebindgen/src/api/lang/cbindgen/trait_impl.rs +++ b/prebindgen/src/api/lang/cbindgen/trait_impl.rs @@ -1,11 +1,12 @@ use super::{builder::callback_fn_type, *}; +use crate::api::core::registry::{Building, Conversions, Crossing, RegistryBuilder}; /// Per-category **input** terminal converter builders. Each returns /// `Some(ConverterImpl)` only for the type category it claims (and `None` /// otherwise); [`Prebindgen::on_input_type`] chains them in priority order /// before the wrapper shapes. The categories are mutually exclusive, so the /// chain's fall-through is equivalent to a sequential `if … return` block. -impl Cbindgen { +impl CbindgenBuilder { /// Opaque handle, by-value consume: `*Box::from_raw(v)` — fallible (null /// handle → message). The wire is the bare handle pointer `*mut #c_struct`. pub(crate) fn in_opaque_handle(&self, ty: &syn::Type) -> Option> { @@ -45,7 +46,7 @@ impl Cbindgen { pub(crate) fn in_data_struct( &self, ty: &syn::Type, - r: &Registry<()>, + r: &impl Conversions<()>, ) -> Option> { let key = TypeKey::from_type(ty); if !self.data.contains_key(&key) { @@ -120,7 +121,7 @@ impl Cbindgen { /// invalid `Box`) forces the full `gravestone()` write. fn nullable_owned_ptr_fields( &self, - registry: &Registry<()>, + registry: &impl Conversions<()>, ty: &syn::Type, ) -> Option> { let cfg = self.value_opaque.get(&TypeKey::from_type(ty))?; @@ -151,7 +152,7 @@ impl Cbindgen { /// declared `kind` (its fields are an opaque blob the generator can't introspect). fn value_opaque_writeback( &self, - registry: &Registry<()>, + registry: &impl Conversions<()>, ty: &syn::Type, slot: &syn::Ident, ) -> Option { @@ -213,7 +214,7 @@ impl Cbindgen { pub(crate) fn in_value_opaque( &self, ty: &syn::Type, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> Option> { let opaque = self.value_opaque_ty(ty)?.clone(); let name = Self::in_name(ty); @@ -269,7 +270,11 @@ impl Cbindgen { /// no generator-side evaluation. An unmatched value is a binding error /// through the wrapper's error channel; no Rust enum is ever constructed /// from it. - pub(crate) fn in_enum(&self, ty: &syn::Type, r: &Registry<()>) -> Option> { + pub(crate) fn in_enum( + &self, + ty: &syn::Type, + r: &impl Conversions<()>, + ) -> Option> { let key = TypeKey::from_type(ty); if !self.enums.contains_key(&key) { return None; @@ -437,10 +442,10 @@ impl Cbindgen { } } -/// Per-section [`Cbindgen::prerequisites`] emitters. Each returns the runtime- +/// Per-section [`CbindgenBuilder::prerequisites`] emitters. Each returns the runtime- /// support items for one concern; the trait method concatenates them in order, /// so the emitted preamble is identical to the former single function. -impl Cbindgen { +impl CbindgenBuilder { /// C allocator extern + raw C-string allocator + the universal memory freer. /// Emitted when the layer hands `char*`/array memory to C. Panics if such /// memory is produced but no `.free_memory_function` is declared. @@ -787,7 +792,7 @@ impl Cbindgen { /// Deliberately NOT routed through the shared /// [`enum_discriminant_values`](crate::api::core::types_util::enum_discriminant_values). /// That helper resolves each variant to a concrete `i64`, which is what an - /// adapter needs when it must *know the number* — JniGen's `jint` decode + /// adapter needs when it must *know the number* — JniGenBuilder's `jint` decode /// and the Kotlin `value(N)` constants. This mirror needs no number: it is /// Rust source that cbindgen re-reads, so passing the expression through /// keeps every discriminant C already accepted — a `const` or `cfg`-driven @@ -834,7 +839,7 @@ impl Cbindgen { /// the idiomatic C tagged union, with no hand-written header fragment. /// Variant shape is mirrored faithfully (named stays named, tuple stays /// tuple, unit stays unit); each payload field takes the wire chosen by - /// [`Cbindgen::payload_field_wire`]. + /// [`CbindgenBuilder::payload_field_wire`]. /// /// A union whose payload wires own memory also gets a typed /// `_drop(t_t *)` that frees the **active arm** and nulls the freed @@ -1049,13 +1054,13 @@ impl Cbindgen { /// read from the front as a plain `c_int` and range-checked against the /// variants (the mirror carries no explicit discriminants, so its tags are /// declaration order `0..N`). Only then is the value `assume_init`ed — - /// which is sound because [`Cbindgen::payload_field_wire`] makes every + /// which is sound because [`CbindgenBuilder::payload_field_wire`] makes every /// payload wire bit-pattern-agnostic, leaving the tag as the sole /// obligation. pub(crate) fn in_tagged_union( &self, ty: &syn::Type, - r: &Registry<()>, + r: &impl Conversions<()>, ) -> Option> { let key = TypeKey::from_type(ty); if !self.tagged_unions.contains_key(&key) { @@ -1185,7 +1190,7 @@ impl Cbindgen { pub(crate) fn out_tagged_union( &self, ty: &syn::Type, - r: &Registry<()>, + r: &impl Conversions<()>, ) -> Option> { let key = TypeKey::from_type(ty); if !self.tagged_unions.contains_key(&key) { @@ -1255,7 +1260,7 @@ impl Cbindgen { &self, fty: &syn::Type, b: &syn::Ident, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> TokenStream { if is_string(fty) { return quote!(if #b.is_null() { @@ -1333,7 +1338,7 @@ impl Cbindgen { &self, fty: &syn::Type, b: &syn::Ident, - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> TokenStream { if is_string(fty) { return quote!(__cbg_alloc_cstr(#b)); @@ -1442,53 +1447,14 @@ impl Cbindgen { } } -impl Prebindgen for Cbindgen { - type Metadata = (); - - // Consts have no declaration mechanism here (`declared_consts` stays - // `None`), so every indexed const re-emits through the default - // `on_const` — a path-alias against this source module, keeping consts - // with non-portable initializers valid in the generated file. (cbindgen - // cannot evaluate a path initializer, so aliased consts don't surface - // as `#define`s in the C header.) - fn source_module(&self) -> Option<&syn::Path> { - self.source_module.as_ref() - } - - // ── Structural type resolution ────────────────────────────────────── - // The adapter peels `ty` itself: a rank-0 terminal category, else a - // wrapper shape (`Option<_>`, `&`/`&mut`/`&[_]`/`&str`). See `in_wrappers` - // / `out_wrappers`. - - fn on_input_type(&self, ty: &syn::Type, r: &Registry<()>) -> Option> { - self.select_input_type(ty, r) - } - - fn on_output_type(&self, ty: &syn::Type, r: &Registry<()>) -> Option> { - self.select_output_type(ty, r) - } - - fn declared_functions(&self) -> HashSet { - self.functions.keys().cloned().collect() - } - - fn ignored_functions(&self) -> HashSet { - self.ignored_functions.clone() - } - - fn helper_functions(&self) -> HashSet { - self.convert_decls - .iter() - .flat_map(|decl| decl.input.iter().chain(decl.output.iter())) - .filter_map(|spec| match spec { - ConvertSpec::PrebindgenFn(ident) => Some(ident.clone()), - ConvertSpec::Trait { .. } => None, - }) - .filter(|ident| !self.functions.contains_key(ident)) - .collect() - } - - fn local_functions(&self) -> Vec<(syn::ItemFn, String)> { +impl CbindgenBuilder { + /// State this binding into `registry` — see `JniGenBuilder::declare_into`. + /// + /// Push, not pull: the build script calls this, and the registry never + /// calls back. cbindgen declares no consts (it has no const mechanism, so + /// every captured const re-emits verbatim) and no decompositions. + /// Binding-local fns declared by `convert!(..).local(..)`. + fn collect_local_functions(&self) -> Vec<(syn::ItemFn, String)> { let mut result = Vec::new(); let mut seen = HashMap::::new(); for (ident, path, sig) in self.convert_decls.iter().flat_map(|decl| &decl.locals) { @@ -1511,71 +1477,80 @@ impl Prebindgen for Cbindgen { result } - fn declared_types(&self) -> HashSet { - self.opaque - .keys() - .chain(self.data.keys()) - .chain(self.value_opaque.keys()) - .chain(self.enums.keys()) - .chain(self.tagged_unions.keys()) - .cloned() - .collect() - } - - fn ignored_types(&self) -> HashSet { - self.ignored_types.clone() - } - - fn prerequisites(&self, registry: &Registry<()>) -> Vec { - // C-string data memory (string returns + `String` fields of data structs) - // is malloc'd raw and freed by the single universal `free_memory_function`. - // Array returns (`Vec`) also hand out a malloc'd block freed via the - // same function (per element through the `z_free_array` macro), so the - // allocator/freer prelude is needed for them too. Each section's emitter - // lives in the `impl Cbindgen` block above; order is significant. - let produces_array = self.produces_array(registry); - let mut items: Vec = Vec::new(); - items.extend(self.prereq_alloc_free(registry, produces_array)); - items.extend(self.prereq_array_builder(produces_array)); - items.extend(self.prereq_opaque_handles(registry)); - items.extend(self.prereq_data_structs(registry)); - items.extend(self.prereq_value_opaque(registry)); - items.extend(self.prereq_enums(registry)); - items.extend(self.prereq_tagged_unions(registry)); - items.extend(self.prereq_callback_structs(registry)); - items.extend(self.prereq_domain_constants(registry)); - items + /// State this binding into `registry`, then resolve it — see + /// `JniGenBuilder::build`. + /// Read the source, resolve every crossing, and hand back the binding — + /// see `JniGenBuilder::build`. + pub fn build(self) -> Result { + let flat = self + .sources + .clone() + .build() + .map_err(crate::core::ScanError::from)?; + let registry = crate::core::Registry::builder(flat)?; + self.build_with(registry) } - // ── Item emission ────────────────────────────────────────────────── - - fn on_function(&self, f: &syn::ItemFn, registry: &Registry<()>) -> TokenStream { - self.emit_function_wrapper(f, registry) + /// [`Self::build`] over a registry described elsewhere — the test seam. + pub(crate) fn build_with( + self, + registry: crate::api::core::registry::RegistryBuilder<()>, + ) -> Result { + let registry = self + .declare_into(registry)? + .validate_with(&self)? + .convert_with(|crossing, built| self.convert_crossing(crossing, built))? + .build()?; + self.validate_resolved(®istry) + .map_err(|message| crate::core::ScanError::AdapterInvariant { message })?; + Ok(Cbindgen { + gen: self, + registry, + }) } - fn on_struct(&self, _s: &syn::ItemStruct, _registry: &Registry<()>) -> TokenStream { - // The `#[repr(C)]` mirror + converters come from prerequisites / - // on_output_type; the original (non-FFI-safe) struct is dropped. - TokenStream::new() + /// Build the conversion for one crossing — see `JniGenBuilder::convert_crossing`. + fn convert_crossing( + &self, + crossing: &Crossing, + built: &Building<'_, ()>, + ) -> Option> { + let (dir, key) = crossing; + let ty = key.to_type(); + match dir { + Direction::Input => self.select_input_type(&ty, built).or_else(|| { + let args = crate::api::core::flat::extract_fn_trait_args(&ty)?; + self.dispatch_fn_input(&args, built) + }), + Direction::Output => self.select_output_type(&ty, built), + } } - fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry<()>) -> TokenStream { - TokenStream::new() + pub fn declare_into( + &self, + mut registry: RegistryBuilder<()>, + ) -> Result, crate::core::ScanError> { + for (item_fn, origin) in self.collect_local_functions() { + registry = registry.local_function(item_fn, origin)?; + } + for ident in self.declared_functions() { + registry = registry.export(&ident); + } + for ident in self.helper_functions() { + registry = registry.reference(&ident); + } + for key in self.declared_types() { + registry = registry.export_type(key); + } + Ok(registry) } +} - /// `impl Fn(Args...) + Send + Sync + 'static` callback input. The C wire is a - /// by-value closure struct (`{ void *context; call; drop }`, emitted in - /// `prerequisites`); the converter rebuilds a Rust closure that, on each - /// invocation, encodes its args through their **output** converters (the - /// args travel Rust→C when the callback fires — they're owned handles the C - /// `call` is responsible for dropping) and invokes the C function pointer. - /// An `Arc` carries the `void *context` + `drop`, releasing it (once, - /// `Send + Sync`) when the Rust closure is dropped. Only signatures declared - /// via [`Cbindgen::callback`] are handled. +impl CbindgenBuilder { fn dispatch_fn_input( &self, args: &[syn::Type], - registry: &Registry<()>, + registry: &impl Conversions<()>, ) -> Option> { let key: CallbackKey = args.iter().map(TypeKey::from_type).collect(); if !self.callbacks.contains_key(&key) { @@ -1684,13 +1659,93 @@ impl Prebindgen for Cbindgen { } } +impl Prebindgen for CbindgenBuilder { + /// Report what this binding left unclaimed. Here because it is the + /// earliest generator-owned hook that sees the model, and it runs exactly + /// where the registry used to print these itself. Moves into + /// `CbindgenBuilder::generate` once that exists (prebindgen#251 phase E). + /// + /// `consts: None` — cbindgen has no const declaration mechanism, so every + /// captured const is re-emitted verbatim and none is ever a skip. + fn validate(&self, binding: &Building<'_, Self::Metadata>) -> Result<(), String> { + let mut functions = self.declared_functions(); + functions.extend(self.helper_functions()); + crate::core::warn_unclaimed( + binding.flat(), + &crate::core::Claimed { + functions, + types: self.declared_types(), + consts: None, + ignored_functions: self.ignored_functions(), + ignored_types: self.ignored_types(), + ..Default::default() + }, + ); + Ok(()) + } + + type Metadata = (); + + // Consts have no declaration mechanism here (`declared_consts` stays + // `None`), so every indexed const re-emits through the default + // `on_const` — a path-alias against this source module, keeping consts + // with non-portable initializers valid in the generated file. (cbindgen + // cannot evaluate a path initializer, so aliased consts don't surface + // as `#define`s in the C header.) + fn source_module(&self) -> Option<&syn::Path> { + self.source_module.as_ref() + } + + // ── Structural type resolution ────────────────────────────────────── + // The adapter peels `ty` itself: a rank-0 terminal category, else a + // wrapper shape (`Option<_>`, `&`/`&mut`/`&[_]`/`&str`). See `in_wrappers` + // / `out_wrappers`. + + fn prerequisites(&self, registry: &Registry<()>) -> Vec { + // C-string data memory (string returns + `String` fields of data structs) + // is malloc'd raw and freed by the single universal `free_memory_function`. + // Array returns (`Vec`) also hand out a malloc'd block freed via the + // same function (per element through the `z_free_array` macro), so the + // allocator/freer prelude is needed for them too. Each section's emitter + // lives in the `impl CbindgenBuilder` block above; order is significant. + let produces_array = self.produces_array(registry); + let mut items: Vec = Vec::new(); + items.extend(self.prereq_alloc_free(registry, produces_array)); + items.extend(self.prereq_array_builder(produces_array)); + items.extend(self.prereq_opaque_handles(registry)); + items.extend(self.prereq_data_structs(registry)); + items.extend(self.prereq_value_opaque(registry)); + items.extend(self.prereq_enums(registry)); + items.extend(self.prereq_tagged_unions(registry)); + items.extend(self.prereq_callback_structs(registry)); + items.extend(self.prereq_domain_constants(registry)); + items + } + + // ── Item emission ────────────────────────────────────────────────── + + fn on_function(&self, f: &syn::ItemFn, registry: &Registry<()>) -> TokenStream { + self.emit_function_wrapper(f, registry) + } + + fn on_struct(&self, _s: &syn::ItemStruct, _registry: &Registry<()>) -> TokenStream { + // The `#[repr(C)]` mirror + converters come from prerequisites / + // on_output_type; the original (non-FFI-safe) struct is dropped. + TokenStream::new() + } + + fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry<()>) -> TokenStream { + TokenStream::new() + } +} + /// Output-direction terminal categories — the rank-0 chain, now an inherent -/// helper called by the structural [`Prebindgen::on_output_type`]. -impl Cbindgen { +/// helper called by [`CbindgenBuilder::select_output_type`]. +impl CbindgenBuilder { pub(crate) fn out_terminal( &self, ty: &syn::Type, - _r: &Registry<()>, + _r: &impl Conversions<()>, ) -> Option> { // Unit return: trivial converter so `()` (and `Result<(), _>`) resolves. // Never actually called — void-returning wrappers ignore it, and @@ -1901,12 +1956,12 @@ impl Cbindgen { /// Structural wrapper-shape resolvers (the post-rank-machinery surface). Each /// peels `ty`'s outermost layer and composes the inner's converter; `subs` /// lists the immediate inner(s) it looked up. -impl Cbindgen { +impl CbindgenBuilder { /// `Option` and reference (`&`/`&mut`/`&[E]`/`&str`) **input** shapes. pub(crate) fn in_wrappers( &self, ty: &syn::Type, - r: &Registry<()>, + r: &impl Conversions<()>, ) -> Option> { // `Option` input: a single nullable C param, NULL = `None`. The inner // `X` is reused wholesale (its own converter — e.g. an `&T` borrow — does @@ -2225,7 +2280,7 @@ impl Cbindgen { pub(crate) fn out_wrappers( &self, ty: &syn::Type, - r: &Registry<()>, + r: &impl Conversions<()>, ) -> Option> { // `Option` / `Vec` marker. if is_option(ty) || is_vec(ty) { @@ -2350,3 +2405,41 @@ impl Cbindgen { None } } + +/// The declaration surface, stated once. +/// +/// These were trait methods the registry called back into the adapter from +/// inside `resolve`. They are the adapter's own business now, gathered into the +/// one value the registry is constructed from. +impl CbindgenBuilder { + pub(crate) fn declared_functions(&self) -> HashSet { + self.functions.keys().cloned().collect() + } + pub(crate) fn ignored_functions(&self) -> HashSet { + self.ignored_functions.clone() + } + pub(crate) fn helper_functions(&self) -> HashSet { + self.convert_decls + .iter() + .flat_map(|decl| decl.input.iter().chain(decl.output.iter())) + .filter_map(|spec| match spec { + ConvertSpec::PrebindgenFn(ident) => Some(ident.clone()), + ConvertSpec::Trait { .. } => None, + }) + .filter(|ident| !self.functions.contains_key(ident)) + .collect() + } + pub(crate) fn declared_types(&self) -> HashSet { + self.opaque + .keys() + .chain(self.data.keys()) + .chain(self.value_opaque.keys()) + .chain(self.enums.keys()) + .chain(self.tagged_unions.keys()) + .cloned() + .collect() + } + pub(crate) fn ignored_types(&self) -> HashSet { + self.ignored_types.clone() + } +} diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index bdbcdba4..0b0438fe 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -1,12 +1,13 @@ -//! Builder API for [`JniGen`]. +//! Builder API for [`JniGenBuilder`]. //! -//! [`JniGen::new`] starts from defaults; global settings are applied with +//! [`JniGenBuilder::new`] starts from defaults; global settings are applied with //! the `set_*` methods (`config.rs`) and declarations are *accepted* as -//! pre-built objects (`decl.rs`) via [`JniGen::package`], [`JniGen::expand`], -//! and [`JniGen::convert`] — there is no fluent typestate cursor. Carved from the former monolithic +//! pre-built objects (`decl.rs`) via [`JniGenBuilder::package`], [`JniGenBuilder::expand`], +//! and [`JniGenBuilder::convert`] — there is no fluent typestate cursor. Carved from the former monolithic //! JNI module; shares the `jni` namespace via `use super::*`. use super::*; +use crate::api::core::registry::Conversions; impl DeclaredKind { /// The declaring macro's name, for the conflict message. @@ -66,7 +67,7 @@ impl DeclaredKind { } } -impl JniGen { +impl JniGenBuilder { /// The module path a generated call to `#[prebindgen]` fn `ident` must be /// qualified with: the fn's **origin crate** as recorded from its /// stream's `SourceLocation` stamp (multi-source bindings — helper @@ -74,7 +75,7 @@ impl JniGen { /// module (first-seen stream origin), else `crate`. pub(crate) fn fn_module( &self, - registry: &Registry, + registry: &impl Conversions, ident: &syn::Ident, ) -> syn::Path { registry @@ -102,19 +103,19 @@ impl JniGen { } } -impl JniGen { +impl JniGenBuilder { /// Start a binding generator with default settings: empty base /// package, no `JNINative` init block, identity /// name-mangling, handle locks enabled. Adjust settings with the `set_*` /// methods, add declarations with [`package`](Self::package), /// [`expand`](Self::expand), [`convert`](Self::convert), etc., then run the - /// result through `Registry::resolve` → `Generation::write_rust` / + /// result through `JniGenBuilder::build` → `JniGen::write_rust` / /// `write_kotlin`. Settings and /// declarations may be interleaved in any order — the builder stores /// only raw inputs, and every setting-derived name is computed at the /// point of use. pub fn new() -> Self { - let mut jni = Self { + Self { package: String::new(), fun_name_mangle: None, ptr_class_name_mangle: None, @@ -125,18 +126,6 @@ impl JniGen { interface_name_mangle: None, types: HashMap::new(), packages: BTreeMap::new(), - input_wrappers: [ - HashMap::new(), - HashMap::new(), - HashMap::new(), - HashMap::new(), - ], - output_wrappers: [ - HashMap::new(), - HashMap::new(), - HashMap::new(), - HashMap::new(), - ], emit_handle_locks: true, jni_native_init: None, convert_decls: Vec::new(), @@ -153,20 +142,8 @@ impl JniGen { local_fns: Vec::new(), iface_specs: Default::default(), fn_plans: Default::default(), - }; - // Built-in rank-2 `Result<_, _>` peel: every Result succeeds - // as T and routes E to the error-sink on Err. The rank tables are - // internal — this is their only entry; `convert!` covers concrete - // types at rank 0. - let pattern: syn::Type = syn::parse_quote!(Result<_, _>); - let key = TypeKey::from_type(&pattern); - jni.output_wrappers[2].insert( - key, - Arc::new(|args: &[syn::Type], _: &Registry| { - Some((args[0].clone(), Some(args[1].clone()), syn::parse_quote!(v))) - }), - ); - jni + sources: Default::default(), + } } /// Apply the package-level function-name mangle closure to `name`. @@ -260,7 +237,7 @@ impl JniGen { } } -impl Default for JniGen { +impl Default for JniGenBuilder { fn default() -> Self { Self::new() } @@ -268,11 +245,49 @@ impl Default for JniGen { // ── Accepting a `PackageDecl` ──────────────────────────────────────────── -impl JniGen { +impl JniGenBuilder { /// Register a package's worth of classes, functions and consts (a /// [`PackageDecl`], built with [`package!`](crate::package)). Call it once /// per package, or several times for the same package name — the /// declarations merge, so you can split a large package across calls. + /// Every `#[prebindgen]` item captured in `dir` — pass + /// `::PREBINDGEN_OUT_DIR`. + /// + /// The same feeder [`FlatBuilder::source`](crate::core::flat::FlatBuilder::source) + /// has, because it is that feeder: the binding says where its source is, + /// and the model is built from it at [`Self::build`]. + pub fn source>(mut self, dir: P) -> Self { + self.sources = std::mem::take(&mut self.sources).source(dir); + self + } + + /// The same, for a dependency this crate **renames** in `Cargo.toml`. + /// + /// The origin recorded at capture time is the dependency's real package + /// name, which will not resolve from a crate that refers to it by another + /// name. `crate_name` is the name *this* crate uses. Per directory, + /// deliberately: a binding may layer several sources. + pub fn source_named>( + mut self, + dir: P, + crate_name: impl Into, + ) -> Self { + self.sources = std::mem::take(&mut self.sources).source_named(dir, crate_name); + self + } + + /// Add a captured item stream — a group selection, an otherwise-configured + /// [`Source`](crate::Source), or synthetic items in a test. + /// + /// Accumulates, so it mixes freely with [`Self::source`]. + pub fn items(mut self, items: I) -> Self + where + I: IntoIterator, + { + self.sources = std::mem::take(&mut self.sources).items(items); + self + } + pub fn package(mut self, decl: PackageDecl) -> Self { let PackageDecl { name, @@ -355,7 +370,7 @@ impl JniGen { /// one-declarator-per-type rule is enforced for all kinds by /// [`DeclaredKind::merge`] and cannot be forgotten by a new one. /// No FQN is derived here — names materialize at read time via - /// [`JniGen::fqn_of`], against whatever the settings are then. + /// [`JniGenBuilder::fqn_of`], against whatever the settings are then. /// /// Returns the stored config so the caller can fold in its cross-kind /// options (`jobject_input`, interfaces). @@ -580,7 +595,7 @@ impl JniGen { // ── Accepting boundary decls ───────────────────────────────────────────── -impl JniGen { +impl JniGenBuilder { /// Declare a type's **default boundary behavior** — either of the two /// [`ExpandDecl`] directions, the direction carried by the decl object /// (the boundary-decl peer of [`PackageDecl::class`]): @@ -748,7 +763,7 @@ impl JniGen { /// once, on the member), else the camel-cased Rust name. fn lower_fields( &self, - registry: &Registry, + registry: &impl Conversions, key: &TypeKey, fields: &[LocalField], ) -> Vec { @@ -805,7 +820,7 @@ impl JniGen { /// a field renamed upstream must not silently lose its adjustment. fn lower_value_form( &self, - registry: &Registry, + registry: &impl Conversions, key: &TypeKey, decl: &FieldsDecl, ) -> Vec { @@ -885,7 +900,7 @@ impl JniGen { #[allow(clippy::too_many_arguments)] fn walk_value_form( &self, - registry: &Registry, + registry: &impl Conversions, key: &TypeKey, decl: &FieldsDecl, st: &syn::ItemStruct, @@ -1071,7 +1086,7 @@ impl JniGen { /// output-flattened. pub(crate) fn build_deconstructors( &self, - registry: &Registry, + registry: &impl Conversions, ) -> crate::api::core::unfold::Deconstructors { use crate::api::core::unfold::{ DeconSel, DeconTarget, DeconstructorDecl, Deconstructors, Delivery, OutputDecl, @@ -1355,7 +1370,7 @@ impl JniGen { // ── Accepting the convert decl ─────────────────────────────────────────── -impl JniGen { +impl JniGenBuilder { /// Declare a type's **canonical single-value conversion** (a /// [`ConvertDecl`], built with [`convert!`](crate::convert)): a pair of /// `#[prebindgen]` functions carrying one value of the type across the @@ -1383,14 +1398,14 @@ impl JniGen { /// type: `(continue_ty, exc, body)` where `continue_ty` is the conversion /// fn's parameter type (by value) — the composed-converter machinery /// chains it through that type's own converter, so the wire and the - /// Kotlin surface derive from it. Consulted by [`Self::lookup_input`] - /// before the wrapper tables; signatures are read from the registry at + /// Kotlin surface derive from it. It is what [`Self::lookup_input`] + /// answers with; signatures are read from the registry at /// lookup time (order-independent, and multi-source qualification via /// [`Self::fn_module`]). pub(crate) fn convert_input_body( &self, key: &TypeKey, - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, Option, syn::Expr)> { let decl = self.convert_decls.iter().find(|d| &d.key == key)?; let target = key.to_type(); @@ -1466,7 +1481,7 @@ impl JniGen { pub(crate) fn convert_output_body( &self, key: &TypeKey, - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, Option, syn::Expr)> { let decl = self.convert_decls.iter().find(|d| &d.key == key)?; let target = key.to_type(); @@ -1684,7 +1699,7 @@ fn fn_return_type(item_fn: &syn::ItemFn) -> syn::Type { } } -impl JniGen { +impl JniGenBuilder { /// Build a `KotlinMeta` carrying just the value-context Kotlin name. /// Used by every built-in converter (primitives, structs, `Option<_>`, /// `Vec<_>`, `impl Fn(...)` lambdas). Errors are routed uniformly to the @@ -1701,7 +1716,7 @@ impl JniGen { fn conversion_domain_niches( &self, key: &TypeKey, - registry: &Registry, + registry: &impl Conversions, direction: Direction, wire: &syn::Type, ) -> (Niches, Vec) { @@ -1720,8 +1735,8 @@ impl JniGen { return (Niches::empty(), Vec::new()); } let demand = registry - .type_table(direction) - .keys() + .crossing_keys(direction) + .iter() .map(|candidate| { let mut ty = candidate.to_type(); let mut depth = 0; @@ -1767,12 +1782,12 @@ impl JniGen { } } - // ── Wrapper-table lookups (used by Prebindgen impl) ─────────── + // ── Converter lookups (used by the Prebindgen impl) ─────────── - /// Look up a registered input converter for `pat` with `args` - /// substituted into its `_` slots. The closure's middle slot (see - /// [`WrapperFn`]) carries the bound exception — `None` ⇒ framework - /// `__JniErr` with an `Ok`-wrap, `Some()` ⇒ + /// The input converter a `convert!` declaration supplies for `outer`. + /// + /// The body triple's middle slot carries the bound exception — `None` ⇒ + /// framework `__JniErr` with an `Ok`-wrap, `Some()` ⇒ /// `Result>` emitted verbatim, decided in /// [`Self::build_input_fn`]. /// @@ -1784,87 +1799,27 @@ impl JniGen { /// [`Self::build_output_fn`]) prepended to the inner chain. Defer /// (`None`) if the inner converter isn't resolved yet. /// - /// Structurally match `ty` against every registered **input** wrapper - /// pattern, most-specific-first (fewest wildcards win, e.g. - /// `Result<_, ConcreteErr>` over `Result<_, _>`), and build the first hit. - pub(crate) fn match_user_input( - &self, - ty: &syn::Type, - registry: &Registry, - ) -> Option> { - for pat in self.ordered_input_patterns() { - if let Some(args) = crate::api::core::types_util::match_pattern(ty, &pat) { - if let Some(c) = self.lookup_input(&pat, &args, registry) { - return Some(c); - } - } - } - None - } - - /// Output-direction peer of [`Self::match_user_input`]. - pub(crate) fn match_user_output( - &self, - ty: &syn::Type, - registry: &Registry, - ) -> Option> { - for pat in self.ordered_output_patterns() { - if let Some(args) = crate::api::core::types_util::match_pattern(ty, &pat) { - if let Some(c) = self.lookup_output(&pat, &args, registry) { - return Some(c); - } - } - } - None - } - - /// Registered input-wrapper patterns, ordered most-specific (fewest - /// wildcards) first; ties keep registration-independent but stable order - /// (by canonical key) so resolution is deterministic. - fn ordered_input_patterns(&self) -> Vec { - ordered_patterns(&self.input_wrappers) - } - fn ordered_output_patterns(&self) -> Vec { - ordered_patterns(&self.output_wrappers) - } - pub(crate) fn lookup_input( &self, - pat: &syn::Type, - args: &[syn::Type], - registry: &Registry, + outer: &syn::Type, + registry: &impl Conversions, ) -> Option> { - let rank = args.len(); - if rank > 3 { - return None; - } - let key = TypeKey::from_type(pat); - // A `convert!`-declared conversion takes precedence at rank 0 (its - // signature-derived body is equivalent to a rank-0 registration, - // just computed at the point of use). - let (ty, exc_ty, body) = match if rank == 0 { - self.convert_input_body(&key, registry) - } else { - None - } { - Some(t) => t, - None => { - let f = self.input_wrappers[rank].get(&key)?; - f(args, registry)? - } - }; + // A `convert!`-declared conversion is the only thing that answers here. + // There was a wildcard-pattern table beside it; nothing ever wrote to + // the input half, so every lookup through it returned `None`. + let key = TypeKey::from_type(outer); + let (ty, exc_ty, body) = self.convert_input_body(&key, registry)?; // The closure's middle slot carries the `Result`'s raw Rust error // type (or `None` for the framework `__JniErr`); it feeds the // converter signature `Result<_, E>` directly — no registration. let exc = exc_ty.as_ref(); - let outer = substitute_wildcards(pat, args); // Terminal vs composed: `ty` is composed iff it's a *distinct* // rust type with its own input converter. The self-check guards // the void/identity case, and the registered-converter probe // distinguishes a rust continue-type (compose) from a wire // (terminal) without forcing `()` either way. A non-wire `ty` that // isn't yet resolved defers. - let is_self = TypeKey::from_type(&ty) == TypeKey::from_type(&outer); + let is_self = TypeKey::from_type(&ty) == TypeKey::from_type(outer); let inner = if is_self { None } else { @@ -1873,21 +1828,17 @@ impl JniGen { match inner { None if is_self || is_wire_type(&ty) => { // Terminal: `ty` is the wire; the body produces `outer`. - let (niches, kotlin_name) = if rank == 0 { - let kn = self - .types - .get(&key) - .and_then(|c| c.name_spec.as_ref()) - .map(|s| kt::KtType::cls(self.fqn_of(s))) - .or_else(|| kotlin_for_wire(&ty)); - (Niches::empty(), kn) - } else { - (default_niches_for_wire(&ty), None) - }; + let kotlin_name = self + .types + .get(&key) + .and_then(|c| c.name_spec.as_ref()) + .map(|s| kt::KtType::cls(self.fqn_of(s))) + .or_else(|| kotlin_for_wire(&ty)); + let niches = Niches::empty(); Some(ConverterImpl { subs: vec![], pre_stages: vec![], - function: self.build_input_fn(&outer, &ty, &body, exc), + function: self.build_input_fn(outer, &ty, &body, exc), destination: ty, niches, metadata: KotlinMeta { @@ -1909,29 +1860,19 @@ impl JniGen { // yields `outer`, i.e. the same shape an output converter // has — so it's built with `build_output_fn`. let stage = Stage { - function: self.build_output_fn(&ty, &outer, &body, exc), + function: self.build_output_fn(&ty, outer, &body, exc), metadata: KotlinMeta::default(), }; let mut pre_stages = vec![stage]; pre_stages.extend(inner.pre_stages.iter().cloned()); - let (kotlin_name, value_rust_key) = if rank >= 1 { - ( - inner.metadata.kotlin_name.clone(), - Some(TypeKey::from_type(&args[0])), - ) - } else { - (inner.metadata.kotlin_name.clone(), None) - }; - let (niches, sentinels) = if rank == 0 { - self.conversion_domain_niches( - &key, - registry, - Direction::Input, - &inner.destination, - ) - } else { - (default_niches_for_wire(&inner.destination), Vec::new()) - }; + let kotlin_name = inner.metadata.kotlin_name.clone(); + let value_rust_key = None; + let (niches, sentinels) = self.conversion_domain_niches( + &key, + registry, + Direction::Input, + &inner.destination, + ); let mut metadata = KotlinMeta { kotlin_name, value_rust_key, @@ -1963,34 +1904,58 @@ impl JniGen { /// (`None`) if `ty`'s converter isn't resolved yet. pub(crate) fn lookup_output( &self, - pat: &syn::Type, - args: &[syn::Type], - registry: &Registry, + outer: &syn::Type, + registry: &impl Conversions, ) -> Option> { - let rank = args.len(); - if rank > 3 { - return None; - } - let key = TypeKey::from_type(pat); - // A `convert!`-declared conversion takes precedence at rank 0 — see - // [`Self::lookup_input`]. - let (ty, exc_ty, body) = match if rank == 0 { - self.convert_output_body(&key, registry) - } else { - None - } { - Some(t) => t, - None => { - let f = self.output_wrappers[rank].get(&key)?; - f(args, registry)? - } - }; - // The closure's middle slot carries the `Result`'s raw Rust error - // type (or `None` for the framework `__JniErr`) — see lookup_input. + let key = TypeKey::from_type(outer); + let (ty, exc_ty, body) = self.convert_output_body(&key, registry)?; + self.build_output_converter(outer, None, ty, exc_ty, body, registry) + } + + /// The `Result` output peel: the value succeeds as `T`, and `E` routes + /// to the error sink on `Err`. + /// + /// This was the sole entry in a four-rank wildcard-pattern table, reached + /// through a general unification engine. The model already calls this shape + /// [`TypeKind::Fallible`](crate::core::flat::TypeKind::Fallible), so the + /// engine expressed one fact the frontend states outright. + pub(crate) fn result_peel( + &self, + outer: &syn::Type, + ok: &syn::Type, + err: &syn::Type, + registry: &impl Conversions, + ) -> Option> { + self.build_output_converter( + outer, + Some(ok), + ok.clone(), + Some(err.clone()), + syn::parse_quote!(v), + registry, + ) + } + + /// Assemble the output `ConverterImpl` from a body triple. + /// + /// `arg0` is the peeled inner type for a shape peel, `None` for a + /// `convert!`-declared conversion — which is what the old `rank == 0` + /// tested. + fn build_output_converter( + &self, + outer: &syn::Type, + arg0: Option<&syn::Type>, + ty: syn::Type, + exc_ty: Option, + body: syn::Expr, + registry: &impl Conversions, + ) -> Option> { + let key = TypeKey::from_type(outer); + // The middle slot carries the `Result`'s raw Rust error type (or `None` + // for the framework `__JniErr`). let exc = exc_ty.as_ref(); - let outer = substitute_wildcards(pat, args); // Terminal vs composed — see [`Self::lookup_input`] for the rule. - let is_self = TypeKey::from_type(&ty) == TypeKey::from_type(&outer); + let is_self = TypeKey::from_type(&ty) == TypeKey::from_type(outer); let inner = if is_self { None } else { @@ -1999,15 +1964,10 @@ impl JniGen { match inner { None if is_self || is_wire_type(&ty) => { // Terminal: `ty` is the wire; the body produces it from `outer`. - let (kotlin_name, value_rust_key) = if rank >= 1 { + let (kotlin_name, value_rust_key) = if let Some(a0) = arg0 { registry - .output_entry(&args[0]) - .map(|e| { - ( - e.metadata.kotlin_name.clone(), - Some(TypeKey::from_type(&args[0])), - ) - }) + .output_entry(a0) + .map(|e| (e.metadata.kotlin_name.clone(), Some(TypeKey::from_type(a0)))) .unwrap_or((None, None)) } else { let kn = self @@ -2018,15 +1978,14 @@ impl JniGen { .or_else(|| kotlin_for_wire(&ty)); (kn, None) }; - let niches = if rank == 0 { - Niches::empty() - } else { - default_niches_for_wire(&ty) + let niches = match arg0 { + None => Niches::empty(), + Some(_) => default_niches_for_wire(&ty), }; Some(ConverterImpl { subs: vec![], pre_stages: vec![], - function: self.build_output_fn(&outer, &ty, &body, exc), + function: self.build_output_fn(outer, &ty, &body, exc), destination: ty, niches, metadata: KotlinMeta { @@ -2043,28 +2002,21 @@ impl JniGen { Some(inner) => { // Composed: `ty` is the continue rust type; chain its converter. let stage = Stage { - function: self.build_output_fn(&outer, &ty, &body, exc), + function: self.build_output_fn(outer, &ty, &body, exc), metadata: KotlinMeta::default(), }; let mut pre_stages = vec![stage]; pre_stages.extend(inner.pre_stages.iter().cloned()); - let (kotlin_name, value_rust_key) = if rank >= 1 { - ( - inner.metadata.kotlin_name.clone(), - Some(TypeKey::from_type(&args[0])), - ) - } else { - (inner.metadata.kotlin_name.clone(), None) - }; - let (niches, sentinels) = if rank == 0 { - self.conversion_domain_niches( + let kotlin_name = inner.metadata.kotlin_name.clone(); + let value_rust_key = arg0.map(TypeKey::from_type); + let (niches, sentinels) = match arg0 { + None => self.conversion_domain_niches( &key, registry, Direction::Output, &inner.destination, - ) - } else { - (default_niches_for_wire(&inner.destination), Vec::new()) + ), + Some(_) => (default_niches_for_wire(&inner.destination), Vec::new()), }; let mut metadata = KotlinMeta { kotlin_name, @@ -2095,7 +2047,7 @@ impl JniGen { /// `()` is deliberately **not** treated as a wire here: it is ambiguous /// (the void wire of a self-converter *and* the unit continue-type of /// `ZResult<()>`). The terminal-vs-composed decision in -/// [`JniGen::lookup_input`] / [`JniGen::lookup_output`] resolves that +/// [`JniGenBuilder::lookup_input`] / [`JniGenBuilder::lookup_output`] resolves that /// ambiguity via the self-check + registered-converter probe, so `()` /// flows correctly without being force-classified here. pub(crate) fn is_wire_type(ty: &syn::Type) -> bool { @@ -2107,7 +2059,7 @@ pub(crate) fn is_wire_type(ty: &syn::Type) -> bool { /// converters use this as their `Result<…, _>` error type so their bodies' /// `<__JniErr as From>::from(...)` calls keep compiling. A /// `Result` return instead binds its own raw `E` (see -/// [`JniGen::lookup_output`]); the extern's `Err` arm funnels both to the +/// [`JniGenBuilder::lookup_output`]); the extern's `Err` arm funnels both to the /// per-call `signal_error` sink via `E: Display`. /// The origin-module prefix of a binding-local fn's declared path /// (`crate::sub::f` → `"crate::sub"`). Paths are validated ≥2 segments at @@ -2145,68 +2097,3 @@ pub(crate) fn body_for_exc(body: &syn::Expr, exc: Option<&syn::Type>) -> syn::Ex syn::parse_quote!(Ok(#body)) } } - -/// Substitute the wildcard `_` slots of `pat` with `args` (left-to-right -/// depth-first), returning the concrete outer `syn::Type`. Mirrors the -/// substitution the resolver performs to derive a wildcard pattern from -/// a concrete type. -pub(crate) fn substitute_wildcards(pat: &syn::Type, args: &[syn::Type]) -> syn::Type { - let mut idx = 0usize; - fn walk(ty: &mut syn::Type, args: &[syn::Type], idx: &mut usize) { - match ty { - syn::Type::Infer(_) => { - if let Some(replacement) = args.get(*idx) { - *ty = replacement.clone(); - } - *idx += 1; - } - syn::Type::Path(tp) => { - for seg in &mut tp.path.segments { - if let syn::PathArguments::AngleBracketed(ab) = &mut seg.arguments { - for arg in &mut ab.args { - if let syn::GenericArgument::Type(inner) = arg { - walk(inner, args, idx); - } - } - } - } - } - syn::Type::Reference(r) => walk(&mut r.elem, args, idx), - syn::Type::Tuple(t) => { - for e in &mut t.elems { - walk(e, args, idx); - } - } - syn::Type::Array(a) => walk(&mut a.elem, args, idx), - syn::Type::Slice(s) => walk(&mut s.elem, args, idx), - syn::Type::Ptr(p) => walk(&mut p.elem, args, idx), - syn::Type::Paren(p) => walk(&mut p.elem, args, idx), - syn::Type::Group(g) => walk(&mut g.elem, args, idx), - _ => {} - } - } - let mut out = pat.clone(); - walk(&mut out, args, &mut idx); - out -} - -/// Flatten the rank-bucketed wrapper tables into one pattern list ordered -/// most-specific-first: ascending wildcard count (so `Result<_, ConcreteErr>` -/// is tried before `Result<_, _>`), then by canonical key for a deterministic -/// tiebreak independent of `HashMap` iteration order. -fn ordered_patterns(buckets: &[HashMap; 4]) -> Vec { - let mut keys: Vec<(usize, String, syn::Type)> = buckets - .iter() - .flat_map(|m| m.keys()) - .map(|k| { - let ty = k.to_type(); - ( - crate::api::core::types_util::wildcard_count(&ty), - k.as_str().to_string(), - ty, - ) - }) - .collect(); - keys.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.cmp(&b.1))); - keys.into_iter().map(|(_, _, ty)| ty).collect() -} diff --git a/prebindgen/src/api/lang/jnigen/jni/classify.rs b/prebindgen/src/api/lang/jnigen/jni/classify.rs index a7256ac6..66613d4f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/classify.rs +++ b/prebindgen/src/api/lang/jnigen/jni/classify.rs @@ -3,6 +3,7 @@ //! re-deriving it from `TypeConfig` flags and `registry.flat()` type probes. use super::*; +use crate::api::core::registry::Conversions; /// The adapter-declared kind of a **bare** (already `Option`/`&`-stripped) /// Rust type: the declared [`DeclaredKind`] when the type is declared to this @@ -41,13 +42,13 @@ impl TypeConfig { } } -impl JniGen { +impl JniGenBuilder { /// Classify `bare` against the declared-type table and the registry's /// captured structs. Callers strip `Option<_>` / `&_` layers first — /// wrapper folding is the resolver's business, not this table's. pub(crate) fn type_kind<'r, 'c>( &'c self, - registry: &'r Registry, + registry: &'r impl Conversions, bare: &syn::Type, ) -> TypeKind<'r, 'c> { let cfg = self.types.get(&TypeKey::from_type(bare)); diff --git a/prebindgen/src/api/lang/jnigen/jni/config.rs b/prebindgen/src/api/lang/jnigen/jni/config.rs index 7b5be72a..e1cd7621 100644 --- a/prebindgen/src/api/lang/jnigen/jni/config.rs +++ b/prebindgen/src/api/lang/jnigen/jni/config.rs @@ -1,8 +1,8 @@ -//! Global settings of a [`JniGen`] instance — the target +//! Global settings of a [`JniGenBuilder`] instance — the target //! package, name-mangling rules, native-init hook and handle-lock toggle. //! //! Every setter carries the `set_` prefix: unlike the declaration methods -//! ([`JniGen::package`], [`JniGen::convert`], …) which each add +//! ([`JniGenBuilder::package`], [`JniGenBuilder::convert`], …) which each add //! one item to the binding surface, a `set_` method changes how *all* other //! declarations are interpreted. Setters are **order-independent by //! construction**: the builder stores only raw inputs — settings here, @@ -22,19 +22,19 @@ //! //! | hook | names | input (the derived default) | default | //! |---|---|---|---| -//! | [`set_harness_name_mangle`](JniGen::set_harness_name_mangle) | the centralized externs object | `"JNINative"` | identity | -//! | [`set_fun_name_mangle`](JniGen::set_fun_name_mangle) | top-level package functions | package, camelCased Rust fn name (`put_publisher` → `"putPublisher"`) | identity | -//! | [`set_ptr_class_name_mangle`](JniGen::set_ptr_class_name_mangle) | `ptr_class` Kotlin classes | package, Rust type short name (`"KeyExpr"`) | identity | -//! | [`set_data_class_name_mangle`](JniGen::set_data_class_name_mangle) | `data_class` Kotlin classes | package, Rust type short name | identity | -//! | [`set_enum_name_mangle`](JniGen::set_enum_name_mangle) | `enum_class` Kotlin classes | package, Rust type short name | identity | -//! | [`set_method_name_mangle`](JniGen::set_method_name_mangle) | class methods/factories and JNI extern methods | package, final class name, full camelCase Rust fn name | identity | +//! | [`set_harness_name_mangle`](JniGenBuilder::set_harness_name_mangle) | the centralized externs object | `"JNINative"` | identity | +//! | [`set_fun_name_mangle`](JniGenBuilder::set_fun_name_mangle) | top-level package functions | package, camelCased Rust fn name (`put_publisher` → `"putPublisher"`) | identity | +//! | [`set_ptr_class_name_mangle`](JniGenBuilder::set_ptr_class_name_mangle) | `ptr_class` Kotlin classes | package, Rust type short name (`"KeyExpr"`) | identity | +//! | [`set_data_class_name_mangle`](JniGenBuilder::set_data_class_name_mangle) | `data_class` Kotlin classes | package, Rust type short name | identity | +//! | [`set_enum_name_mangle`](JniGenBuilder::set_enum_name_mangle) | `enum_class` Kotlin classes | package, Rust type short name | identity | +//! | [`set_method_name_mangle`](JniGenBuilder::set_method_name_mangle) | class methods/factories and JNI extern methods | package, final class name, full camelCase Rust fn name | identity | //! //! One further hook does NOT follow the identity rule, because its input and //! output are two names that must differ: //! //! | hook | names | input | default | //! |---|---|---|---| -//! | [`set_interface_name_mangle`](JniGen::set_interface_name_mangle) | the generated `.interface()` interface | package, final **class** name (`"Storage"`) | append `"Api"` (`"StorageApi"`); identity is a hard error | +//! | [`set_interface_name_mangle`](JniGenBuilder::set_interface_name_mangle) | the generated `.interface()` interface | package, final **class** name (`"Storage"`) | append `"Api"` (`"StorageApi"`); identity is a hard error | //! //! A per-decl `.name()` / `.interface_name()` override is always verbatim and //! **bypasses** the hooks entirely. @@ -52,7 +52,7 @@ pub(crate) enum NameKind { /// Raw naming spec of one declared class type, stored in [`TypeConfig`] as /// declared and turned into a concrete Kotlin FQN only when read -/// ([`JniGen::fqn_of`]), against whatever the settings are at that moment. +/// ([`JniGenBuilder::fqn_of`]), against whatever the settings are at that moment. #[derive(Clone)] pub(crate) struct NameSpec { pub(crate) subpackage: String, @@ -65,7 +65,7 @@ pub(crate) struct NameSpec { pub(crate) kind: NameKind, } -impl JniGen { +impl JniGenBuilder { /// Set the JVM/Kotlin **base** package (dot-separated, e.g. /// `"io.zenoh.jni"`). All derived forms (slash-separated `FindClass` /// paths, `_`-mangled JNI extern idents, Kotlin `package` declarations) @@ -209,7 +209,7 @@ impl JniGen { } } -impl JniGen { +impl JniGenBuilder { /// Materialize a [`NameSpec`] into a concrete Kotlin FQN under the /// current settings. Precedence for a declared class: per-decl /// `name_override` (package-resolved, mangle-bypassed), then the mangle diff --git a/prebindgen/src/api/lang/jnigen/jni/decl.rs b/prebindgen/src/api/lang/jnigen/jni/decl.rs index be45b650..5920c684 100644 --- a/prebindgen/src/api/lang/jnigen/jni/decl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/decl.rs @@ -1,20 +1,20 @@ //! Declaration objects: one standalone, independently-constructible value -//! type per kind of thing `JniGen` can be told about (a `ptr_class`, an +//! type per kind of thing `JniGenBuilder` can be told about (a `ptr_class`, an //! `enum_class`, a function, a scalar wire mapping, …), plus the `PackageDecl` //! that aggregates the package-scoped ones. Each type is both its own -//! "builder" and the final value `JniGen`/`PackageDecl` accepts — no separate +//! "builder" and the final value `JniGenBuilder`/`PackageDecl` accepts — no separate //! `Builder`/`Decl` split, no terminal `.build()` call. //! -//! `JniGen` itself only ever *accepts* fully-built values of these types -//! (`JniGen::package`, `JniGen::expand`, `JniGen::convert`, in +//! `JniGenBuilder` itself only ever *accepts* fully-built values of these types +//! (`JniGenBuilder::package`, `JniGenBuilder::expand`, `JniGenBuilder::convert`, in //! `builder.rs`); none of them reach back -//! into any `JniGen` state while being built. +//! into any `JniGenBuilder` state while being built. use super::*; // ────────────────────────────────────────────────────────────────────── // Shared local accumulators (replayed into `Expansions`/`Deconstructors` -// by the accept logic in `builder.rs` once a decl is handed to `JniGen`) +// by the accept logic in `builder.rs` once a decl is handed to `JniGenBuilder`) // ────────────────────────────────────────────────────────────────────── /// One arm of an `expand_param!` `.variant*` list (type-level or per-fn). @@ -325,7 +325,7 @@ macro_rules! fields { /// out. /// /// Build one with [`ptr_class!`](crate::ptr_class), add it to a -/// [`PackageDecl`], and hand that to [`JniGen::package`]. +/// [`PackageDecl`], and hand that to [`JniGenBuilder::package`]. /// /// A `PtrClassDecl` defines the **Kotlin class only** — its name /// ([`name`](Self::name)), its instance methods ([`method`](Self::method)), and its @@ -333,7 +333,7 @@ macro_rules! fields { /// type crosses the FFI boundary by default — accepted as which parameter /// variants, returned as which field set — is declared separately with /// [`expand_param!`](crate::expand_param) / [`expand_return!`](crate::expand_return) -/// handed to [`JniGen::expand`]; any single +/// handed to [`JniGenBuilder::expand`]; any single /// function can override those defaults locally (see [`FunctionDecl`]). /// /// ``` @@ -374,7 +374,7 @@ macro_rules! class_interface_methods { /// public instance surface, and make the class implement it (every /// class-body member gains the `override` modifier). The interface /// is named by [`interface_name`](Self::interface_name), else the - /// [`JniGen::set_interface_name_mangle`] hook over the final class + /// [`JniGenBuilder::set_interface_name_mangle`] hook over the final class /// name (default: append `"Api"`). /// /// This is the compiler-checked half of the integration hatch: a @@ -390,7 +390,7 @@ macro_rules! class_interface_methods { } /// Name the generated interface literally (relative, no dots), - /// bypassing the [`JniGen::set_interface_name_mangle`] hook. + /// bypassing the [`JniGenBuilder::set_interface_name_mangle`] hook. /// Implies [`interface`](Self::interface). pub fn interface_name(mut self, name: impl Into) -> Self { let name = name.into(); @@ -474,7 +474,7 @@ impl PtrClassDecl { } /// Rename the generated Kotlin class. By default it is named after the - /// Rust type (via the [`JniGen::set_ptr_class_name_mangle`] hook); `.name("Foo")` + /// Rust type (via the [`JniGenBuilder::set_ptr_class_name_mangle`] hook); `.name("Foo")` /// sets it literally instead. Relative name, no dots — the package comes /// from the enclosing [`PackageDecl`]. pub fn name(mut self, name: impl Into) -> Self { @@ -524,7 +524,7 @@ impl From for PtrClassDecl { /// /// Build one with [`expand_param!`](crate::expand_param), add arms with /// [`variant`](Self::variant) / [`variant_self`](Self::variant_self), and hand -/// it to [`JniGen::expand`]. +/// it to [`JniGenBuilder::expand`]. /// /// **Generated shape** — at the wire tier this is a selector dispatch: with /// more than one arm the parameter crosses as a selector `Int` plus one @@ -642,7 +642,7 @@ impl ExpandParamDecl { /// /// Build one with [`expand_return!`](crate::expand_return), add fields with /// [`field`](Self::field) / [`field_self`](Self::field_self), and hand it to -/// [`JniGen::expand`]. +/// [`JniGenBuilder::expand`]. /// /// The type does **not** have to be declared in any package. A boundary decl /// on an undeclared type makes it **rust-side-only**: every returned / @@ -952,7 +952,7 @@ impl FieldsDecl { } } -/// Unifies the two boundary decls into one type so [`JniGen::expand`] can +/// Unifies the two boundary decls into one type so [`JniGenBuilder::expand`] can /// expose a single entry point — the boundary-decl peer of [`ClassDecl`]. /// Deliberately **no** `impl From for ExpandDecl` — a bare /// `syn::Type` alone doesn't say which direction it describes, so every @@ -1234,7 +1234,7 @@ impl From for ClassDecl { /// [`name`](Self::name) to set its Kotlin name. /// [`expand_param`](Self::expand_param) / [`expand_return`](Self::expand_return) /// **override, for this one function**, the boundary defaults its -/// parameter/return types declare at the generator level ([`JniGen::expand`]) +/// parameter/return types declare at the generator level ([`JniGenBuilder::expand`]) /// — using the very same decl objects, so the complete-set rule is identical /// at both scopes. pub struct FunctionDecl { @@ -1532,7 +1532,7 @@ impl ConstDecl { /// exactly when nothing flows in (a unary conversion source must be a /// named callable — see [`ConvertDecl`]). Fns referenced only inside /// expressions are undeclared to the registry — acknowledge them via - /// [`JniGen::ignore`] (+ [`matching`](crate::lang::matching)). + /// [`JniGenBuilder::ignore`] (+ [`matching`](crate::lang::matching)). pub fn expr(self, ty: syn::Type, expr: syn::Expr) -> Self { self.set_source(ConstSource::Expr { ty, expr }) } @@ -1554,7 +1554,7 @@ pub(crate) struct ConstExprDecl { /// Declares a `#[prebindgen]` item this binding deliberately does NOT /// bind: nothing is emitted for it and the registry's per-item "skipping /// undeclared" warning is suppressed. One acceptor -/// ([`JniGen::ignore`]), the kind carried by what you built: +/// ([`JniGenBuilder::ignore`]), the kind carried by what you built: /// /// ```rust,ignore /// .ignore(fun!(string_len)) // a fn @@ -1627,7 +1627,7 @@ where /// (`package!("session")`, or `package!()` for the base package), fill it /// with [`class`](Self::class) / [`fun`](Self::fun) / /// [`constant`](Self::constant), and hand it to -/// [`JniGen::package`]. Reopening the same subpackage across several +/// [`JniGenBuilder::package`]. Reopening the same subpackage across several /// `PackageDecl`s is fine — they merge. pub struct PackageDecl { pub(crate) name: String, @@ -1638,7 +1638,7 @@ pub struct PackageDecl { impl PackageDecl { /// `name` is dot-separated, relative to the base package set by - /// [`JniGen::set_package_prefix`]; the empty string is the base + /// [`JniGenBuilder::set_package_prefix`]; the empty string is the base /// package itself. See [`crate::package!`] for the equivalent macro form /// (`package!("model")` / `package!()`). pub fn new(name: impl Into) -> Self { @@ -1718,7 +1718,7 @@ impl PackageDecl { /// an otherwise legal domain. A `try_` source's `Err` /// routes to the caller's error handler. Conversion fns may live in the flat /// crate or in a **helper crate** whose item stream is chained into the same -/// [`crate::core::Registry::from_items`] call; generated calls qualify each +/// [`crate::core::Flat::builder`] parse; generated calls qualify each /// function with its origin crate. /// /// Distinct from the [`expand_param!`](crate::expand_param) / @@ -1884,7 +1884,7 @@ pub struct ConvertDecl { pub(crate) output: Option, pub(crate) domain: Option, /// Binding-local fn sources declared on this convert (`fun!(crate::f) - /// .sig(…)`): drained into [`JniGen::local_fns`] at acceptance so the + /// .sig(…)`): drained into [`JniGenBuilder::local_fns`] at acceptance so the /// synthesis pre-pass covers them. pub(crate) locals: Vec<(syn::Ident, syn::Path, syn::Signature)>, } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs index 67bc9f11..353921bf 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs @@ -2,6 +2,7 @@ //! Kotlin `run`. use super::*; +use crate::api::core::registry::Conversions; /// Build the input-converter body for an `impl Fn(args)` parameter: a /// trampoline that wraps the Kotlin **lambda** (`(leaves…) -> Unit`, erased to @@ -19,9 +20,9 @@ use super::*; /// Errors cannot reach a caller-side error sink (the declaring call already /// returned), so they are converted to `__JniErr` and logged via `tracing`. pub(crate) fn callback_input( - ext: &JniGen, + ext: &JniGenBuilder, args: &[syn::Type], - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { // Human-readable tag for attach/log messages. let name = format!( @@ -70,8 +71,7 @@ pub(crate) fn callback_input( // user callback's `run(List)`. Reuses the OUTPUT fold's folder // interface + appender singleton, driven from the trampoline. if let Some(plan) = registry - .callback_arg_plans - .get(&TypeKey::from_type(arg_ty)) + .callback_arg_plan(&TypeKey::from_type(arg_ty)) .filter(|p| super::render::is_iterable_fold(&p.shape)) { // Every leaf converter must already be resolved (deferral safety). @@ -162,7 +162,7 @@ pub(crate) fn callback_input( // Decomposed arg: deliver the leaves of its type-level canonical // output, exactly like a return delivery. - if let Some(plan) = registry.callback_arg_plans.get(&TypeKey::from_type(arg_ty)) { + if let Some(plan) = registry.callback_arg_plan(&TypeKey::from_type(arg_ty)) { // Deferral safety: every leaf converter (and identity-leaf // projection) must already be resolved — return None so the rank // resolver retries this converter later otherwise. A synthesized diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs b/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs index f7b48c71..d840a347 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs @@ -1,7 +1,7 @@ //! Scalar / `Option` / enum converter bodies and their wire probes. use super::*; -use crate::api::core::registry::TypeEntry; +use crate::api::core::registry::{Conversions, TypeEntry}; /// Sentinel value to return through the wrapper signature when the inner /// closure errors. Must compile against any wire type we emit. @@ -243,7 +243,7 @@ pub(crate) fn composed_inner_output( /// fails and the resolver falls through to other rank-1 attempts. pub(crate) fn option_input( t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr, Niches)> { let inner_entry = registry.input_entry(t1)?; let inner_wire = inner_entry.destination.clone(); @@ -309,7 +309,7 @@ pub(crate) fn option_input( /// Build `Option`'s output converter — symmetric to [`option_input`]. pub(crate) fn option_output( t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr, Niches)> { let inner_entry = registry.output_entry(t1)?; let inner_wire = inner_entry.destination.clone(); @@ -403,8 +403,8 @@ pub(crate) fn default_niches_for_wire(wire: &syn::Type) -> Niches { /// upstream type a bare `` resolves to in their include-site /// `use` statements. Pairs with output body below. pub(crate) fn enum_input_body( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, e: &syn::ItemEnum, ) -> (syn::Type, syn::Expr) { assert_only_unit_variants(e); @@ -443,7 +443,7 @@ pub(crate) fn enum_input_body( /// upstream of the cast. The body works without naming the enum type /// at all — `v` is already typed via the wrapper signature, so the /// `as` cast picks up the right type by inference. -pub(crate) fn enum_output_body(_ext: &JniGen, e: &syn::ItemEnum) -> (syn::Type, syn::Expr) { +pub(crate) fn enum_output_body(_ext: &JniGenBuilder, e: &syn::ItemEnum) -> (syn::Type, syn::Expr) { assert_only_unit_variants(e); let body: syn::Expr = syn::parse_quote!({ v as jni::sys::jint }); (syn::parse_quote!(jni::sys::jint), body) @@ -485,7 +485,7 @@ pub(crate) fn assert_only_unit_variants(e: &syn::ItemEnum) { pub(crate) fn nullable_kind_for( outer_wire: &syn::Type, inner_ty: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> NullableKind { let inner_dest = registry .input_entry(inner_ty) @@ -504,7 +504,7 @@ pub(crate) fn nullable_kind_for( pub(crate) fn nullable_kind_for_output( outer_wire: &syn::Type, inner_ty: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> NullableKind { let inner_dest = registry .output_entry(inner_ty) diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index dc17426e..65bb6bc7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -1,7 +1,10 @@ //! Output-expansion delivery: unfold plans and leaf encoding. use super::*; -use crate::api::core::unfold::{steps_are_movable, PathStep}; +use crate::api::core::{ + registry::Conversions, + unfold::{steps_are_movable, PathStep}, +}; /// Emit the output-expansion delivery body (output phase) for a function /// marked `.expand_output()`. The return value (`__out`) is decomposed by the @@ -27,7 +30,7 @@ use crate::api::core::unfold::{steps_are_movable, PathStep}; /// [`UnfoldShape::Base`]: crate::api::core::unfold::UnfoldShape::Base /// [`UnfoldShape::Optional`]: crate::api::core::unfold::UnfoldShape::Optional pub(crate) fn emit_unfold_delivery( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, plan: &crate::api::core::unfold::UnfoldPlan, iface: Option<&IfaceSpec>, @@ -785,8 +788,8 @@ fn reach_leaf( /// arm of fallible externs (whose `fail` falls back to a binding-error /// `signal_error` with default ze values). pub(crate) fn encode_plan_leaves( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, plan: &crate::api::core::unfold::UnfoldPlan, obj_idents: &[syn::Ident], value: &TokenStream, @@ -1302,7 +1305,7 @@ pub(crate) fn encode_plan_leaves( /// [`crate::api::lang::jnigen::jni::iface`] derives for the same leaf — a /// nullable primitive boxes (object chunk), object wires pass as objects. pub(crate) fn leaf_is_prim( - registry: &Registry, + registry: &impl Conversions, leaf: &crate::api::core::unfold::UnfoldLeaf, ) -> bool { // The synthesized sum selector is a `jint` by definition — it is assigned, @@ -1326,7 +1329,7 @@ pub(crate) fn leaf_is_prim( /// primitive** slot? Split out so the interface derivation can ask the question /// about a leaf whose own `nullable` flag it is in the middle of computing (an /// inert sum group slot). -pub(crate) fn leaf_ty_is_prim(registry: &Registry, out_ty: &syn::Type) -> bool { +pub(crate) fn leaf_ty_is_prim(registry: &impl Conversions, out_ty: &syn::Type) -> bool { let Some(entry) = registry.output_entry(out_ty) else { return false; }; diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 9d6cd965..1c4a60a9 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -2,11 +2,12 @@ //! expressions, and the Rust-side reconstruct. use super::*; +use crate::api::core::registry::Conversions; pub(crate) fn struct_input_body( - ext: &JniGen, + ext: &JniGenBuilder, s: &syn::ItemStruct, - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { let struct_name = s.ident.to_string(); let struct_module = struct_module_path(ext, registry, s); @@ -279,9 +280,9 @@ pub(crate) fn struct_input_body( /// one field out of a `JObject` the caller already handed us costs nothing /// extra, so the asymmetry is real rather than an oversight. pub(crate) fn sum_input_body( - ext: &JniGen, + ext: &JniGenBuilder, e: &syn::ItemEnum, - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { use crate::api::core::types_util::SumSpec; @@ -376,8 +377,8 @@ pub(crate) fn sum_input_body( /// [`struct_input_body`] performs, for the positions that are properties of a /// generated class rather than fields of a data class. fn read_kotlin_property( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, receiver: &TokenStream, prop: &str, ty: &syn::Type, @@ -868,7 +869,7 @@ fn wire_kotlin_type(entry: &crate::api::core::registry::TypeEntry) - /// text spliced into a wrapper whose import set this plan does not own. #[allow(clippy::too_many_arguments)] fn build_flat_sum_field( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, sum_ty: &syn::Type, field: syn::Ident, @@ -1115,7 +1116,7 @@ fn push_handle_leaf( /// `.jobject_input()` opt-in); an unmarked data class either returns a complete /// plan or a validation error — never a silent object fallback. pub(crate) fn build_flat_input_plan( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, param_name: &syn::Ident, arg_ty: &syn::Type, @@ -1201,7 +1202,7 @@ pub(crate) fn build_flat_input_plan( #[allow(clippy::too_many_arguments)] fn build_flat_struct_node( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, st: &syn::ItemStruct, optional: bool, @@ -1796,7 +1797,7 @@ pub(crate) struct OptionScalarInputPlan { /// only the cases that *would* box are intercepted — niche cases (already /// unboxed / ABI-clean) and opaque/value projections are left untouched. pub(crate) fn build_option_scalar_input_plan( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, param_name: &syn::Ident, arg_ty: &syn::Type, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs index 911dfeda..a5d19bb7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs @@ -5,8 +5,8 @@ use super::*; /// Last-segment ident of a `TypeKey` — e.g. `"Publisher<'static>"` → /// `"Publisher"`, `"AdvancedSubscriber<()>"` → `"AdvancedSubscriber"`. Used by -/// the structured builders ([`JniGen::ptr_class`], -/// [`JniGen::data_class`]) to derive a default Kotlin class name from +/// the structured builders ([`JniGenBuilder::ptr_class`], +/// [`JniGenBuilder::data_class`]) to derive a default Kotlin class name from /// the Rust type-key. Panics for non-path types (e.g. closures, references) — /// the per-kind `*_name_mangle` closures see only path-shaped /// shorts. For verbatim Kotlin expressions on non-path types, use a @@ -23,7 +23,7 @@ pub(crate) fn rust_short_name(key: &TypeKey) -> String { /// Fallible variant of [`rust_short_name`] — returns `None` for /// non-path types instead of panicking. Used by -/// [`JniGen::note_wrapper_registration`] which is called for rank-0 +/// [`JniGenBuilder::note_wrapper_registration`] which is called for rank-0 /// wrapper patterns including non-path shapes like `()` where there /// is no Kotlin short name to derive. pub(crate) fn rust_short_name_opt(key: &TypeKey) -> Option { diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index dfb6cf38..1d5b6016 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -2,6 +2,7 @@ //! synthesis probe. use super::*; +use crate::api::core::registry::Conversions; /// Resolve the typed-handle Kotlin FQN for a handle-bearing struct field /// and assert its folded strategy is one the struct encode/decode bridge @@ -9,7 +10,7 @@ use super::*; /// in `Nullable`) are encodable as a single `L;` ctor arg; a /// collection layer (`Iterable`, i.e. `Vec`) would need array /// codegen and is a loud build-time error until implemented. -pub(crate) fn handle_field_fqn(ext: &JniGen, h: &Projection) -> String { +pub(crate) fn handle_field_fqn(ext: &JniGenBuilder, h: &Projection) -> String { fn assert_scalar(s: &FoldStrategy) { match s { FoldStrategy::Base => {} @@ -80,8 +81,8 @@ pub(crate) fn primitive_default_for_descriptor(sig: &str) -> TokenStream { /// model (`registry.flat()`) — both populated before `resolve` — never the /// output converter table (not yet built at this stage). pub(crate) fn synth_value_struct_leaves( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, s: &syn::ItemStruct, path_prefix: &[crate::api::core::unfold::PathStep], name_prefix: &str, @@ -170,8 +171,8 @@ pub(crate) fn synth_value_struct_leaves( /// plan `flatten_struct_factory` walks for the Kotlin side, so the slot /// order and JVM descriptors agree by construction. pub(crate) fn flatten_struct_encode( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, s: &syn::ItemStruct, access: &TokenStream, prefix: &str, @@ -574,9 +575,9 @@ fn encode_field( } pub(crate) fn struct_output_body( - ext: &JniGen, + ext: &JniGenBuilder, s: &syn::ItemStruct, - registry: &Registry, + registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { let struct_name = s.ident.to_string(); // Prefer the registered Kotlin FQN (`io.zenoh.jni.JniSample`) so the @@ -636,8 +637,8 @@ pub(crate) fn struct_output_body( } pub(crate) fn struct_module_path( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, s: &syn::ItemStruct, ) -> syn::Path { // The module the struct is reachable under from the generated file: its diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index 8b350f1b..95707c61 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -17,6 +17,7 @@ //! parent's `fromParts`, a return's ride the hoisted builder singleton. use super::*; +use crate::api::core::registry::Conversions; /// Leaf name of the synthesized selector. Distinct from every group slot by /// construction: a group slot always contains the `_` that separates its @@ -40,7 +41,7 @@ pub(crate) const SUM_TAG_LEAF: &str = "tag"; /// `variant!(V).name(...)` rename carries through to the builder's parameter /// names too. pub(crate) fn synth_sum_leaves( - ext: &JniGen, + ext: &JniGenBuilder, sum_cfg: &SumConfig, item_enum: &syn::ItemEnum, ) -> Vec { @@ -104,7 +105,7 @@ pub(crate) struct Slot { } pub(crate) fn leaf_slot( - registry: &Registry, + registry: &impl Conversions, leaf: &crate::api::core::unfold::UnfoldLeaf, ) -> Slot { use crate::api::core::unfold::LeafSource; @@ -161,8 +162,8 @@ pub(crate) fn is_sum_leaves(leaves: &[crate::api::core::unfold::UnfoldLeaf]) -> /// is that a leaf here is not an independent expression — its slot exists in /// every arm and only one arm computes it. pub(crate) fn encode_sum_group( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, leaves: &[crate::api::core::unfold::UnfoldLeaf], obj_idents: &[syn::Ident], matched: TokenStream, @@ -320,7 +321,7 @@ pub(crate) fn encode_sum_group( /// payload and a struct field of the same type reach their converter the same /// way. fn encode_group_leaf( - registry: &Registry, + registry: &impl Conversions, leaf: &crate::api::core::unfold::UnfoldLeaf, obj_ident: &syn::Ident, prim: bool, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs index c6f672d7..8a77fd92 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs @@ -30,7 +30,7 @@ pub(crate) fn slice_or_vec_elem(arg_ty: &syn::Type) -> Option<(syn::Type, bool)> /// classifier, `render_extern_decl`, and the synthetic-extern emitter so all /// four sites agree on which params take the handle path. pub(crate) fn vec_build_elem( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, arg_ty: &syn::Type, ) -> Option<(syn::Type, bool)> { @@ -60,7 +60,7 @@ pub(crate) fn vec_build_elem( /// Deduped by [`TypeKey`] and sorted for deterministic output (mirrors /// [`build_handle_destructor_items`]). pub(crate) fn collect_vec_build_elem_types( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, ) -> Vec { let declared = ext.declared_functions(); @@ -99,7 +99,7 @@ pub(crate) struct VecBuildHelpers { /// from the element's **Kotlin** data-class short name (first char lowercased) so /// the generated methods read naturally (`Payload` → `payloadVec`). pub(crate) fn vec_build_helpers( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, elem: &syn::Type, ) -> Option { @@ -137,7 +137,7 @@ pub(crate) fn vec_build_helpers( /// through the method mangler like every other `JNINative` extern. The Rust JNI symbol /// (see [`vec_helper_symbol`]) and the Kotlin call site both use this, so they /// agree. -pub(crate) fn vec_helper_method_name(ext: &JniGen, base: &str, suffix: &str) -> String { +pub(crate) fn vec_helper_method_name(ext: &JniGenBuilder, base: &str, suffix: &str) -> String { ext.mangle_jni_method(&format!("{base}{suffix}")) } @@ -145,7 +145,7 @@ pub(crate) fn vec_helper_method_name(ext: &JniGen, base: &str, suffix: &str) -> /// `Java___…` scheme function wrappers use via the plan's /// `native_symbol` (see `symbol`, #86); these helpers live on the /// `JNINative` object, so they share its class path. -fn vec_helper_symbol(ext: &JniGen, base: &str, suffix: &str) -> String { +fn vec_helper_symbol(ext: &JniGenBuilder, base: &str, suffix: &str) -> String { ext.native_method_symbol(&vec_helper_method_name(ext, base, suffix)) } @@ -164,7 +164,7 @@ fn vec_helper_symbol(ext: &JniGen, base: &str, suffix: &str) -> String { /// shared across all callers of a given element type). This keeps the Kotlin /// push loop free of a per-element failure check. pub(crate) fn build_vec_build_helper_items( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, ) -> Vec { let mut named: Vec<(String, syn::Item)> = Vec::new(); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 93d8ea8c..4546f6cb 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -2,10 +2,10 @@ //! params, and the expanded-param path. use super::*; -use crate::api::core::types_util::result_ok_type; +use crate::api::core::{registry::Conversions, types_util::result_ok_type}; pub(crate) fn emit_jni_function_wrapper( - ext: &JniGen, + ext: &JniGenBuilder, f: &syn::ItemFn, registry: &Registry, ) -> TokenStream { @@ -14,7 +14,7 @@ pub(crate) fn emit_jni_function_wrapper( /// The synthetic nullary getter signature a declared const is emitted /// through: `pub fn const_get_() -> `. Both sides — -/// the Rust extern ([`JniGen::on_const`] via +/// the Rust extern ([`JniGenBuilder::on_const`] via /// [`emit_jni_function_wrapper_with_callee`]) and the Kotlin `val` /// initializer (`render_const_val`) — derive the extern symbol from this one /// ident, so they stay in sync by construction. The body is never used. @@ -32,7 +32,7 @@ pub(crate) fn const_getter_fn(c: &syn::ItemConst) -> syn::ItemFn { /// shared closeable `val` is semantically wrong (whose `close()` is it?). /// Expose a factory function instead — the established idiom (e.g. zenoh's /// `encoding_const_*` companion factories). -pub(crate) fn reject_handle_const(ext: &JniGen, c: &syn::ItemConst) { +pub(crate) fn reject_handle_const(ext: &JniGenBuilder, c: &syn::ItemConst) { reject_handle_constant_type(ext, &c.ty, "const", &c.ident.to_string()); } @@ -40,7 +40,12 @@ pub(crate) fn reject_handle_const(ext: &JniGen, c: &syn::ItemConst) { /// `&`/`Option`/`Vec` layers off `ty` and reject if what remains is a /// declared opaque handle. `what`/`ident` shape the error message /// (`const MAX_LEN` / `constant fn encoding_const_x_str`). -pub(crate) fn reject_handle_constant_type(ext: &JniGen, ty: &syn::Type, what: &str, name: &str) { +pub(crate) fn reject_handle_constant_type( + ext: &JniGenBuilder, + ty: &syn::Type, + what: &str, + name: &str, +) { let mut ty = ty.clone(); loop { if let syn::Type::Reference(r) = &ty { @@ -74,7 +79,7 @@ pub(crate) fn reject_handle_constant_type(ext: &JniGen, ty: &syn::Type, what: &s /// the `val` initializer's throwing `JniErrorHandler` only fits the /// infallible wrapper shape), and its return type must not peel to a /// declared opaque handle (same rationale as [`reject_handle_const`]). -pub(crate) fn validate_constant_fn(ext: &JniGen, f: &syn::ItemFn) { +pub(crate) fn validate_constant_fn(ext: &JniGenBuilder, f: &syn::ItemFn) { assert!( f.sig.inputs.is_empty(), "constant fn `{}`: takes {} parameter(s) — a function-backed constant must be nullary \ @@ -110,7 +115,7 @@ pub(crate) fn const_expr_getter_fn(kotlin_name: &str, ty: &syn::Type) -> syn::It /// Validates an expression constant's declared value type (checked on both /// write paths): not a `Result` (a domain-fallible value is not a constant), /// not (peeled to) a declared opaque handle. -pub(crate) fn validate_constant_expr(ext: &JniGen, kotlin_name: &str, ty: &syn::Type) { +pub(crate) fn validate_constant_expr(ext: &JniGenBuilder, kotlin_name: &str, ty: &syn::Type) { assert!( result_ok_type(ty).is_none(), "constant expr `{kotlin_name}`: type is a `Result` — an expression constant must be \ @@ -122,11 +127,11 @@ pub(crate) fn validate_constant_expr(ext: &JniGen, kotlin_name: &str, ty: &syn:: /// [`emit_jni_function_wrapper`] with the raw callee expression overridable: /// `None` = the ordinary `::(args)` call; `Some(e)` /// splices `e` verbatim as the value the output phase converts. Used by the -/// const getter emission (`JniGen::on_const`), whose synthetic nullary `f` +/// const getter emission (`JniGenBuilder::on_const`), whose synthetic nullary `f` /// carries the signature while the value comes from /// `::` — a path, not a call. pub(crate) fn emit_jni_function_wrapper_with_callee( - ext: &JniGen, + ext: &JniGenBuilder, f: &syn::ItemFn, registry: &Registry, callee: Option, @@ -163,13 +168,13 @@ pub(crate) fn emit_jni_function_wrapper_with_callee( // value is **returned** directly through its ordinary output // converter — the wrapper behaves exactly like a normal function // whose return type is `convert_out_ty`. - let unfold_plan = registry.unfold_plans.get(original_ident); + let unfold_plan = registry.unfold_plans().get(original_ident); // Error-position expansion: when the fn returns `Result` and an error // plan is declared, the **`?`** is applied here — the extern peels the // `Result` (Err arm decomposes `E` into the `ze` leaves and invokes the // typed DOMAIN handler), and the success path uses `T`'s converter (not the // `Result` rank-2 wrapper). - let error_plan = registry.error_plans.get(original_ident); + let error_plan = registry.error_plans().get(original_ident); let is_convert = matches!(&plan.output, FnOutputPlan::Value(v) if v.is_convert); // The output converter entry (`None` for callback delivery). The lookup // was validated at plan build; re-resolving here keeps the plan free of @@ -428,7 +433,7 @@ fn unfold_builder_param(iterable_fold: bool) -> TokenStream { /// site only renders each [`InputKind`]'s decode. #[allow(clippy::type_complexity)] fn emit_input_param( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, original_ident: &syn::Ident, param: &PlanParam, @@ -440,7 +445,7 @@ fn emit_input_param( let leaf = match ¶m.form { ParamForm::Expanded(leaves) => { let fold = registry - .expansion_plans + .expansion_plans() .get(&(original_ident.clone(), param.ident.clone())) .expect("ParamForm::Expanded ⇒ expansion plan present"); return emit_expanded_param(ext, registry, fold, leaves, ¶m.ident, on_err); @@ -697,7 +702,7 @@ fn emit_plain_decode( /// through the same error sink as any fallible input. The returned call /// argument is the built value (`&value` when the original parameter was `&T`). pub(crate) fn emit_expanded_param( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, plan: &crate::api::core::expand::FoldPlan, leaves: &[PlanLeaf], diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index b4d6f6b8..cdb770a1 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -11,6 +11,7 @@ //! plan to function granularity; the output side follows in a later stage. use super::*; +use crate::api::core::registry::Conversions; /// The lowered plan for one bound function: one [`PlanParam`] per source /// `syn::Signature` parameter (non-`Typed`/non-`Ident` args — `self`, @@ -30,7 +31,7 @@ pub(crate) struct JniFunctionPlan { /// The onError handler interfaces — the always-present binding /// `JniErrorHandler` plus, for a fallible function, its typed domain /// `Handler` (see [`ErrorIfaces`]). Shared from the - /// [`JniGen::iface_spec`] memo: one derivation per channel feeds the Rust + /// [`JniGenBuilder::iface_spec`] memo: one derivation per channel feeds the Rust /// `__SINK_*` statics, the Kotlin sink wiring, and the interface /// declarations, so the FQN/descriptor pairs of the cached `run` lookups /// cannot drift. `None` = the domain channel is underivable (the Rust @@ -149,7 +150,7 @@ pub(crate) struct UnfoldOutputPlan { /// The builder/folder `fun interface` spec the delivery calls into — /// [`folder_iface_for_plan`] for an iterable fold (incl. the fixed /// whole-element form), the memoized [`SpecKey::Builder`] spec - /// otherwise. Shared from the [`JniGen::iface_spec`] memo: one + /// otherwise. Shared from the [`JniGenBuilder::iface_spec`] memo: one /// derivation feeds the Rust upcall statics, every Kotlin surface read, /// and the interface declaration, so the cached `run` FQN/descriptor /// pair cannot drift. `None` = underivable (the Rust emitter keeps its @@ -241,7 +242,7 @@ impl PlanError { ), PlanError::UnresolvedOutput { ty } => format!( "JniGen::on_function: return type `{}` of `{}` has no registered output \ - converter — register one via `JniGen::output_wrapper(pat, |…| Some((ty, exc, body)))` \ + converter — register one via `JniGenBuilder::output_wrapper(pat, |…| Some((ty, exc, body)))` \ (exc = `None` for non-throwing, `Some(parse_quote!())` \ to bind a domain exception)", ty, fn_ident, @@ -266,7 +267,7 @@ impl PlanError { /// /// [`Prebindgen::validate_resolved`]: crate::api::core::prebindgen::Prebindgen::validate_resolved pub(crate) fn validate_bindings( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, ) -> Result<(), String> { let mut errors: Vec = Vec::new(); @@ -314,7 +315,7 @@ pub(crate) fn validate_bindings( } // Declared consts: their synthetic nullary getters run through the same - // plan machinery (`JniGen::on_const`). + // plan machinery (`JniGenBuilder::on_const`). if let Some(declared_consts) = ext.declared_consts() { let mut consts: Vec<&crate::api::core::flat::Constant> = registry.flat().constants().collect(); @@ -371,7 +372,7 @@ impl FnOutputPlan { } } -impl JniGen { +impl JniGenBuilder { /// The memoized lowered plan for one bound function — the "build the plan /// once and store it" stage [`JniFunctionPlan::build`] anticipated (issue /// #90). Keyed by the function's ident (bound functions live in one flat @@ -381,7 +382,7 @@ impl JniGen { /// (an unresolved converter) is passed through — it only occurs at the /// validation phase, which reports it and fails `resolve` before any /// emitter runs. Same interior-mutable contract as - /// [`JniGen::iface_spec`]; drift is guarded externally by the byte-identity + /// [`JniGenBuilder::iface_spec`]; drift is guarded externally by the byte-identity /// regen check (a plan change alters generated code). pub(crate) fn fn_plan( &self, @@ -401,10 +402,10 @@ impl JniGen { impl JniFunctionPlan { /// Lower `f`'s inputs. Deterministic over `(ext, registry, f)`. Emission - /// and validation go through the memo [`JniGen::fn_plan`], so the plan is + /// and validation go through the memo [`JniGenBuilder::fn_plan`], so the plan is /// built ONCE per function and shared; this is the underlying derivation. pub fn build( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, f: &syn::ItemFn, ) -> Result { @@ -427,7 +428,7 @@ impl JniFunctionPlan { let ty = (*pt.ty).clone(); let form = if let Some(plan) = registry - .expansion_plans + .expansion_plans() .get(&(f.sig.ident.clone(), ident.clone())) { let mut leaves = Vec::new(); @@ -496,7 +497,7 @@ impl JniFunctionPlan { FnOutputPlan::Value(_) => 0, }; slots += 1; // binding-error sink - if registry.error_plans.contains_key(&f.sig.ident) { + if registry.error_plans().contains_key(&f.sig.ident) { slots += 1; } slots @@ -515,7 +516,7 @@ fn kotlin_jvm_slots(ty: &str) -> usize { /// collection helper; recursive data-class leaves are valid in constructor /// expansions and reuse the same Rust/Kotlin lowering as ordinary parameters. fn classify_leaf( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, ident: &syn::Ident, ty: &syn::Type, @@ -609,7 +610,7 @@ fn classify_leaf( /// declared-surface facts from `classify_return`'s inputs /// (render_extern_decl's `ret_decl` reconstruction). fn build_output( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, f: &syn::ItemFn, ) -> Result { @@ -618,7 +619,7 @@ fn build_output( unfold::{Delivery, UnfoldShape}, }; let ident = &f.sig.ident; - let unfold_plan = registry.unfold_plans.get(ident); + let unfold_plan = registry.unfold_plans().get(ident); // Callback delivery: the return is decomposed to a foreign builder/fold // lambda; no output converter runs and the wire is the erased `JObject`. @@ -664,7 +665,7 @@ fn build_output( syn::ReturnType::Default => syn::parse_quote!(()), syn::ReturnType::Type(_, ty) => (**ty).clone(), }; - let error_plan = registry.error_plans.get(ident); + let error_plan = registry.error_plans().get(ident); let ok_ty = error_plan.and_then(|_| result_ok_type(&return_ty)); let target_ty = match unfold_plan { Some(p) => p @@ -711,8 +712,8 @@ impl ReturnSurface { /// the single peel that subsumed both `classify_return`'s inline peel /// and the former `canonical_return_ty`. pub fn classify( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, output: &syn::ReturnType, ) -> (Self, syn::Type) { let ty = match output { diff --git a/prebindgen/src/api/lang/jnigen/jni/fold.rs b/prebindgen/src/api/lang/jnigen/jni/fold.rs index 8514ed7a..4e3caf49 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fold.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fold.rs @@ -55,7 +55,7 @@ pub(crate) fn handle_kt_type(strategy: &FoldStrategy, leaf: &kt::KtType) -> kt:: /// Typed Kotlin leaf of a projection. Declared handle projections /// take their configured class FQN; the built-in `u64` projection is Kotlin's /// stable unsigned scalar type. -pub(crate) fn projection_leaf_kt(ext: &JniGen, proj: &Projection) -> Option { +pub(crate) fn projection_leaf_kt(ext: &JniGenBuilder, proj: &Projection) -> Option { match proj.kind { ProjectionKind::Handle => ext.kotlin_fqn(&proj.leaf_key).map(kt::KtType::cls), ProjectionKind::Unsigned64 => Some(kt::KtType::cls("ULong")), @@ -169,7 +169,7 @@ pub(crate) fn is_kotlin_primitive_ty(t: &kt::KtType) -> bool { /// optional) and a leaf reconstructs with its wrap. #[allow(clippy::too_many_arguments)] pub(crate) fn flatten_struct_factory( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, s: &syn::ItemStruct, prefix: &str, diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index fd5b2e89..6413ffef 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -14,7 +14,7 @@ //! `run` with raw typed `jvalue`s: no per-leaf boxing upcalls, no erased //! `FunctionN`. //! -//! Each identity's spec is derived ONCE, through the [`JniGen::iface_spec`] +//! Each identity's spec is derived ONCE, through the [`JniGenBuilder::iface_spec`] //! memo keyed by [`SpecKey`], and shared by all three sites — the //! FQN/descriptor pair cannot drift between the artifact tiers (issue #107). //! The constructors stay deterministic over `(ext, registry)`; in debug @@ -22,7 +22,10 @@ //! determinism is a checked invariant rather than a convention. use super::*; -use crate::api::core::unfold::{dedup_names, DeconId, LeafSource, UnfoldPlan}; +use crate::api::core::{ + registry::Conversions, + unfold::{dedup_names, DeconId, LeafSource, UnfoldPlan}, +}; /// The JVM-visible single method name of every generated callback interface. pub(crate) const IFACE_METHOD: &str = "run"; @@ -665,7 +668,7 @@ fn subject_short(ty: &syn::Type) -> String { /// Package a subject type's interface lives in: the package of the type's /// registered Kotlin FQN, the root `ext.package` otherwise. -fn subject_package(ext: &JniGen, subject: &syn::Type) -> String { +fn subject_package(ext: &JniGenBuilder, subject: &syn::Type) -> String { let key = TypeKey::from_type(&crate::api::core::types_util::peel_ref_option_vec(subject)); ext.kotlin_fqn(&key) .and_then(|fqn| fqn.rsplit_once('.').map(|(p, _)| p.to_string())) @@ -675,8 +678,8 @@ fn subject_package(ext: &JniGen, subject: &syn::Type) -> String { /// The interface param list for a decomposition's leaves: names from /// [`plan_leaf_names`], typed + raw views per leaf. fn plan_leaf_params( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, leaves: &[crate::api::core::unfold::UnfoldLeaf], ) -> Option> { // Decomposition leaf names are author-supplied, literal, and unique by @@ -695,8 +698,8 @@ fn plan_leaf_params( /// inert-group nullability rule below have to hold at every one of those sites, /// not just where the plan happens to be walked as a whole. fn plan_leaf_param( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, name: String, leaf: &crate::api::core::unfold::UnfoldLeaf, ) -> Option { @@ -740,8 +743,8 @@ fn plan_leaf_param( /// the close-unless-taken contract needs the native side to `close()` the /// wrapped object after the invoke. fn leaf_iface_param( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, name: String, out_ty: &syn::Type, nullable: bool, @@ -852,8 +855,8 @@ fn leaf_iface_param( /// `run` (close-unless-taken). Replaces the former Rust-side `new_object` + /// post-invoke `close()`. `None` if the arg's projection FQN can't be resolved. pub(crate) fn owned_handle_iface_param( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, name: String, out_ty: &syn::Type, nullable: bool, @@ -917,12 +920,12 @@ impl SpecKey { /// derivation ([`SpecKey::Folder`]'s typed groups) and the declaration /// emitter (the hoisted `fromParts`/appender singletons). pub(crate) fn fixed_decon_ids( - registry: &Registry, + registry: &impl Conversions, ) -> std::collections::HashSet { let fixed: std::collections::HashSet = registry - .unfold_plans + .unfold_plans() .values() - .chain(registry.callback_arg_plans.values()) + .chain(registry.callback_arg_plans().values()) .filter(|p| p.fixed_builder) .filter_map(|p| p.decon.clone()) .collect(); @@ -941,9 +944,9 @@ pub(crate) fn fixed_decon_ids( // wrapper that references an interface which no longer exists. debug_assert!( !registry - .unfold_plans + .unfold_plans() .values() - .chain(registry.callback_arg_plans.values()) + .chain(registry.callback_arg_plans().values()) .any(|p| !p.fixed_builder && p.decon.as_ref().is_some_and(|d| fixed.contains(d))), "fixed and non-fixed plans share one DeconId — the typed interface \ cannot be shaped (or suppressed) for both" @@ -958,9 +961,9 @@ pub(crate) fn fixed_leaf_element_keys( registry: &Registry, ) -> std::collections::HashSet { registry - .unfold_plans + .unfold_plans() .values() - .chain(registry.callback_arg_plans.values()) + .chain(registry.callback_arg_plans().values()) .filter(|p| p.fixed_builder) .filter_map(|p| p.element.as_ref()) .map(TypeKey::from_type) @@ -968,13 +971,13 @@ pub(crate) fn fixed_leaf_element_keys( } /// Derive the spec for one identity — the SINGLE construction point behind -/// [`JniGen::iface_spec`]. Any `syn` context comes from the key's stored +/// [`JniGenBuilder::iface_spec`]. Any `syn` context comes from the key's stored /// normalized type ([`TypeKey::to_type`] — a clone, not a reparse). A /// `Folder` derivation folds the fixed-builder typed-group view in per /// `DeconId` (see [`fixed_decon_ids`]). fn derive_iface_spec( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, key: &SpecKey, ) -> Option { match key { @@ -996,7 +999,7 @@ fn derive_iface_spec( } } -impl JniGen { +impl JniGenBuilder { /// The memoized spec for one interface identity: derived once per /// generator run and shared by every consumer — the resolve-time /// trampoline, the per-function plan, and the declaration emitter — so @@ -1008,7 +1011,7 @@ impl JniGen { /// instead of shipping descriptor drift. pub(crate) fn iface_spec( &self, - registry: &Registry, + registry: &impl Conversions, key: &SpecKey, ) -> Option> { let hit = self.iface_specs.borrow().get(key).cloned(); @@ -1042,8 +1045,8 @@ impl JniGen { /// property types, not the wire — so it reassembles through the same inlined /// `when` over the tag that a sum-typed struct field gets. fn fixed_reassembly( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, source: &syn::Type, leaves: &[crate::api::core::unfold::UnfoldLeaf], class_fqn: &str, @@ -1067,8 +1070,8 @@ fn fixed_reassembly( /// returning `Unit`. Named `Callback` (`Fn()` → `VoidCallback`), /// placed in the first arg type's package (root for `Fn()`). pub(crate) fn callback_iface_spec( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, cb_args: &[syn::Type], ) -> Option { // Per-arg grouping over the flat raw leaves. A **fixed-builder** (by-value @@ -1119,7 +1122,7 @@ pub(crate) fn callback_iface_spec( // params, no reassembly group). Only `Base`/accessor plans decompose the // arg into the callback's `run` params here. let plan = registry - .callback_arg_plans + .callback_arg_plans() .get(&TypeKey::from_type(t)) .filter(|p| !super::render::is_iterable_fold(&p.shape)); if let Some(plan) = plan { @@ -1299,11 +1302,11 @@ pub(crate) fn callback_iface_spec( /// function's own plan. Named `Builder`, placed in the source /// type's package. pub(crate) fn builder_iface_spec( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, decon: &DeconId, ) -> Option { - let spec = registry.decon_plans.get(decon)?; + let spec = registry.decon_plans().get(decon)?; let params = plan_leaf_params(ext, registry, &spec.leaves)?; let name = format!( "{}Builder", @@ -1325,11 +1328,11 @@ pub(crate) fn builder_iface_spec( /// the element's deconstructor declaration. Named `Folder`, /// placed in the element type's package. pub(crate) fn folder_iface_spec( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, decon: &DeconId, ) -> Option { - let spec = registry.decon_plans.get(decon)?; + let spec = registry.decon_plans().get(decon)?; let mut params: Vec = vec![IfaceParam::same("acc".to_string(), kt::KtType::var_("A"))]; params.extend(plan_leaf_params(ext, registry, &spec.leaves)?); @@ -1351,8 +1354,8 @@ pub(crate) fn folder_iface_spec( /// without a deconstructor — no declaration involved): /// `run(acc: A, element): A`. One shape per element type by construction. pub(crate) fn whole_folder_iface_spec( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, element: &syn::Type, ) -> Option { let mut params: Vec = @@ -1383,12 +1386,12 @@ pub(crate) fn whole_folder_iface_spec( /// The folder spec for an `Iterable` plan: declaration-keyed when the /// element decomposes, whole-element otherwise. Thin KEY dispatch into the -/// [`JniGen::iface_spec`] memo — the fixed-builder typed-group view is +/// [`JniGenBuilder::iface_spec`] memo — the fixed-builder typed-group view is /// applied there per `DeconId` (the declaration identity the JVM resolves /// against), not per this plan's own `fixed_builder` flag. pub(crate) fn folder_iface_for_plan( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, plan: &UnfoldPlan, ) -> Option> { debug_assert!( @@ -1412,11 +1415,11 @@ pub(crate) fn folder_iface_for_plan( /// emission (`write_iface_files`), keyed by the element's deconstructor so the /// two stay in lockstep. pub(crate) fn fixed_folder_typed_groups( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, decon: &DeconId, ) -> Option> { - let spec = registry.decon_plans.get(decon)?; + let spec = registry.decon_plans().get(decon)?; let fqn = ext.kotlin_fqn(&TypeKey::from_type(&spec.source))?; let (reassemble, imports) = fixed_reassembly(ext, registry, &spec.source, &spec.leaves, &fqn); Some(vec![ @@ -1446,11 +1449,11 @@ pub(crate) fn fixed_folder_typed_groups( /// Keyed by the error type's deconstructor declaration. Named /// `Handler`, placed in the error type's package. pub(crate) fn error_handler_iface_spec( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, decon: &DeconId, ) -> Option { - let spec = registry.decon_plans.get(decon)?; + let spec = registry.decon_plans().get(decon)?; let params: Vec = plan_leaf_params(ext, registry, &spec.leaves)?; let name = format!( "{}Handler", @@ -1478,7 +1481,7 @@ pub(crate) fn error_handler_iface_spec( /// The shared infallible handler `JniErrorHandler { run(je: String?): R }` /// — every function without an error plan takes one; placed in the root /// package. -pub(crate) fn jni_error_handler_iface_spec(ext: &JniGen) -> IfaceSpec { +pub(crate) fn jni_error_handler_iface_spec(ext: &JniGenBuilder) -> IfaceSpec { let params = vec![IfaceParam::same( "je".to_string(), kt::KtType::string().nullable(), @@ -1518,16 +1521,16 @@ pub(crate) struct ErrorIfaces { /// The onError handler interfaces for a declared function — the always-present /// binding `JniErrorHandler` plus, for a fallible function, its /// declaration-keyed typed domain `Handler`. Thin KEY dispatch into the -/// [`JniGen::iface_spec`] memo. `None` = the domain handler is underivable +/// [`JniGenBuilder::iface_spec`] memo. `None` = the domain handler is underivable /// (the Rust emitter panics, the Kotlin renderer skips) — the binding channel /// alone always derives. pub(crate) fn onerror_iface_spec( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, fn_ident: &syn::Ident, ) -> Option { let binding = ext.iface_spec(registry, &SpecKey::JniErrorHandler)?; - let domain = match registry.error_plans.get(fn_ident) { + let domain = match registry.error_plans().get(fn_ident) { Some(plan) => { let decon = plan .decon diff --git a/prebindgen/src/api/lang/jnigen/jni/jni_binding_error.rs b/prebindgen/src/api/lang/jnigen/jni/jni_binding_error.rs index e29633a6..8c5d442a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/jni_binding_error.rs +++ b/prebindgen/src/api/lang/jnigen/jni/jni_binding_error.rs @@ -13,7 +13,7 @@ //! The generated wrapper's error callback receives a fixed first `je: String?` //! (the binding message, set only on `JniError`) plus the domain error //! converted/deconstructed into one or more leaves (set only on `UserError`). -//! `JniGen::new()` pre-registers this type so `__JniErr` (= `JniBindingError<()>`) +//! `JniGenBuilder::new()` pre-registers this type so `__JniErr` (= `JniBindingError<()>`) //! is always available to framework converter bodies. /// Framework error type for the JNI binding's error channel. `T` is the diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 3e42be84..8b85c8d9 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -1,6 +1,6 @@ -//! `KotlinExt` impl for [`JniGen`]. +//! `KotlinExt` impl for [`JniGenBuilder`]. //! -//! [`JniGen::write_kotlin`] is the single entry point for every Kotlin +//! [`JniGenBuilder::write_kotlin`] is the single entry point for every Kotlin //! file the JNI back-end emits. Each per-kind emitter builds in-memory //! [`kt::KtFile`] *model fragments* (declarations, not strings — the //! generator module `api::gen::kotlin` owns formatting and imports): @@ -23,18 +23,21 @@ //! Every `#[prebindgen]` function must be assigned a Kotlin home — as a //! class member (`.method`/`.constructor` on a class decl) or a free function //! (`PackageDecl::fun`). Undeclared functions are skipped with a build -//! warning (`Registry::scan_declared`); there is no "orphan" bucket. +//! warning (the generator's unclaimed-item report); there is no "orphan" bucket. use super::*; -use crate::api::gen::{ - kotlin as kt, - kotlin::{ClassKind, Code, KtClass, KtCtorParam, KtFun, KtParam, KtProperty, KtType, Vis}, +use crate::api::{ + core::registry::Conversions, + gen::{ + kotlin as kt, + kotlin::{ClassKind, Code, KtClass, KtCtorParam, KtFun, KtParam, KtProperty, KtType, Vis}, + }, }; /// Declaration of one auto-generated typed `NativeHandle` subclass. /// -/// Consumed by [`JniGen::write_typed_handles`] (and forwarded to -/// [`JniGen::write_jni_wrappers`] so the same promotion list can carve +/// Consumed by [`JniGenBuilder::write_typed_handles`] (and forwarded to +/// [`JniGenBuilder::write_jni_wrappers`] so the same promotion list can carve /// the matching skip-list). Each entry says "this Kotlin class is the /// home for the named `#[prebindgen]` functions"; everything else stays /// in the catch-all `JNIWrappers` object. @@ -51,7 +54,7 @@ pub(crate) struct TypedHandle<'a> { pub key: &'a TypeKey, } -impl crate::api::core::Generation { +impl super::JniGen { /// Unified Kotlin emission — the JNI adapter's second artifact, /// alongside [`write_rust`](Self::write_rust). Each per-kind emitter /// builds in-memory [`kt::KtFile`] model fragments; they are merged @@ -64,20 +67,21 @@ impl crate::api::core::Generation { /// `write_rust`. Returns every path written (one per non-empty /// package). pub fn write_kotlin(&self, kotlin_root: &Path) -> Result, WriteKotlinError> { - self.adapter().write_kotlin(self.registry(), kotlin_root) + self.declarations() + .write_kotlin(self.registry(), kotlin_root) } } -impl JniGen { +impl JniGenBuilder { /// Kotlin emission body — the public entry point is - /// `Generation::::write_kotlin`, which guarantees the registry + /// `JniGen::write_kotlin`, which guarantees the registry /// was resolved first. pub(crate) fn write_kotlin( &self, registry: &Registry, kotlin_root: &Path, ) -> Result, WriteKotlinError> { - // Validation already ran once in `Registry::resolve` — this emitter + // Validation already ran once in `RegistryBuilder::build` — this emitter // is a pure consumer of the resolved, validated registry. let mut fragments: Vec = Vec::new(); fragments.push(self.write_native_handle()); @@ -459,7 +463,7 @@ pub(crate) struct OwnedTypedHandle { pub key: TypeKey, } -impl JniGen { +impl JniGenBuilder { /// Emit one Kotlin `enum class` file per `enum_class`-declared type. /// Variants render in declaration order using SCREAMING_SNAKE_CASE names; the /// constructor stores the Rust discriminant value (or the ordinal as @@ -1050,7 +1054,7 @@ impl JniGen { // A decomposition is a sum's when it carries the synthesized selector. let is_sum = |d: &DeconId| { registry - .decon_plans + .decon_plans() .get(d) .is_some_and(|p| is_sum_leaves(&p.leaves)) }; @@ -1095,7 +1099,7 @@ impl JniGen { } } if let Some(plan) = registry - .unfold_plans + .unfold_plans() .get(&item_fn.sig.ident) .filter(|p| p.delivery == Delivery::Callback) { @@ -1113,7 +1117,7 @@ impl JniGen { _ => {} } } - match registry.error_plans.get(&item_fn.sig.ident) { + match registry.error_plans().get(&item_fn.sig.ident) { Some(ep) => { let d = ep .decon @@ -1131,7 +1135,7 @@ impl JniGen { uses.into_iter() .filter_map(|u| { // Every spec comes from the SAME memo the wrappers and the - // resolve-time trampoline read ([`JniGen::iface_spec`]) — + // resolve-time trampoline read ([`JniGenBuilder::iface_spec`]) — // this site only classifies the extras: `is_error` ⇒ also // emit the zero-alloc capture holder used by the generated // wrappers' error channel; `fixed` carries a @@ -1253,7 +1257,7 @@ impl JniGen { spec: &crate::api::lang::jnigen::jni::IfaceSpec, decon: &crate::api::core::unfold::DeconId, ) -> kt::KtDecl { - let source = ®istry.decon_plans[decon].source; + let source = ®istry.decon_plans()[decon].source; let class_fqn = self .kotlin_fqn(&TypeKey::from_type(source)) .unwrap_or_else(|| { @@ -1296,7 +1300,7 @@ impl JniGen { spec: &crate::api::lang::jnigen::jni::IfaceSpec, decon: &crate::api::core::unfold::DeconId, ) -> kt::KtDecl { - let source = ®istry.decon_plans[decon].source; + let source = ®istry.decon_plans()[decon].source; let class_fqn = self .kotlin_fqn(&TypeKey::from_type(source)) .unwrap_or_else(|| { @@ -1351,7 +1355,7 @@ impl JniGen { spec: &crate::api::lang::jnigen::jni::IfaceSpec, decon: &crate::api::core::unfold::DeconId, ) -> kt::KtDecl { - let plan = ®istry.decon_plans[decon]; + let plan = ®istry.decon_plans()[decon]; let mut imports: BTreeSet = BTreeSet::new(); let names: Vec = spec.params.iter().map(|p| p.name.clone()).collect(); let (iface_short, when) = self.sum_reconstruct( @@ -1390,7 +1394,7 @@ impl JniGen { spec: &crate::api::lang::jnigen::jni::IfaceSpec, decon: &crate::api::core::unfold::DeconId, ) -> kt::KtDecl { - let plan = ®istry.decon_plans[decon]; + let plan = ®istry.decon_plans()[decon]; let mut imports: BTreeSet = BTreeSet::new(); let names: Vec = spec.params.iter().map(|p| p.name.clone()).collect(); let (iface_short, when) = self.sum_reconstruct( @@ -1433,7 +1437,7 @@ impl JniGen { /// its variant-constructor argument by [`Self::sum_ctor_arg`]. pub(crate) fn sum_reconstruct( &self, - registry: &Registry, + registry: &impl Conversions, source: &syn::Type, leaves: &[crate::api::core::unfold::UnfoldLeaf], params: &[crate::api::lang::jnigen::jni::IfaceParam], @@ -1507,7 +1511,7 @@ impl JniGen { /// verbatim. fn sum_ctor_arg( &self, - registry: &Registry, + registry: &impl Conversions, leaf: &crate::api::core::unfold::UnfoldLeaf, param: &crate::api::lang::jnigen::jni::IfaceParam, name: &str, @@ -1691,15 +1695,15 @@ impl JniGen { } /// Emit the centralized Native-object Kotlin file under `output_dir` - /// (class name from [`JniGen::jni_native_class_name`]). Holds one + /// (class name from [`JniGenBuilder::jni_native_class_name`]). Holds one /// `external fun` per `#[prebindgen]` function — names mangled as methods - /// via [`JniGen::set_method_name_mangle`], parameter and return types rendered at + /// via [`JniGenBuilder::set_method_name_mangle`], parameter and return types rendered at /// the JNI **wire** level so the declarations match the Rust extern /// symbols generated under the spec-escaped /// `Java___` (see `symbol`, #86). Every generated native /// call routes through this object, so its static initializer is the /// single point at which native-library loading can be triggered: when - /// [`JniGen::jni_native_init`] is set, its Kotlin statement(s) are emitted + /// [`JniGenBuilder::jni_native_init`] is set, its Kotlin statement(s) are emitted /// inside an `init { … }` block here (e.g. a reference to the consumer's /// own loader object). Unset, the holder stays free of any loading logic /// and the wrapper layer is responsible for loading. @@ -1840,7 +1844,7 @@ impl JniGen { /// same `handles` slice to both methods. /// /// Each handle's `kotlin_fqn` must be registered via - /// [`JniGen::kotlin_fqn`] so the generator can map it back to its + /// [`JniGenBuilder::kotlin_fqn`] so the generator can map it back to its /// Rust type-key (which identifies the first param to drop in each /// promoted method's signature). pub(crate) fn write_typed_handles( diff --git a/prebindgen/src/api/lang/jnigen/jni/metadata.rs b/prebindgen/src/api/lang/jnigen/jni/metadata.rs index 5f249079..30cef3ae 100644 --- a/prebindgen/src/api/lang/jnigen/jni/metadata.rs +++ b/prebindgen/src/api/lang/jnigen/jni/metadata.rs @@ -64,7 +64,7 @@ pub enum ProjectionKind { #[derive(Clone, Debug)] pub struct Projection { /// Canonical key of the leaf type (e.g. `ZKeyExpr`, `ZenohId`); derive - /// the typed Kotlin FQN via `JniGen::kotlin_fqn` — a typed key, so the + /// the typed Kotlin FQN via `JniGenBuilder::kotlin_fqn` — a typed key, so the /// lookup cannot drift from the declaration table's constructor. pub leaf_key: crate::api::core::registry::TypeKey, /// `false` for `&T` borrows of a handle — still a projection (param diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index 5ae4abd0..f4ac4b60 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -1,16 +1,16 @@ //! JNI back-end for the Registry pipeline. //! -//! [`JniGen`] implements [`crate::api::core::prebindgen::Prebindgen`] +//! [`JniGenBuilder`] implements [`crate::api::core::prebindgen::Prebindgen`] //! (Rust-side conversion bodies) and provides an inherent -//! [`JniGen::write_kotlin`] for emitting all Kotlin output +//! [`JniGenBuilder::write_kotlin`] for emitting all Kotlin output //! (`NativeHandle.kt`, typed-handle classes, `JNIWrappers.kt`). //! //! The implementation is split across sibling submodules, all sharing this //! `jni` module's namespace via the `pub(crate) use …::*` glob re-exports //! below (each sibling needs only `use super::*;`): -//! * this file — type / metadata definitions ([`JniGen`], [`KotlinMeta`], +//! * this file — type / metadata definitions ([`JniGenBuilder`], [`KotlinMeta`], //! [`Projection`], [`FoldStrategy`], the config structs) + the shared imports; -//! * `builder` — the [`JniGen`] builder API; +//! * `builder` — the [`JniGenBuilder`] builder API; //! * `trait_impl` — the [`Prebindgen`] impl + its converter-selector helpers; //! * `emit` — Rust-side `extern "C"` wrapper / converter-body emission; //! * `prim` — JNI primitive (un)boxing tables; @@ -152,7 +152,7 @@ impl FunctionEntry { /// /// Adding a sixth class kind is one variant here plus its emitter: there is /// no flag to add and no precedence chain to extend, because every consumer -/// reads this one field (via [`TypeConfig`]'s accessors, [`JniGen::type_kind`], +/// reads this one field (via [`TypeConfig`]'s accessors, [`JniGenBuilder::type_kind`], /// or a direct match). #[derive(Clone)] pub(crate) enum DeclaredKind { @@ -180,9 +180,9 @@ pub(crate) enum DeclaredKind { #[derive(Clone)] pub(crate) struct TypeConfig { /// The class declarator this type is registered under, carrying that - /// kind's own options. Every entry in [`JniGen::types`] has one: entries + /// kind's own options. Every entry in [`JniGenBuilder::types`] has one: entries /// are created only by a class declarator (see - /// `JniGen::register_class`), which is why presence in the table *is* + /// `JniGenBuilder::register_class`), which is why presence in the table *is* /// "declared as a class" — declared classes are required in **both** /// directions at scan (their converters always resolve both ways), /// unlike a wrapper registration, which is required per **usage** @@ -192,7 +192,7 @@ pub(crate) struct TypeConfig { /// settings-derived class name. Required for any type emitted in /// Kotlin; the concrete FQN (`Sample` → `"io.zenoh.jni.Sample"`, /// `Vec` → `"ByteArray"`) is materialized only at read time via - /// [`JniGen::fqn_of`], which is what makes the `set_*` settings + /// [`JniGenBuilder::fqn_of`], which is what makes the `set_*` settings /// order-independent w.r.t. declarations. pub name_spec: Option, /// Explicit opt-in for a `data_class` to cross Kotlin → Rust as one @@ -261,7 +261,7 @@ impl TypeConfig { #[derive(Clone, Default)] pub(crate) struct PackageConfig { /// `#[prebindgen]` fns declared as free-standing wrappers under this - /// subpackage via [`JniGen::fun`]. + /// subpackage via [`JniGenBuilder::fun`]. pub functions: Vec, /// `#[prebindgen]` consts declared under this subpackage via /// [`PackageDecl::constant`] — each surfaces as a top-level Kotlin `val` @@ -309,7 +309,7 @@ pub(crate) struct ClassMember { /// Rust function ident (`registry.flat().function(ident)`). pub rust_ident: syn::Ident, /// Per-member `.name()` override, stored RAW — the effective Kotlin - /// name is derived at point of use by [`JniGen::class_method_kotlin_name`] + /// name is derived at point of use by [`JniGenBuilder::class_method_kotlin_name`] /// (override, else the package/class-aware method hook over the full /// camelCase ident), keeping `set_method_name_mangle` order-independent. An /// `expand_return!` `.field` referencing the same underlying function @@ -319,39 +319,8 @@ pub(crate) struct ClassMember { /// Member kind (method / constructor). pub kind: MemberKind, } - -/// Boxed closure that builds a converter when applied to the wildcard -/// substitutions. Returns `None` to defer (an inner converter the -/// builder depends on isn't yet resolved; the resolver retries on the -/// next phase), or `Some((ty, exc, body))` where: -/// -/// * `ty` — the type the body produces. Auto-classified at lookup: -/// a wire shape (or the self-converter case) ⇒ terminal converter -/// with `destination = ty`; a rust type with its own converter ⇒ -/// composed as a value-inspecting stage onto that converter's chain. -/// * `exc` — the bound domain error **as a Rust type**: the `E` peeled -/// from a source `Result`, matched by exact canonical-form -/// equality (use the same full path the source signature uses, e.g. -/// `parse_quote!(zenoh_flat::errors::ZError)` — no short-name -/// matching). `Some(...)` ⇒ domain-fallible: the body evaluates to -/// `Result` and is emitted as-is; a failure routes to the -/// wrapper's error sink (never a JVM throw). `None` ⇒ binding-fallible -/// only: the body evaluates to a bare `ty` and the framework wraps it -/// `Ok(body)` with `Result` (= `JniBindingError`). -/// * `body` — the closure body. The decision between Ok-wrap vs -/// verbatim is keyed on `exc` (see [`JniGen::build_input_fn`] / -/// [`JniGen::build_output_fn`]). -/// -/// Receives `&Registry` so the closure can look up -/// inner-type entries (`registry.output_entry(t)`). -pub(crate) type WrapperFn = Arc< - dyn Fn(&[syn::Type], &Registry) -> Option<(syn::Type, Option, syn::Expr)> - + Send - + Sync, ->; - /// Closure that transforms a Kotlin short name with the fully-qualified -/// package in which the named object is emitted. Installed via [`JniGen`]'s +/// package in which the named object is emitted. Installed via [`JniGenBuilder`]'s /// per-kind `set_*_name_mangle` setters. Closure-unset = identity. pub(crate) type NameMangle = Arc String + Send + Sync>; @@ -371,12 +340,12 @@ pub(crate) type MethodNameMangle = Arc String + Send /// `set_*` methods; declarations are accepted as pre-built objects /// (`PackageDecl`, `ExpandParamDecl`, `ExpandReturnDecl`, /// `ConvertDecl` — see `decl.rs`) built -/// independently of `JniGen` itself; there is no fluent typestate cursor. +/// independently of `JniGenBuilder` itself; there is no fluent typestate cursor. /// /// ``` -/// use prebindgen::lang::JniGen; +/// use prebindgen::lang::JniGenBuilder; /// -/// let jni = JniGen::new() +/// let jni = JniGenBuilder::new() /// .set_package_prefix("io.test.jni") /// .package( /// prebindgen::package!("keyexpr") @@ -393,15 +362,71 @@ pub(crate) type MethodNameMangle = Arc String + Send /// ) /// .expand(prebindgen::expand_return!(KeyExpr).field(prebindgen::fun!(keyexpr_get_str))); /// ``` -#[derive(Clone)] +/// A resolved JNI binding: every crossing has a conversion, and the artifacts +/// can be written. +/// +/// Built by [`JniGenBuilder::build`]. Read-only — the registry inside it is +/// complete, which is what lets every `write_*` be a pure emission that can run +/// in any order, or not at all. pub struct JniGen { + /// What the binding declared. The emitters read it for names, classes and + /// decompositions. + pub(crate) gen: JniGenBuilder, + /// Every crossing this binding needs, each with its conversion. + pub(crate) registry: crate::core::Registry, +} + +// Opaque — exists so `Result::expect_err` works in tests. +impl std::fmt::Debug for JniGen { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("JniGen(..)") + } +} + +impl JniGen { + /// Describe a JNI binding. + /// + /// The entry point: everything a binding states — its Kotlin surface, its + /// decompositions, and where its `#[prebindgen]` source lives — goes on the + /// builder, and [`JniGenBuilder::build`] turns it into a [`JniGen`]. + pub fn builder() -> JniGenBuilder { + JniGenBuilder::new() + } + + /// Write the generated Rust file — the JNI externs and the converters they + /// call. `out_path` may be relative (resolved against `OUT_DIR`) or + /// absolute; returns the path actually written. + pub fn write_rust( + &self, + out_path: impl AsRef, + ) -> Result { + Ok(crate::api::core::write::write_rust( + &self.registry, + &self.gen, + out_path, + )?) + } + + /// The resolved registry — conversions, decompositions, and the model. + pub fn registry(&self) -> &crate::core::Registry { + &self.registry + } + + /// What the binding declared. + pub fn declarations(&self) -> &JniGenBuilder { + &self.gen + } +} + +#[derive(Clone)] +pub struct JniGenBuilder { /// Single source of truth for the JVM/Kotlin namespace this binding /// targets, dot-separated (e.g. `io.zenoh.jni`). Empty = no prefix. /// Every derived form — slash-separated for `FindClass` - /// (`JniGen::java_class_prefix()`), `_`-mangled for JNI extern idents - /// (`JniGen::jni_class_path()`), dot-separated for Kotlin `package` + /// (`JniGenBuilder::java_class_prefix()`), `_`-mangled for JNI extern idents + /// (`JniGenBuilder::jni_class_path()`), dot-separated for Kotlin `package` /// declarations — is computed from this at the point of use. - /// `pub(crate)`: consumers go through [`JniGen::set_package_prefix`], + /// `pub(crate)`: consumers go through [`JniGenBuilder::set_package_prefix`], /// whose trimming a direct field write would bypass. pub(crate) package: String, @@ -417,7 +442,7 @@ pub struct JniGen { /// Mangler for `EnumClassDecl`-declared C-like enum class /// names. Default = identity. pub(crate) enum_name_mangle: Option, - /// Method-name mangle hook ([`JniGen::set_method_name_mangle`]) — applied + /// Method-name mangle hook ([`JniGenBuilder::set_method_name_mangle`]) — applied /// to the camelCase Rust function name of every class method/factory /// without a per-method `.name()`, with package and class context. pub(crate) method_name_mangle: Option, @@ -432,38 +457,26 @@ pub struct JniGen { /// Structured per-type configuration keyed by canonical Rust type. /// One entry per declared class; populated when accepting a `ClassDecl`, - /// through the table's single writer `JniGen::register_class` — so + /// through the table's single writer `JniGenBuilder::register_class` — so /// presence here *is* "declared as a class", and each entry's /// [`TypeConfig::kind`] is the one representation of which declarator it /// came from. Also holds the raw [`NameSpec`] (Kotlin FQNs are - /// derived from it on read via [`JniGen::kotlin_fqn`] / - /// [`JniGen::fqn_of`]); the converter bodies themselves live in - /// [`Self::input_wrappers`] / [`Self::output_wrappers`]. The rank-0 - /// dispatch order is opaque → enum → wrapper-table → primitive → struct. + /// derived from it on read via [`JniGenBuilder::kotlin_fqn`] / + /// [`JniGenBuilder::fqn_of`]). Terminal dispatch order is opaque → enum → + /// `convert!` → primitive → struct; see + /// [`JniGenBuilder::select_input_type`](crate::lang::JniGenBuilder)'s selector. pub(crate) types: HashMap, /// Free-standing package-level wrappers, keyed by subpackage path /// (relative to [`Self::package`], dot-separated; the empty key is the - /// base package itself). Populated by [`JniGen::package`], merging into + /// base package itself). Populated by [`JniGenBuilder::package`], merging into /// whatever the named subpackage already holds. pub(crate) packages: BTreeMap, - /// Per-rank input converters — index `n` holds rank-`n` registrations - /// keyed by the pattern's `TypeKey`. Rank 0 is non-wildcard (e.g. - /// `"i32"`); ranks 1..3 carry that many `_` slots (e.g. `"Vec < _ >"`). - /// Each [`WrapperFn`] closure carries the builder body AND the bound - /// exception (the closure returns `(ty, exc, body)`); terminal vs - /// composed is derived at lookup time, throwing vs non-throwing - /// from the closure's `Option` middle slot. - pub(crate) input_wrappers: [HashMap; 4], - - /// Per-rank output converters. Same shape as [`Self::input_wrappers`]. - pub(crate) output_wrappers: [HashMap; 4], - /// Canonical single-value conversions ([`ConvertDecl`], accepted by - /// [`JniGen::convert`]), stored raw — the rank-0 converter bodies derive + /// [`JniGenBuilder::convert`]), stored raw — the rank-0 converter bodies derive /// from the conversion fns' registry signatures at lookup time - /// ([`JniGen::convert_input_body`] / [`JniGen::convert_output_body`]), + /// ([`JniGenBuilder::convert_input_body`] / [`JniGenBuilder::convert_output_body`]), /// keeping declarations order-independent and origin-qualified. pub(crate) convert_decls: Vec, @@ -472,12 +485,12 @@ pub struct JniGen { /// scaffold (deadlock-safe N-ary monitor acquisition + atomic /// consume). When `false`, the scaffold is omitted — wrappers emit /// only the raw `ptr` read + closed-handle null-check + native call. - /// Toggled via [`JniGen::set_emit_handle_locks`]. + /// Toggled via [`JniGenBuilder::set_emit_handle_locks`]. pub(crate) emit_handle_locks: bool, /// Optional Kotlin statement(s) to place inside an `init { … }` block of /// the generated centralized externs object (`JNINative`). Set via - /// [`JniGen::set_jni_native_init`]. Every generated native call routes + /// [`JniGenBuilder::set_jni_native_init`]. Every generated native call routes /// through that object, so its `` is the single point at which a /// consumer can trigger native-library loading (e.g. /// `"io.zenoh.jni.NativeLibrary.ensureLoaded()"`). `None` (default) emits no @@ -485,12 +498,12 @@ pub struct JniGen { pub(crate) jni_native_init: Option, /// Type-level default input boundaries ([`ExpandParamDecl`], accepted by - /// [`JniGen::expand`]), stored raw — merged into the expansion set + /// [`JniGenBuilder::expand`]), stored raw — merged into the expansion set /// at the point of use so declarations stay order-independent. pub(crate) param_expand_decls: Vec, /// Type-level default output boundaries ([`ExpandReturnDecl`], accepted - /// by [`JniGen::expand`]), stored raw — field names (member + /// by [`JniGenBuilder::expand`]), stored raw — field names (member /// inheritance) resolve at the point of use so declarations stay /// order-independent. pub(crate) return_expand_decls: Vec, @@ -519,23 +532,23 @@ pub struct JniGen { pub(crate) class_members: HashMap>, /// `#[prebindgen]` fns the binding deliberately does NOT wrap, declared - /// via [`JniGen::ignore`]. Backs [`Prebindgen::ignored_functions`]: + /// via [`JniGenBuilder::ignore`]. Backs [`Prebindgen::ignored_functions`]: /// suppresses the registry's per-item "skipping undeclared" warning /// without emitting anything. pub(crate) ignored_fns: std::collections::HashSet, - /// Bulk name-family ignore predicates, declared via [`JniGen::ignore`] + + /// Bulk name-family ignore predicates, declared via [`JniGenBuilder::ignore`] + /// [`matching`](crate::lang::matching). Backs /// [`Prebindgen::ignored_name_predicates`]: every undeclared item /// (fn/type/const) whose name matches is an acknowledged skip. pub(crate) ignored_name_predicates: Vec, /// `#[prebindgen]` types the binding deliberately does NOT declare, - /// via [`JniGen::ignore`]. Backs [`Prebindgen::ignored_types`]. + /// via [`JniGenBuilder::ignore`]. Backs [`Prebindgen::ignored_types`]. pub(crate) ignored_class_types: std::collections::HashSet, /// `#[prebindgen]` consts the binding deliberately does NOT declare, - /// via [`JniGen::ignore_const`]. Backs [`Prebindgen::ignored_consts`]. + /// via [`JniGenBuilder::ignore_const`]. Backs [`Prebindgen::ignored_consts`]. pub(crate) ignored_const_idents: std::collections::HashSet, /// Binding-local fns declared via path-built [`fun!`](crate::fun) + /// [`FunctionDecl::sig`]: `(fn ident = path last segment, declared path, @@ -544,7 +557,7 @@ pub struct JniGen { pub(crate) local_fns: Vec<(syn::Ident, syn::Path, syn::Signature)>, /// Memoized callback-interface specs, one per [`SpecKey`] identity — - /// populated lazily via [`JniGen::iface_spec`] (first touch may be the + /// populated lazily via [`JniGenBuilder::iface_spec`] (first touch may be the /// resolve-time trampoline, which runs before any function plan exists) /// and shared by every later consumer, so the FQN/descriptor pair cannot /// drift between the Rust, Kotlin-wrapper, and interface-declaration @@ -564,6 +577,14 @@ pub struct JniGen { /// "derived state, keyed by `(self, registry)`" contract as /// [`Self::iface_specs`]. pub(crate) fn_plans: std::cell::RefCell>>, + + /// Where the `#[prebindgen]` items come from. + /// + /// A [`FlatBuilder`](crate::core::flat::FlatBuilder), stated with the same + /// three feeders it has — so a build script says where the source is in the + /// vocabulary the model already uses, and never names a `Flat` or a + /// `Registry` itself. + pub(crate) sources: crate::api::core::flat::FlatBuilder, } // ── Sibling submodules (carved from the former monolithic file) ───────── diff --git a/prebindgen/src/api/lang/jnigen/jni/overloads.rs b/prebindgen/src/api/lang/jnigen/jni/overloads.rs index 11e94523..475a2229 100644 --- a/prebindgen/src/api/lang/jnigen/jni/overloads.rs +++ b/prebindgen/src/api/lang/jnigen/jni/overloads.rs @@ -4,7 +4,7 @@ //! (`expectedSel: Int, expected00: Long?, …`); the raw call site passes magic //! ints and null-padding. Two mechanisms turn that into idiomatic Kotlin: //! -//! * **Proactive splittability check** ([`JniGen::validate_split_declarations`]): +//! * **Proactive splittability check** ([`JniGenBuilder::validate_split_declarations`]): //! every multi-variant `expand_param!` declaration (type-level or per-fn) is //! verified up front to be *splittable* — its arms surface as pairwise-distinct //! JVM signatures — so a function can safely request overloads. A collision is @@ -30,9 +30,12 @@ //! still pairwise-distinct per the checks above. use super::*; -use crate::api::core::expand::{FoldArg, FoldPlan}; +use crate::api::core::{ + expand::{FoldArg, FoldPlan}, + registry::Conversions, +}; -impl JniGen { +impl JniGenBuilder { /// Proactively verify every multi-variant `expand_param!` declaration is /// splittable (its arms have pairwise-distinct JVM-erased signatures), so /// [`FunctionDecl::split_on_param`](crate::fun) can emit unambiguous @@ -105,7 +108,7 @@ impl JniGen { /// Uses the shared [`erase_kt_type`] model (issue #89 stage 2) so the split /// ambiguity check and the whole-artifact overload table agree on erasure. fn arm_erased_sig( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, target: &syn::Type, ctor: Option<&syn::Ident>, @@ -138,7 +141,7 @@ fn arm_erased_sig( /// type with no resolved surface. References are peeled first (`&T` erases /// like `T`). fn rust_type_erased( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, ty: &syn::Type, ) -> ErasedJvmType { @@ -245,7 +248,7 @@ fn is_option(ty: &syn::Type) -> bool { /// for an `Option<…>` parameter `null` encodes absence (nullable-arm rule). /// Returns `None` if any input is not a flat leaf. fn variant_typed_params( - registry: &Registry, + registry: &impl Conversions, variant: &crate::api::core::expand::FoldVariant, origin: &syn::Ident, block: &[kt::KtParam], @@ -322,7 +325,7 @@ fn resolve_split<'a>( ) -> Split<'a> { let param = syn::Ident::new(param_name, Span::call_site()); let plan = registry - .expansion_plans + .expansion_plans() .get(&(f.sig.ident.clone(), param.clone())) .unwrap_or_else(|| { panic!( @@ -402,7 +405,7 @@ fn resolve_split<'a>( /// Emits the cartesian product of the named params' arms; panics (a build /// error) if the product has two combinations with the same JVM signature. pub(crate) fn render_param_overloads( - ext: &JniGen, + ext: &JniGenBuilder, f: &syn::ItemFn, registry: &Registry, sel_fun: &kt::KtFun, @@ -607,10 +610,9 @@ mod tests { } }; let registry = - Registry::::from_items(crate::api::test_util::declare_referenced(vec![( - syn::Item::Fn(ctor), - SourceLocation::default(), - )])) + crate::api::test_util::reg_from_items(crate::api::test_util::declare_referenced(vec![ + (syn::Item::Fn(ctor), SourceLocation::default()), + ])) .expect("index constructor"); let variant = crate::api::core::expand::FoldVariant { ctor: Some(syn::parse_quote!(z_summary_optional)), diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index 65ac6ca5..ae2e50ff 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -5,6 +5,7 @@ //! via `use super::*`. use super::*; +use crate::api::core::registry::Conversions; // ── Safe-wrapper emitters ────────────────────────────────────────────── @@ -66,7 +67,7 @@ pub(crate) fn build_enum_class(class_name: &str, item_enum: &syn::ItemEnum) -> k /// them), and the `fromParts` factory's raw-text class references carry their /// imports on the factory body `Code`. pub(crate) fn build_data_class( - ext: &JniGen, + ext: &JniGenBuilder, class_name: &str, item_struct: &syn::ItemStruct, registry: &Registry, @@ -253,7 +254,7 @@ pub(crate) fn build_data_class( /// to the matching `Java___` /// extern on the Rust side (the auto-generated destructor). pub(crate) fn build_typed_handle( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, class_name: &str, rust_doc_name: &str, @@ -463,7 +464,7 @@ pub(crate) fn is_iterable_fold(shape: &crate::api::core::unfold::UnfoldShape) -> /// `kt_return` (Unit is no return type). `None` if a param's converter isn't /// resolved. Full-FQN types throughout — no derivation-time shortening. pub(crate) fn render_extern_decl( - ext: &JniGen, + ext: &JniGenBuilder, f: &syn::ItemFn, registry: &Registry, ) -> Option { @@ -549,7 +550,7 @@ pub(crate) fn render_extern_decl( // erased to `Any` (JObject) on the wire; the wrapper passes a capture for // each. A domain plan ⇒ `error_plans` has this fn. params.push(kt::KtParam::new("errorSink", kt::KtType::any())); - if registry.error_plans.contains_key(&f.sig.ident) { + if registry.error_plans().contains_key(&f.sig.ident) { params.push(kt::KtParam::new("domainSink", kt::KtType::any())); } @@ -751,7 +752,7 @@ pub(crate) struct WrapperSurface { /// import set. Validation calls this directly and skips the body work /// (`build_native_call` / `render_body` / KDoc / opaque-lock collection). pub(crate) fn build_wrapper_surface( - ext: &JniGen, + ext: &JniGenBuilder, f: &syn::ItemFn, registry: &Registry, kotlin_name_override: Option<&str>, @@ -830,7 +831,7 @@ pub(crate) fn build_wrapper_surface( } pub(crate) fn render_wrapper_fn( - ext: &JniGen, + ext: &JniGenBuilder, f: &syn::ItemFn, registry: &Registry, kotlin_name_override: Option<&str>, @@ -887,7 +888,7 @@ pub(crate) fn render_wrapper_fn( /// the ordinary output machinery — plus the public lazily-initialized `val` /// that calls it once, on first use (see [`render_val_over_helper`]). pub(crate) fn render_const_val( - ext: &JniGen, + ext: &JniGenBuilder, package: &str, c: &syn::ItemConst, registry: &Registry, @@ -918,7 +919,7 @@ pub(crate) fn render_const_val( /// computed once, on first use, through the ordinary generated wrapper /// (one JNI call, exactly like a const getter). pub(crate) fn render_constant_fn_val( - ext: &JniGen, + ext: &JniGenBuilder, package: &str, f: &syn::ItemFn, registry: &Registry, @@ -948,7 +949,7 @@ pub(crate) fn render_constant_fn_val( /// is the binding-defined expression, evaluated once, on first use, through /// the generated getter. pub(crate) fn render_const_expr_val( - ext: &JniGen, + ext: &JniGenBuilder, package: &str, decl: &crate::api::lang::jnigen::jni::decl::ConstExprDecl, registry: &Registry, @@ -981,7 +982,7 @@ pub(crate) fn render_const_expr_val( /// `error(...)` at first use). Lazy, not eager: a consts-heavy package must /// not fire one JNI call per `val` at class-load (issue #58). fn render_val_over_helper( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, mut helper: kt::KtFun, val_name: String, @@ -1071,7 +1072,7 @@ struct DomainSink { /// instance-method receiver (the first param whose peeled type matches /// `receiver_key`), which is bound to `this` and dropped from the signature. fn classify_params( - ext: &JniGen, + ext: &JniGenBuilder, fplan: &JniFunctionPlan, registry: &Registry, imports: &mut BTreeSet, @@ -1261,13 +1262,13 @@ fn classify_params( /// with no such projection ⇒ the callback is passed directly (M1–M4 /// unchanged). fn classify_output( - ext: &JniGen, + ext: &JniGenBuilder, f: &syn::ItemFn, fplan: &JniFunctionPlan, registry: &Registry, imports: &mut BTreeSet, ) -> Option { - let unfold = registry.unfold_plans.get(&f.sig.ident); + let unfold = registry.unfold_plans().get(&f.sig.ident); // `builder_param` is the trailing **lambda** param (build / fold) as a // `(name, function-type)` pair. For the `Iterable` shape, the non-lambda // accumulator (`acc: A`) goes in `builder_lead` — it must precede @@ -1433,7 +1434,7 @@ fn classify_output( /// deliberately deferred to [`build_success_return`], after the native error /// captures have been checked. fn build_native_call( - ext: &JniGen, + ext: &JniGenBuilder, jni_call: &str, params: &[Param], out: &OutputPlan, @@ -1529,7 +1530,7 @@ fn build_native_call( /// This expression is emitted only after binding/domain captures have been /// checked, so a native failure placeholder can never reach an enum lookup, /// value projection, or erased-result cast. -fn build_success_return(ext: &JniGen, out: &OutputPlan, raw: &str) -> String { +fn build_success_return(ext: &JniGenBuilder, out: &OutputPlan, raw: &str) -> String { if let Some(p) = &out.projection { // Fold the wrap through the projection strategy. The wrap class is // the projection leaf's typed short name (a Handle's typed-handle @@ -1627,7 +1628,7 @@ fn error_sink_parts( // plan; when present, both the interface spec and the error plan are. let domain = if let Some(domain_spec) = &ifaces.domain { let error_plan = registry - .error_plans + .error_plans() .get(&f.sig.ident) .expect("domain handler ⇒ error plan"); // Per ze leaf: (raw capture Kotlin type, raw→typed wrap). The CAPTURE @@ -1806,7 +1807,7 @@ fn render_value_stmt(bind: &str, body_expr: &str, opaques: &[Opaque]) -> kt::Cod /// statements are needed) so the caller can bind it to `__ret`, rethrow a /// captured sink error, then return. fn render_core_stmt( - ext: &JniGen, + ext: &JniGenBuilder, opaques: &[Opaque], body_expr: &str, imports: &mut BTreeSet, @@ -1911,7 +1912,7 @@ enum BodyReturn { } fn render_body( - ext: &JniGen, + ext: &JniGenBuilder, params: &[Param], opaques: &[Opaque], sink: &ErrorSink, @@ -2006,8 +2007,8 @@ fn render_body( /// value projection that can't be built Rust-side). /// Shared by the unfold builder/fold lambda and the callback lambda params. pub(crate) fn unfold_leaf_kt( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, out_ty: &syn::Type, nullable: bool, pk: &str, @@ -2144,9 +2145,9 @@ pub(crate) fn kotlin_for_wire(wire: &syn::Type) -> Option { /// and pick the JNI extern's wire return (`Long` for `Handle`). `None` for /// plain non-projection returns. pub(crate) fn classify_return( - ext: &JniGen, + ext: &JniGenBuilder, output: &syn::ReturnType, - registry: &Registry, + registry: &impl Conversions, ) -> Option<( Option, Option, @@ -2248,7 +2249,7 @@ fn shape_notes(f: &syn::ItemFn, registry: &Registry) -> Option = Vec::new(); let mut plans: Vec<(&syn::Ident, &crate::api::core::expand::FoldPlan)> = registry - .expansion_plans + .expansion_plans() .iter() .filter(|((func, _), _)| func == fn_ident) .map(|((_, param), plan)| (param, plan)) @@ -2291,7 +2292,7 @@ fn shape_notes(f: &syn::ItemFn, registry: &Registry) -> Option = plan.leaves.iter().map(|l| l.name.as_str()).collect(); match plan.delivery { @@ -2311,7 +2312,7 @@ fn shape_notes(f: &syn::ItemFn, registry: &Registry) -> Option = plan.leaves.iter().map(|l| l.name.as_str()).collect(); notes.push(format!( diff --git a/prebindgen/src/api/lang/jnigen/jni/report.rs b/prebindgen/src/api/lang/jnigen/jni/report.rs index 7b42148c..aa044ef6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/report.rs +++ b/prebindgen/src/api/lang/jnigen/jni/report.rs @@ -1,4 +1,4 @@ -//! `Generation::::report()` — the resolved binding surface, +//! `JniGen::report()` — the resolved binding surface, //! explained. //! //! Declarations act at a distance: one `expand_return!` / `convert!` / @@ -11,22 +11,23 @@ //! committed regen so a decl's effect is reviewable in a PR without //! reading generated Kotlin. //! -//! The report is deliberately an inherent method of `Generation` +//! The report is deliberately an inherent method of `JniGen` //! (the `write_kotlin` seam): the *pattern* — describe your resolved //! surface — is adapter-universal, but the *content* is intrinsically in //! the destination language's vocabulary, so each adapter implements its //! own. use super::*; +use crate::api::core::registry::Conversions; -impl crate::api::core::Generation { +impl super::JniGen { /// Render the resolved binding surface as a deterministic markdown /// report: per package / class the final Kotlin signature of every /// wrapper (exactly as generated) with the expand/error plans that /// shaped it, then the type table (kind, Kotlin FQN, wire, conversion /// sources). Pure read over the resolved registry. pub fn report(&self) -> String { - let ext = self.adapter(); + let ext = self.declarations(); let registry = self.registry(); let mut out = String::new(); out.push_str("# JniGen binding report\n\n"); @@ -186,7 +187,7 @@ impl crate::api::core::Generation { kotlin_name: Option<&str>, receiver_key: Option<&TypeKey>, ) { - let ext = self.adapter(); + let ext = self.declarations(); let registry = self.registry(); let Some(item_fn) = registry .flat() @@ -203,7 +204,7 @@ impl crate::api::core::Generation { // Param expansions. let mut shaped: Vec = Vec::new(); let mut plans: Vec<(&syn::Ident, &crate::api::core::expand::FoldPlan)> = registry - .expansion_plans + .expansion_plans() .iter() .filter(|((func, _), _)| func == rust_ident) .map(|((_, param), plan)| (param, plan)) @@ -224,7 +225,7 @@ impl crate::api::core::Generation { variants.join(", ") )); } - if let Some(plan) = registry.unfold_plans.get(rust_ident) { + if let Some(plan) = registry.unfold_plans().get(rust_ident) { let leaves: Vec<&str> = plan.leaves.iter().map(|l| l.name.as_str()).collect(); shaped.push(format!( "return `{}` decomposed → [{}] ({:?} delivery)", @@ -233,7 +234,7 @@ impl crate::api::core::Generation { plan.delivery )); } - if let Some(plan) = registry.error_plans.get(rust_ident) { + if let Some(plan) = registry.error_plans().get(rust_ident) { let leaves: Vec<&str> = plan.leaves.iter().map(|l| l.name.as_str()).collect(); shaped.push(format!( "domain error `{}` decomposed → onError [{}] (binding failures → onBindingError)", @@ -247,7 +248,7 @@ impl crate::api::core::Generation { } } -impl JniGen { +impl JniGenBuilder { /// Human-readable class-kind name of a declared type (report use). pub(crate) fn class_kind_name(&self, key: &TypeKey) -> &'static str { let Some(cfg) = self.types.get(key) else { diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index e137bdcd..069f0e60 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -1,12 +1,12 @@ -//! Structural converter-selection policy for [`JniGen`]. +//! Structural converter-selection policy for [`JniGenBuilder`]. use super::*; +use crate::api::core::registry::Conversions; /// Clone a single-type-arg generic (`Option` / `Vec` / any `Path`) /// replacing its last segment's first type argument with `repl` — yielding the -/// canonical wildcard pattern (`Option<_>`) the rank-1 handlers `pat_match`, -/// with the type's own path/qualification preserved exactly as the enumerator -/// would have produced it. +/// canonical shape (`Option<_>`) the built-in wrapper handlers key on, with the +/// type's own path/qualification preserved exactly. fn with_first_arg(ty: &syn::Type, repl: syn::Type) -> syn::Type { let mut out = ty.clone(); if let syn::Type::Path(tp) = &mut out { @@ -33,22 +33,18 @@ fn ref_wildcard(r: &syn::TypeReference) -> syn::Type { syn::Type::Reference(pr) } -impl JniGen { +impl JniGenBuilder { /// Select the input converter for `ty`: terminals, user wrappers, then /// built-in structural wrappers. pub(crate) fn select_input_type( &self, ty: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { // 1. Terminal categories (incl. the terminal user-wrapper lookup). if let Some(c) = self.input_terminal(ty, registry) { return Some(c); } - // 2. Higher-arity user-registered input patterns (any depth). - if let Some(c) = self.match_user_input(ty, registry) { - return Some(c); - } // 3. Built-in wrapper shapes. `Option<&T>` tries the DEEP `Option<&_>` // (borrowed-handle → `Option>`) before the shallow // `Option<_>`; the shape that resolves correctly wins. @@ -110,17 +106,20 @@ impl JniGen { pub(crate) fn select_output_type( &self, ty: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { // 1. Terminal categories (incl. the terminal user-wrapper lookup). if let Some(c) = self.output_terminal(ty, registry) { return Some(c); } - // 2. User-registered patterns, specificity-ordered — the built-in - // `Result<_, _>` peel and any consumer override (`Result<_, - // ConcreteErr>` wins over the catch-all). Any depth. - if let Some(c) = self.match_user_output(ty, registry) { - return Some(c); + // 2. `Result`: succeeds as `T`, routes `E` to the error sink. + // Read off the model, which calls this shape `TypeKind::Fallible`. + // `result_parts` covers a `Result` the adapter composed itself, which + // the frontend never read. + if let Some((ok, err)) = fallible_parts(ty, registry) { + if let Some(c) = self.result_peel(ty, &ok, &err, registry) { + return Some(c); + } } // 3. Built-in wrapper shapes (`Option<_>`, `Vec<_>`, `&T` borrow). An // `Option<&Handle>` resolves via the shallow `Option<_>` whose inner @@ -160,3 +159,26 @@ impl JniGen { None } } + +/// The `Ok`/`Err` of a `Result`, preferring the frontend's reading. +/// +/// The model classifies a `Result` as [`TypeKind::Fallible`]; the syntactic +/// fallback is for a `Result` the adapter composed itself, which no captured +/// item spells and the frontend therefore never read. +/// +/// **Measured: the fallback never fires in-tree** — zero occurrences across +/// covertest-kotlin and perftest-kotlin, because #246 indexes a binding-local +/// fn's types, so even a `sig!((..) -> Result)` has a reading. +/// It is kept rather than made a hard error because an out-of-tree consumer may +/// compose a `Result` the model never sees, and it costs nothing: `result_parts` +/// already exists and already has six other callers. +fn fallible_parts( + ty: &syn::Type, + registry: &impl Conversions, +) -> Option<(syn::Type, syn::Type)> { + use crate::api::core::flat::TypeKind; + if let Some(TypeKind::Fallible { ok, err }) = registry.flat().type_ref(ty).map(|t| &t.kind) { + return Some((ok.origin.syntax.clone(), err.origin.syntax.clone())); + } + crate::api::core::types_util::result_parts(ty) +} diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index ce8694f1..5c24a21e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -16,6 +16,7 @@ //! instead of by hand-synchronized parallel walks. use super::*; +use crate::api::core::registry::Conversions; /// The flattened `fromParts` bridge plan of one struct. pub(crate) struct StructPlan { @@ -182,8 +183,8 @@ pub(crate) struct SumPlanField { /// name) — consistently for BOTH sides, where the former parallel walks /// could silently diverge on such edge cases. pub(crate) fn build_struct_plan( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, s: &syn::ItemStruct, depth: usize, ) -> Option { @@ -214,8 +215,8 @@ pub(crate) fn build_struct_plan( /// `owner` is the dotted path used in diagnostics (`Config.mode`, /// `Reading::Exact.v0`). pub(crate) fn classify_field( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, ty: &syn::Type, owner: &str, depth: usize, @@ -463,8 +464,8 @@ impl PlanFieldKind { /// whenever a payload's converter happened to resolve later than this plan /// was first attempted. fn sum_plan_kind( - ext: &JniGen, - registry: &Registry, + ext: &JniGenBuilder, + registry: &impl Conversions, ty: &syn::Type, owner: &str, optional: bool, diff --git a/prebindgen/src/api/lang/jnigen/jni/symbol.rs b/prebindgen/src/api/lang/jnigen/jni/symbol.rs index b4b9921c..213c1fc3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/symbol.rs +++ b/prebindgen/src/api/lang/jnigen/jni/symbol.rs @@ -69,7 +69,7 @@ pub(crate) fn native_symbol(package: &str, class: &str, method: &str) -> String /// The long native symbol for **overloaded** natives: the short name plus /// `__` and the escaped argument signature (the descriptor between `(` and /// `)`, e.g. `ILjava/lang/String;` — `/`→`_`, `;`→`_2`, `[`→`_3`). Nothing -/// JniGen emits today is overloaded at the extern level (every `JNINative` +/// JniGenBuilder emits today is overloaded at the extern level (every `JNINative` /// method is uniquely named), so this is provided-but-unwired per #86's /// direction: if overloaded natives are ever emitted, they must come from /// this same abstraction. diff --git a/prebindgen/src/api/lang/jnigen/jni/symbols.rs b/prebindgen/src/api/lang/jnigen/jni/symbols.rs index 89975286..bc09ba3f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/symbols.rs +++ b/prebindgen/src/api/lang/jnigen/jni/symbols.rs @@ -34,7 +34,10 @@ use super::*; /// names colliding in one package, including a collision the mangler /// created. /// * **Warnings** — where the default mangler sanitized a Rust-derived name. -pub(crate) fn validate_symbols(ext: &JniGen, registry: &Registry) -> Vec { +pub(crate) fn validate_symbols( + ext: &JniGenBuilder, + registry: &Registry, +) -> Vec { let mut errors: Vec = Vec::new(); // (package, name) → origin, for top-level-unique Kotlin declarations. let mut top_level: BTreeMap<(String, String), String> = BTreeMap::new(); @@ -110,7 +113,7 @@ pub(crate) fn validate_symbols(ext: &JniGen, registry: &Registry) -> // name `Companion` is ours — an artifact of emitting a companion at // all, not a name Kotlin reserves — so when a variant wants it the // generator renames the companion instead of making the source crate - // rename a legitimate variant (`JniGen::sum_companion_name`). + // rename a legitimate variant (`JniGenBuilder::sum_companion_name`). // // The interface's own name is different: BOTH colliding names come // from the source crate (the enum's name and its variant's), so the @@ -259,7 +262,7 @@ fn check_ident(name: &str, origin: &str, errors: &mut Vec) { /// Emit a `cargo:warning` for each Rust struct field (data-class property) or /// enum variant whose Kotlin name the default mangler had to change. -fn warn_derived_name_changes(ext: &JniGen, registry: &Registry) { +fn warn_derived_name_changes(ext: &JniGenBuilder, registry: &Registry) { let warn = |raw: &str, mangled: &str, what: &str, owner: &str| { if raw != mangled { println!( diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs b/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs index 3d2b7aaa..adec1622 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/aliasing.rs @@ -41,14 +41,14 @@ fn build(fns: &[&str], tag: &str) -> String { decls = decls.fun(crate::lang::FunctionDecl::new(id)); items.push((syn::Item::Fn(f), loc.clone())); } - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(decls); - super::flatten::write_all(registry.resolve(jni).expect("resolve"), tag) + super::flatten::write_all(jni.build_with(registry).expect("resolve"), tag) } -/// The generation predicate on the Kotlin side. JniGen has no exclusive-borrow +/// The generation predicate on the Kotlin side. JniGenBuilder has no exclusive-borrow /// mode of its own — `&T` and `&mut T` both reach Kotlin as a locked borrow — /// so the rule reduces to "at least one consumed handle, and any other handle /// in the same domain". diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs index 52b9660c..4f1bd9bc 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs @@ -1,4 +1,5 @@ use super::*; +use crate::api::core::registry::Conversions; fn callback_snapshot_pipeline() -> (String, std::collections::BTreeMap) { use crate::SourceLocation; @@ -33,9 +34,9 @@ fn callback_snapshot_pipeline() -> (String, std::collections::BTreeMap::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("thing") @@ -57,7 +58,7 @@ fn callback_snapshot_pipeline() -> (String, std::collections::BTreeMap::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("thing") @@ -245,7 +246,7 @@ fn callback_root_identity_moved_after_nested_borrow() { let dir = unique_test_dir("jnigen_root_id_order"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -315,9 +316,9 @@ fn callback_double_option_unwrap_pipeline() { loc.clone(), )); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("query") @@ -364,7 +365,7 @@ fn callback_double_option_unwrap_pipeline() { let dir = unique_test_dir("jnigen_double_opt"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -450,7 +451,7 @@ fn callback_double_option_unwrap_pipeline() { // ──────────────────────────────────────────────────────────────────────── // Spec memo (issue #107): every consumer — resolve-time trampoline, // per-function plan, declaration emitter — reads ONE derivation per -// interface identity through `JniGen::iface_spec`. +// interface identity through `JniGenBuilder::iface_spec`. // ──────────────────────────────────────────────────────────────────────── #[test] @@ -484,8 +485,8 @@ fn iface_spec_memo_shares_one_derivation() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("thing") @@ -498,8 +499,8 @@ fn iface_spec_memo_shares_one_derivation() { .field_self() .field(crate::fun!(z_thing_name)), ); - let gen = registry.resolve(jni).expect("resolve"); - let (ext, registry) = (gen.adapter(), gen.registry()); + let gen = jni.build_with(registry).expect("resolve"); + let (ext, registry) = (gen.declarations(), gen.registry()); // Same key twice ⇒ the same allocation (resolve already populated the // memo through the trampoline — a hit also exercises the debug-build @@ -516,7 +517,7 @@ fn iface_spec_memo_shares_one_derivation() { // allocation — the wrapper surface and the interface declaration cannot // diverge from the fold upcall's descriptor. let plan = registry - .unfold_plans + .unfold_plans() .get(&syn::parse_str::("z_things_all").unwrap()) .expect("fold plan"); let via_plan = folder_iface_for_plan(ext, registry, plan).expect("folder spec"); @@ -557,13 +558,13 @@ fn fn_plan_memo_shares_one_derivation() { loc.clone(), )]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_do_thing))); // resolve runs validation, which builds and stores every function's plan. - let gen = registry.resolve(jni).expect("resolve"); - let (ext, registry) = (gen.adapter(), gen.registry()); + let gen = jni.build_with(registry).expect("resolve"); + let (ext, registry) = (gen.declarations(), gen.registry()); let f = ®istry .flat() .function("z_do_thing") diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/config.rs b/prebindgen/src/api/lang/jnigen/jni/tests/config.rs index c391b0e7..74c3229b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/config.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/config.rs @@ -24,21 +24,23 @@ fn ptr_class_implements_adds_interface_supertypes() { .iter() .map(|src| (syn::Item::Fn(syn::parse_str(src).unwrap()), loc.clone())) .collect(); - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .class( - crate::ptr_class!(ZThing) - .implements("io.other.Resource") - .implements("LocalIface") - .method(crate::fun!(z_thing_size)), - ) - .fun(crate::fun!(z_thing_new)), - ); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .class( + crate::ptr_class!(ZThing) + .implements("io.other.Resource") + .implements("LocalIface") + .method(crate::fun!(z_thing_size)), + ) + .fun(crate::fun!(z_thing_new)), + ); let dir = unique_test_dir("jnigen_implements"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let thing = paths @@ -74,21 +76,23 @@ fn ptr_class_interface_emits_generated_api() { .iter() .map(|src| (syn::Item::Fn(syn::parse_str(src).unwrap()), loc.clone())) .collect(); - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .class( - crate::ptr_class!(ZThing) - .interface() - .implements("io.other.Resource") - .method(crate::fun!(z_thing_size)), - ) - .fun(crate::fun!(z_thing_new)), - ); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .class( + crate::ptr_class!(ZThing) + .interface() + .implements("io.other.Resource") + .method(crate::fun!(z_thing_size)), + ) + .fun(crate::fun!(z_thing_new)), + ); let dir = unique_test_dir("jnigen_interface"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let thing = paths @@ -125,7 +129,7 @@ fn ptr_class_duplicate_implements_rejected() { /// name and its result must differ from the class name. An identity hook /// makes the interface collide with the class in the same package — a /// COLLECTED error from the whole-artifact symbol pass (issue #89), surfaced -/// by `resolve` (validation runs once there) before any `Generation` exists, +/// by `build` (validation runs once there) before any `JniGen` exists, /// rather than an emission-time panic. #[test] fn interface_name_mangle_identity_rejected() { @@ -133,9 +137,9 @@ fn interface_name_mangle_identity_rejected() { let f: syn::ItemFn = syn::parse_str("pub fn z_thing_new() -> ZThing { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_interface_name_mangle(|package, n| { assert_eq!(package, "io.test.jni.thing"); @@ -146,8 +150,8 @@ fn interface_name_mangle_identity_rejected() { .class(crate::ptr_class!(ZThing).interface()) .fun(crate::fun!(z_thing_new)), ); - let err = registry - .resolve(jni) + let err = jni + .build_with(registry) .expect_err("interface==class must fail resolve"); let msg = err.to_string(); assert!( @@ -180,8 +184,8 @@ fn interface_name_override_and_hook() { loc.clone(), ), ]; - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_interface_name_mangle(|package, n| { assert_eq!(package, "io.test.jni.m", "hook receives the target package"); @@ -196,7 +200,7 @@ fn interface_name_override_and_hook() { let dir = unique_test_dir("jnigen_iface_name"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let all: String = paths @@ -242,18 +246,20 @@ fn data_class_interface_emits_generated_api() { loc.clone(), ), ]; - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("t").class( - crate::data_class!(ZStamp) - .interface() - .method(crate::fun!(z_stamp_secs).name("secs")), - ), - ); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("t").class( + crate::data_class!(ZStamp) + .interface() + .method(crate::fun!(z_stamp_secs).name("secs")), + ), + ); let dir = unique_test_dir("jnigen_data_iface"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let all: String = paths @@ -313,9 +319,9 @@ fn per_class_name_and_base_package_fun() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") // Rename the handle class; the mangle closures do NOT apply to it. .set_ptr_class_name_mangle(|package, n| { @@ -332,7 +338,7 @@ fn per_class_name_and_base_package_fun() { let dir = unique_test_dir("jnigen_class_name_base_fun"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let kdir = dir.join("kotlin"); let paths = gen.write_kotlin(&kdir).expect("write_kotlin"); @@ -390,10 +396,10 @@ fn setters_after_declarations_apply() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); // Declarations first, settings last. - let jni = JniGen::new() + let jni = JniGenBuilder::new() .package( crate::package!("things") .class(crate::ptr_class!(ZThing)) @@ -408,7 +414,7 @@ fn setters_after_declarations_apply() { let dir = unique_test_dir("jnigen_setters_after_decls"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let kdir = dir.join("kotlin"); let paths = gen.write_kotlin(&kdir).expect("write_kotlin"); @@ -429,7 +435,7 @@ fn setters_after_declarations_apply() { ); } -/// The I3 contract: after `Registry::resolve`, `write_kotlin` and +/// The I3 contract: after `JniGenBuilder::resolve`, `write_kotlin` and /// `write_rust` are pure reads on one receiver — calling Kotlin FIRST /// produces byte-identical output to the usual order. #[test] @@ -438,13 +444,15 @@ fn generation_writes_are_order_free() { let loc = myflat_loc(); let f: syn::ItemFn = syn::parse_str("pub fn z_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); - let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) - .expect("index"); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( + syn::Item::Fn(f), + loc, + )])) + .expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_ping))); - registry.resolve(jni).expect("resolve") + jni.build_with(registry).expect("resolve") }; let read_all = |dir: &std::path::Path, paths: &[std::path::PathBuf]| -> String { let mut out = String::new(); @@ -497,8 +505,8 @@ fn method_hook_can_strip_flat_class_prefix() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_method_name_mangle(|package, class, name| { if class == "JNINative" { @@ -531,7 +539,7 @@ fn method_hook_can_strip_flat_class_prefix() { let dir = unique_test_dir("jnigen_member_names"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let all: String = paths @@ -576,8 +584,8 @@ fn method_name_mangle_hook_applies_order_independently() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("t").class( @@ -600,7 +608,7 @@ fn method_name_mangle_hook_applies_order_independently() { let dir = unique_test_dir("jnigen_method_mangle"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let all: String = paths @@ -626,9 +634,9 @@ fn harness_hook_receives_derived_default() { let f: syn::ItemFn = syn::parse_str("pub fn z_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_harness_name_mangle(|n| { assert_eq!(n, "JNINative", "hook must receive the derived default"); @@ -638,7 +646,7 @@ fn harness_hook_receives_derived_default() { let dir = unique_test_dir("jnigen_harness_mangle"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); // The extern symbol path carries the replaced harness name. @@ -663,9 +671,9 @@ fn function_and_native_method_hooks_receive_placement() { let f: syn::ItemFn = syn::parse_str("pub fn z_session_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_fun_name_mangle(|package, name| { assert_eq!(package, "io.test.jni.session"); @@ -682,7 +690,7 @@ fn function_and_native_method_hooks_receive_placement() { let dir = unique_test_dir("jnigen_placement_mangles"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).unwrap()).unwrap(); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let all = paths @@ -708,12 +716,12 @@ fn write_kotlin_owns_and_resets_the_root() { let f: syn::ItemFn = syn::parse_str("pub fn z_ping(v: i64) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_ping))); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let dir = unique_test_dir("jnigen_owned_root"); let _ = std::fs::remove_dir_all(&dir); @@ -737,7 +745,7 @@ fn write_kotlin_owns_and_resets_the_root() { assert!(paths2.iter().all(|p| p.exists())); } -/// C7: `Generation::report()` — the explain mode. The report carries the +/// C7: `JniGen::report()` — the explain mode. The report carries the /// FINAL Kotlin signature of each fn (same render path as the emitters), /// the plans that shaped it, an unshaped fn with no `shaped by:` lines, /// and the type table. Deterministic across calls. @@ -758,8 +766,8 @@ fn report_explains_the_resolved_surface() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -776,7 +784,7 @@ fn report_explains_the_resolved_surface() { .field(crate::fun!(summary_count)) .field(crate::fun!(summary_total)), ); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let report = gen.report(); // The reshaped fn: exact signature (builder callback form) + provenance. @@ -875,8 +883,8 @@ fn docs_become_kdoc_with_shape_notes() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -894,7 +902,7 @@ fn docs_become_kdoc_with_shape_notes() { .field(crate::fun!(summary_count)) .field(crate::fun!(summary_total)), ); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let dir = unique_test_dir("jnigen_kdoc"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs b/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs index 760ea660..57d8b3e7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/consts.rs @@ -29,19 +29,21 @@ fn const_items() -> Vec<(syn::Item, crate::SourceLocation)> { /// private helpers, and `JNINative` declares the matching `external fun`s. #[test] fn declared_consts_emit_getter_and_val() { - let registry = - Registry::::from_items(declare_referenced(const_items())).expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced(const_items())) + .expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("cfg") - .constant(crate::constant!(MAX_LEN)) - .constant(crate::constant!(GREETING).name("HELLO")), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("cfg") + .constant(crate::constant!(MAX_LEN)) + .constant(crate::constant!(GREETING).name("HELLO")), + ); let dir = unique_test_dir("jnigen_consts_basic"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); // Path-alias const re-emission (the initializer tokens are never @@ -109,20 +111,20 @@ fn declared_consts_emit_getter_and_val() { /// return: public `ULong`, private/native `Long`, with a bit-preserving wrap. #[test] fn unsigned_const_uses_ulong_surface() { - let registry = Registry::::from_items(declare_referenced(vec![( + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( syn::Item::Const(syn::parse_quote!( pub const MAX_UNSIGNED: u64 = u64::MAX; )), myflat_loc(), )])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("cfg").constant(crate::constant!(MAX_UNSIGNED))); let dir = unique_test_dir("jnigen_consts_unsigned"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let kotlin = paths @@ -139,15 +141,15 @@ fn unsigned_const_uses_ulong_surface() { assert!(kc.contains(".toULong()"), "{kotlin}"); } -/// An undeclared const emits nothing (JniGen has a const declaration +/// An undeclared const emits nothing (JniGenBuilder has a const declaration /// mechanism, so const emission is declared-only); `ignore_const` /// acknowledges it without emitting. #[test] fn undeclared_const_not_emitted() { - let registry = - Registry::::from_items(declare_referenced(const_items())).expect("index items"); + let registry = crate::api::test_util::reg_from_items(declare_referenced(const_items())) + .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("cfg").constant(crate::constant!(MAX_LEN))) .ignore(crate::constant!(GREETING)); @@ -155,7 +157,7 @@ fn undeclared_const_not_emitted() { let dir = unique_test_dir("jnigen_consts_undeclared"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); assert!(rust.contains("pub const MAX_LEN"), "{rust}"); @@ -187,16 +189,16 @@ fn constant_fun_source_emits_val_over_ordinary_wrapper() { loc.clone(), )]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("cfg").constant(crate::constant!(THE_TAG).fun(crate::fun!(tag)))); let dir = unique_test_dir("jnigen_constant_fun_basic"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); // Ordinary declared-function wrapper: an extern calling `myflat::tag()`. @@ -240,14 +242,16 @@ fn constant_fun_source_non_nullary_rejected() { loc.clone(), )]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("cfg").constant(crate::constant!(SCALED).fun(crate::fun!(scaled))), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("cfg").constant(crate::constant!(SCALED).fun(crate::fun!(scaled))), + ); let dir = unique_test_dir("jnigen_constant_fun_arity_reject"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let _ = gen.write_kotlin(&dir.join("kotlin")); } @@ -277,16 +281,18 @@ fn constant_fun_source_handle_return_rejected() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("things") - .class(crate::ptr_class!(ZThing)) - .constant(crate::constant!(DEFAULT_THING).fun(crate::fun!(default_thing))), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("things") + .class(crate::ptr_class!(ZThing)) + .constant(crate::constant!(DEFAULT_THING).fun(crate::fun!(default_thing))), + ); let dir = unique_test_dir("jnigen_constant_fun_handle_reject"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let _ = gen.write_kotlin(&dir.join("kotlin")); } @@ -309,18 +315,18 @@ fn constant_expr_emits_getter_and_val() { loc.clone(), )]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("cfg").fun(crate::fun!(tag_of)).constant( + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package(crate::package!("cfg").fun(crate::fun!(tag_of)).constant( crate::constant!(DEFAULT_TAG).expr(crate::ty!(String), crate::expr!(tag_of(7))), - ), - ); + )); let dir = unique_test_dir("jnigen_constant_expr_basic"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -381,20 +387,23 @@ fn constant_expr_handle_type_rejected() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("things") - .class(crate::ptr_class!(ZThing)) - .fun(crate::fun!(thing_new)) - .constant( - crate::constant!(DEFAULT_THING).expr(crate::ty!(ZThing), crate::expr!(thing_new())), - ), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("things") + .class(crate::ptr_class!(ZThing)) + .fun(crate::fun!(thing_new)) + .constant( + crate::constant!(DEFAULT_THING) + .expr(crate::ty!(ZThing), crate::expr!(thing_new())), + ), + ); let dir = unique_test_dir("jnigen_constant_expr_handle_reject"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .and_then(|gen| gen.write_rust(dir.join("gen.rs"))); } @@ -429,20 +438,22 @@ fn handle_const_rejected() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("things") - .class(crate::ptr_class!(ZThing)) - .fun(crate::fun!(thing_new)) - .constant(crate::constant!(DEFAULT_THING)), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("things") + .class(crate::ptr_class!(ZThing)) + .fun(crate::fun!(thing_new)) + .constant(crate::constant!(DEFAULT_THING)), + ); let dir = unique_test_dir("jnigen_consts_handle_reject"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .and_then(|gen| gen.write_rust(dir.join("gen.rs"))); } @@ -462,17 +473,19 @@ fn constant_with_source_calls_path_verbatim() { loc.clone(), )]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("cfg").fun(crate::fun!(unrelated)).constant( - crate::constant!(COVER_VERSION) - .with(crate::ty!(String), crate::path!(crate::cover_version)), - ), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("cfg").fun(crate::fun!(unrelated)).constant( + crate::constant!(COVER_VERSION) + .with(crate::ty!(String), crate::path!(crate::cover_version)), + ), + ); let dir = unique_test_dir("jnigen_constant_with_basic"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs b/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs index 7d3602f7..ca17309e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/cross_artifact.rs @@ -259,14 +259,14 @@ fn assert_cross_artifact(rust_src: &str, kotlin: &BTreeMap) { fn run_pipeline( tag: &str, items: Vec<(syn::Item, crate::SourceLocation)>, - jni: JniGen, + jni: JniGenBuilder, ) -> (String, BTreeMap) { let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); @@ -351,7 +351,7 @@ fn cross_artifact_representative_shapes_agree() { loc.clone(), ), ]; - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -430,7 +430,7 @@ fn cross_artifact_flatten_vec_callback_builder_agree() { loc.clone(), ), ]; - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("thing") @@ -489,7 +489,7 @@ fn cross_artifact_optional_iterable_fold_agrees() { loc.clone(), ), ]; - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("thing") diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs b/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs index 5fbb3914..f888281d 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs @@ -1,4 +1,5 @@ use super::*; +use crate::api::core::registry::{Conversions, RegistryBuilder}; /// Two fns returning the same type under different output decompositions: /// the type-level `expand_return!` default and a per-fn `.return_expand(...)` @@ -20,9 +21,9 @@ fn inline_output_gets_own_builder() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("thing") @@ -56,7 +57,7 @@ fn inline_output_gets_own_builder() { let dir = unique_test_dir("jnigen_inline_out"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -118,9 +119,9 @@ fn error_unwrap_universal_records() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("errors") @@ -146,7 +147,7 @@ fn error_unwrap_universal_records() { let dir = unique_test_dir("jnigen_err_universal"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -250,32 +251,34 @@ fn method_constructor_and_inline_field_self() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .class( - crate::ptr_class!(ZThing) - .method(crate::fun!(z_thing_name).name("name")) - // A method with extra params: `&ZThing` receiver + a `name: String` param. - .method(crate::fun!(z_thing_rename).name("rename")) - // A constructor: factory returning ZThing. - .constructor(crate::fun!(z_thing_make).name("make")), - ) - // A free fn whose per-fn inline output decomposes to (handle, name). - .fun( - crate::fun!(z_get).expand_return( - crate::expand_return!(ZThing) - .field_self() - .field(crate::fun!(z_thing_name).name("name")), + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .class( + crate::ptr_class!(ZThing) + .method(crate::fun!(z_thing_name).name("name")) + // A method with extra params: `&ZThing` receiver + a `name: String` param. + .method(crate::fun!(z_thing_rename).name("rename")) + // A constructor: factory returning ZThing. + .constructor(crate::fun!(z_thing_make).name("make")), + ) + // A free fn whose per-fn inline output decomposes to (handle, name). + .fun( + crate::fun!(z_get).expand_return( + crate::expand_return!(ZThing) + .field_self() + .field(crate::fun!(z_thing_name).name("name")), + ), ), - ), - ); + ); let dir = unique_test_dir("jnigen_method_ctor"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let kdir = dir.join("kotlin"); let paths = gen.write_kotlin(&kdir).expect("write_kotlin"); @@ -321,9 +324,9 @@ fn rust_side_only_error_type() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_fallible))) // No class declaration for ZErr anywhere — rust-side-only. The field @@ -333,7 +336,7 @@ fn rust_side_only_error_type() { let dir = unique_test_dir("jnigen_rust_side_only_err"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -390,9 +393,9 @@ fn rust_side_only_input_type() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_run))) .expand(crate::expand_param!(ZOpts).variant(crate::fun!(z_opts_new))); @@ -400,7 +403,7 @@ fn rust_side_only_input_type() { let dir = unique_test_dir("jnigen_rust_side_only_in"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -434,16 +437,16 @@ fn rust_side_only_variant_self_rejected() { let f: syn::ItemFn = syn::parse_str("pub fn z_run(opts: ZOpts) -> i64 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .package(crate::package!("ops").fun(crate::fun!(z_run))) .expand(crate::expand_param!(ZOpts).variant_self()); let dir = unique_test_dir("jnigen_rso_self_in"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .and_then(|gen| gen.write_rust(dir.join("gen.rs"))); } @@ -455,16 +458,16 @@ fn rust_side_only_field_self_rejected() { let loc = myflat_loc(); let f: syn::ItemFn = syn::parse_str("pub fn z_make() -> ZThing { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .package(crate::package!("ops").fun(crate::fun!(z_make))) .expand(crate::expand_return!(ZThing).field_self()); let dir = unique_test_dir("jnigen_rso_self_out"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .and_then(|gen| gen.write_rust(dir.join("gen.rs"))); } @@ -486,8 +489,8 @@ fn fn_expand_param_type_mismatch_rejected() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().package( + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new().package( crate::package!("ops") .class(crate::ptr_class!(ZThing).constructor(crate::fun!(z_thing_make))) .class(crate::ptr_class!(ZOther)) @@ -500,7 +503,9 @@ fn fn_expand_param_type_mismatch_rejected() { let dir = unique_test_dir("jnigen_fn_param_mismatch"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let err = registry.resolve(jni).expect_err("type mismatch must fail"); + let err = jni + .build_with(registry) + .expect_err("type mismatch must fail"); let msg = format!("{err}"); assert!(msg.contains("ZOther") && msg.contains("ZThing"), "{msg}"); } @@ -522,8 +527,8 @@ fn fn_expand_return_type_mismatch_rejected() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().package( + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new().package( crate::package!("ops") .class(crate::ptr_class!(ZThing).method(crate::fun!(z_thing_name).name("name"))) .class(crate::ptr_class!(ZOther)) @@ -533,7 +538,9 @@ fn fn_expand_return_type_mismatch_rejected() { let dir = unique_test_dir("jnigen_fn_return_mismatch"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let err = registry.resolve(jni).expect_err("type mismatch must fail"); + let err = jni + .build_with(registry) + .expect_err("type mismatch must fail"); let msg = format!("{err}"); assert!(msg.contains("ZOther") && msg.contains("ZThing"), "{msg}"); } @@ -555,8 +562,8 @@ fn fn_expand_param_unknown_param_rejected() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().package( + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new().package( crate::package!("ops") .class(crate::ptr_class!(ZThing).constructor(crate::fun!(z_thing_make))) .fun(crate::fun!(z_use).expand_param( @@ -567,7 +574,9 @@ fn fn_expand_param_unknown_param_rejected() { let dir = unique_test_dir("jnigen_fn_param_unknown"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let err = registry.resolve(jni).expect_err("unknown param must fail"); + let err = jni + .build_with(registry) + .expect_err("unknown param must fail"); assert!(format!("{err}").contains("typo"), "{err}"); } @@ -592,9 +601,9 @@ fn typo_in_expand_decl_is_hard_error() { let f: syn::ItemFn = syn::parse_str("pub fn z_fallible() -> Result { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_fallible))) // `z_err_mesage` (sic) exists nowhere among the indexed items. @@ -602,8 +611,8 @@ fn typo_in_expand_decl_is_hard_error() { let dir = unique_test_dir("jnigen_expand_typo_hard_error"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let err = registry - .resolve(jni) + let err = jni + .build_with(registry) .expect_err("typo'd expand accessor must fail the scan"); match err { WriteRustError::Scan(ScanError::DeclaredNotFound { entries }) => { @@ -637,8 +646,8 @@ fn ignore_matching_acknowledges_naming_family() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_len))) .ignore(crate::matching(|name| name.starts_with("detail_const_"))) @@ -646,7 +655,6 @@ fn ignore_matching_acknowledges_naming_family() { .ignore(crate::ty!(ZUnusedThing)); // The predicate flows through the Prebindgen hook… { - use crate::api::core::prebindgen::Prebindgen; let preds = jni.ignored_name_predicates(); assert_eq!(preds.len(), 1); assert!(preds[0]("detail_const_a") && !preds[0]("z_len")); @@ -658,7 +666,7 @@ fn ignore_matching_acknowledges_naming_family() { let dir = unique_test_dir("jnigen_ignore_funs_where"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); assert!(rust.contains("Java_io_test_jni_JNINative_zLen"), "{rust}"); @@ -735,15 +743,17 @@ fn method_without_receiver_rejected() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("t").class( - crate::ptr_class!(ZThing) - .method(crate::fun!(z_thing_free_standing)) - .constructor(crate::fun!(z_make)), - ), - ); - let err = registry.resolve(jni).expect_err("receiver-less member"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("t").class( + crate::ptr_class!(ZThing) + .method(crate::fun!(z_thing_free_standing)) + .constructor(crate::fun!(z_make)), + ), + ); + let err = jni.build_with(registry).expect_err("receiver-less member"); let msg = format!("{err}"); assert!( msg.contains("method `z_thing_free_standing`") && msg.contains("`ZThing`"), @@ -767,15 +777,17 @@ fn constructor_with_wrong_return_rejected() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("t").class( - crate::ptr_class!(ZThing) - .method(crate::fun!(z_thing_len)) - .constructor(crate::fun!(z_make_number)), - ), - ); - let err = registry.resolve(jni).expect_err("wrong ctor return"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("t").class( + crate::ptr_class!(ZThing) + .method(crate::fun!(z_thing_len)) + .constructor(crate::fun!(z_make_number)), + ), + ); + let err = jni.build_with(registry).expect_err("wrong ctor return"); let msg = format!("{err}"); assert!( msg.contains("constructor `z_make_number`") && msg.contains("it returns `i64`"), @@ -811,8 +823,8 @@ fn binding_local_field_conditional_handle() { )); } let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("enc") @@ -831,7 +843,7 @@ fn binding_local_field_conditional_handle() { let dir = unique_test_dir("jnigen_local_field"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -896,8 +908,8 @@ fn binding_local_field_name_collision_rejected() { )); } let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("enc") @@ -913,8 +925,8 @@ fn binding_local_field_name_collision_rejected() { .name("id"), ), ); - let err = registry - .resolve(jni) + let err = jni + .build_with(registry) .expect_err("collision must be rejected"); let msg = format!("{err}"); assert!(msg.contains("collides"), "{msg}"); @@ -958,8 +970,8 @@ fn binding_local_field_splices_through_parent() { )); } let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("msg") @@ -984,7 +996,7 @@ fn binding_local_field_splices_through_parent() { let dir = unique_test_dir("jnigen_local_field_splice"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -1033,8 +1045,8 @@ fn binding_local_functions_all_positions() { )); } let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("t") @@ -1069,7 +1081,7 @@ fn binding_local_functions_all_positions() { let dir = unique_test_dir("jnigen_local_funs"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -1129,8 +1141,8 @@ fn binding_local_fn_names_flow_through_manglers() { items.push((syn::Item::Fn(syn::parse_str(src).unwrap()), loc.clone())); } let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") // Custom hooks: prefix every derived name — proof the hook RAN and // received the camel-cased last segment with its context. @@ -1169,7 +1181,7 @@ fn binding_local_fn_names_flow_through_manglers() { .field_self(), ); let raw = write_all( - registry.resolve(jni).expect("resolve"), + jni.build_with(registry).expect("resolve"), "jnigen_local_mangle", ); let all: String = raw.split_whitespace().collect(); @@ -1186,7 +1198,7 @@ fn binding_local_fn_names_flow_through_manglers() { #[test] #[should_panic(expected = ".sig(sig!(")] fn binding_local_fun_missing_sig_rejected() { - let _ = JniGen::new() + let _ = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("t").fun(crate::fun!(crate::z_no_sig))); } @@ -1219,15 +1231,15 @@ fn binding_local_fun_name_collision_rejected() { loc.clone(), )); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("t").class(crate::ptr_class!(ZThing)).fun( + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package(crate::package!("t").class(crate::ptr_class!(ZThing)).fun( // shadows the #[prebindgen] fn of the same name crate::fun!(crate::z_thing_len).sig(crate::sig!((t: &ZThing) -> i64)), - ), - ); - let err = registry - .resolve(jni) + )); + let err = jni + .build_with(registry) .expect_err("collision must be rejected"); assert!(format!("{err}").contains("collides"), "{err}"); } @@ -1273,19 +1285,24 @@ fn gc_managed_handle_lifecycle() { )); } let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("t") - .class( - crate::ptr_class!(ZThing) - .gc_managed() - .constructor(crate::fun!(z_thing_new)), - ) - .class(crate::ptr_class!(ZOther).constructor(crate::fun!(z_other_new))) - .fun(crate::fun!(z_thing_use)) - .fun(crate::fun!(z_other_use)), + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("t") + .class( + crate::ptr_class!(ZThing) + .gc_managed() + .constructor(crate::fun!(z_thing_new)), + ) + .class(crate::ptr_class!(ZOther).constructor(crate::fun!(z_other_new))) + .fun(crate::fun!(z_thing_use)) + .fun(crate::fun!(z_other_use)), + ); + let raw = write_all( + jni.build_with(registry).expect("resolve"), + "jnigen_gc_managed", ); - let raw = write_all(registry.resolve(jni).expect("resolve"), "jnigen_gc_managed"); let all: String = raw.split_whitespace().collect(); // Shared harness: cell-backed base, CAS helper, shared Cleaner, register fn. @@ -1337,7 +1354,7 @@ fn gc_managed_handle_lifecycle() { /// #52 shared fixture: a `ZSummary` ptr class, its `(count, total)` builder, a /// splittable 2-variant type-level `expand_param!`, and functions taking one or /// two `ZSummary` params. `extra` fns are appended before indexing. -fn split_fixture(extra: &[&str]) -> Registry { +fn split_fixture(extra: &[&str]) -> RegistryBuilder { let loc = myflat_loc(); let base: &[&str] = &[ "pub fn z_summary_new(count: i64, total: f64) -> ZSummary { unimplemented!() }", @@ -1358,10 +1375,10 @@ fn split_fixture(extra: &[&str]) -> Registry { loc.clone(), )); } - Registry::::from_items(declare_referenced(items)).expect("index items") + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items") } -pub(super) fn write_all(gen: crate::api::core::Generation, tag: &str) -> String { +pub(super) fn write_all(gen: crate::api::lang::jnigen::JniGen, tag: &str) -> String { let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); @@ -1381,7 +1398,7 @@ pub(super) fn write_all(gen: crate::api::core::Generation, tag: &str) -> #[test] fn split_on_param_emits_typed_overloads() { let registry = split_fixture(&[]); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1393,7 +1410,10 @@ fn split_on_param_emits_typed_overloads() { .variant(crate::fun!(z_summary_new)) .variant_self(), ); - let raw = write_all(registry.resolve(jni).expect("resolve"), "jnigen_split_one"); + let raw = write_all( + jni.build_with(registry).expect("resolve"), + "jnigen_split_one", + ); let all: String = raw.split_whitespace().collect(); assert!(all.contains("expectedSel:Int"), "{raw}"); // selector retained assert!( @@ -1411,7 +1431,7 @@ fn split_on_param_emits_typed_overloads() { #[test] fn split_on_param_cartesian_product() { let registry = split_fixture(&[]); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1427,7 +1447,10 @@ fn split_on_param_cartesian_product() { .variant(crate::fun!(z_summary_new)) .variant_self(), ); - let raw = write_all(registry.resolve(jni).expect("resolve"), "jnigen_split_prod"); + let raw = write_all( + jni.build_with(registry).expect("resolve"), + "jnigen_split_prod", + ); let all: String = raw.split_whitespace().collect(); // build / build assert!( @@ -1469,7 +1492,7 @@ fn split_on_param_preserves_wrapper_generics() { "pub fn z_summary_total(s: &ZSummary) -> f64 { unimplemented!() }", "pub fn z_summarize(primary: ZSummary, fallback: ZSummary) -> ZSummary { unimplemented!() }", ]); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1491,7 +1514,7 @@ fn split_on_param_preserves_wrapper_generics() { .field(crate::fun!(z_summary_total)), ); let raw = write_all( - registry.resolve(jni).expect("resolve"), + jni.build_with(registry).expect("resolve"), "jnigen_split_generic", ); let all: String = raw.split_whitespace().collect(); @@ -1544,8 +1567,8 @@ fn split_on_param_product_ambiguous_rejected() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops").class(crate::ptr_class!(ZThing)).fun( @@ -1560,7 +1583,7 @@ fn split_on_param_product_ambiguous_rejected() { .variant(crate::fun!(z_thing_two)), ); let _ = write_all( - registry.resolve(jni).expect("resolve"), + jni.build_with(registry).expect("resolve"), "jnigen_split_ambig", ); } @@ -1587,8 +1610,8 @@ fn split_declaration_colliding_variants_rejected() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1600,12 +1623,15 @@ fn split_declaration_colliding_variants_rejected() { .variant(crate::fun!(z_name_from_text)) .variant(crate::fun!(z_name_from_label)), ); - let _ = write_all(registry.resolve(jni).expect("resolve"), "jnigen_split_decl"); + let _ = write_all( + jni.build_with(registry).expect("resolve"), + "jnigen_split_decl", + ); } /// #90: the validation boundary is now in `resolve` — a colliding split /// declaration (a Kotlin-side concern) fails `resolve` as a clean `Err`, so -/// no `Generation` is produced and neither artifact can be written. +/// no `JniGen` is produced and neither artifact can be written. #[test] fn split_declaration_collision_fails_resolve() { let loc = myflat_loc(); @@ -1625,8 +1651,8 @@ fn split_declaration_collision_fails_resolve() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1638,8 +1664,8 @@ fn split_declaration_collision_fails_resolve() { .variant(crate::fun!(z_name_from_text)) .variant(crate::fun!(z_name_from_label)), ); - let err = registry - .resolve(jni) + let err = jni + .build_with(registry) .expect_err("colliding split declaration must fail resolve"); assert!( err.to_string().contains("same JVM signature"), @@ -1668,8 +1694,8 @@ fn split_no_split_suppresses_check() { for s in srcs { items.push((syn::Item::Fn(syn::parse_str(s).unwrap()), loc.clone())); } - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1683,7 +1709,10 @@ fn split_no_split_suppresses_check() { .no_split(), ); // No panic: the colliding variants are tolerated as selector-only. - let raw = write_all(registry.resolve(jni).expect("resolve"), "jnigen_no_split"); + let raw = write_all( + jni.build_with(registry).expect("resolve"), + "jnigen_no_split", + ); let all: String = raw.split_whitespace().collect(); assert!(all.contains("nameSel:Int"), "{raw}"); // selector form emitted } @@ -1694,7 +1723,7 @@ fn split_no_split_suppresses_check() { #[should_panic(expected = "no parameter named")] fn split_on_unknown_param_rejected() { let registry = split_fixture(&[]); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1706,7 +1735,10 @@ fn split_on_unknown_param_rejected() { .variant(crate::fun!(z_summary_new)) .variant_self(), ); - let _ = write_all(registry.resolve(jni).expect("resolve"), "jnigen_split_typo"); + let _ = write_all( + jni.build_with(registry).expect("resolve"), + "jnigen_split_typo", + ); } /// Nullable-arm rule: `.split_on_param` on an `Option` parameter emits @@ -1718,7 +1750,7 @@ fn split_on_unknown_param_rejected() { fn split_on_option_param_emits_nullable_arm() { let registry = split_fixture(&["pub fn z_maybe(opt: Option) -> bool { unimplemented!() }"]); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1730,7 +1762,10 @@ fn split_on_option_param_emits_nullable_arm() { .variant(crate::fun!(z_summary_new)) .variant_self(), ); - let raw = write_all(registry.resolve(jni).expect("resolve"), "jnigen_split_opt"); + let raw = write_all( + jni.build_with(registry).expect("resolve"), + "jnigen_split_opt", + ); let all: String = raw.split_whitespace().collect(); // Selector form retained; single nullable overload for the identity arm. assert!(all.contains("optSel:Int"), "{raw}"); @@ -1753,7 +1788,7 @@ fn split_on_option_param_without_single_leaf_arm_rejected() { "pub fn z_summary_scaled(units: String, factor: f64) -> ZSummary { unimplemented!() }", "pub fn z_maybe(opt: Option) -> bool { unimplemented!() }", ]); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1766,7 +1801,7 @@ fn split_on_option_param_without_single_leaf_arm_rejected() { .variant(crate::fun!(z_summary_scaled)), ); let _ = write_all( - registry.resolve(jni).expect("resolve"), + jni.build_with(registry).expect("resolve"), "jnigen_split_opt_no_arm", ); } @@ -1780,7 +1815,7 @@ fn split_on_param_optional_cartesian_with_plain() { let registry = split_fixture(&[ "pub fn z_mixed(primary: ZSummary, fallback: Option<&ZSummary>) -> i64 { unimplemented!() }", ]); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1797,7 +1832,7 @@ fn split_on_param_optional_cartesian_with_plain() { .variant_self(), ); let raw = write_all( - registry.resolve(jni).expect("resolve"), + jni.build_with(registry).expect("resolve"), "jnigen_split_opt_prod", ); let all: String = raw.split_whitespace().collect(); @@ -1854,8 +1889,8 @@ fn optional_selector_dispatch_end_to_end() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1870,7 +1905,7 @@ fn optional_selector_dispatch_end_to_end() { let dir = unique_test_dir("jnigen_opt_selector"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -1914,8 +1949,8 @@ fn constructor_member_skips_default_output_expand() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("ops") @@ -1933,11 +1968,11 @@ fn constructor_member_skips_default_output_expand() { .field_self() .field(crate::fun!(z_thing_name)), ); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let registry = gen.registry(); // …the free fn is decomposed… assert!( - registry.unfold_plans.contains_key(&syn::Ident::new( + registry.unfold_plans().contains_key(&syn::Ident::new( "z_thing_get", proc_macro2::Span::call_site() )), @@ -1945,7 +1980,7 @@ fn constructor_member_skips_default_output_expand() { ); // …but the constructor member is NOT (its return is the factory value). assert!( - !registry.unfold_plans.contains_key(&syn::Ident::new( + !registry.unfold_plans().contains_key(&syn::Ident::new( "z_thing_make", proc_macro2::Span::call_site() )), @@ -1981,16 +2016,20 @@ fn qualified_signature_spelling_matches_bare_ptr_class() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .class(crate::ptr_class!(ZThing).method(crate::fun!(z_thing_name).name("name"))) - .fun(crate::fun!(z_thing_get)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .class(crate::ptr_class!(ZThing).method(crate::fun!(z_thing_name).name("name"))) + .fun(crate::fun!(z_thing_get)), + ); let dir = unique_test_dir("jnigen_q95"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("qualified spellings resolve"); + let gen = jni + .build_with(registry) + .expect("qualified spellings resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let all: String = paths diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs index 6e23b58e..c5cf3b8e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs @@ -67,8 +67,7 @@ fn install_input( e: TypeEntry, ) { let key = TypeKey::parse(ty_str).expect("test type"); - reg.input_types - .insert(key.clone(), cell(&key, true, Some(e))); + reg.input_types.insert(key.clone(), cell(true, Some(e))); } fn install_output( @@ -78,6 +77,5 @@ fn install_output( e: TypeEntry, ) { let key = TypeKey::parse(ty_str).expect("test type"); - reg.output_types - .insert(key.clone(), cell(&key, true, Some(e))); + reg.output_types.insert(key.clone(), cell(true, Some(e))); } diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs b/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs index f5f1a3f3..12d6134b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/niches.rs @@ -232,7 +232,7 @@ fn option_box_fallback_exposes_no_niches() { } // ──────────────────────────────────────────────────────────────────────── -// End-to-end pipeline snapshot: drive a representative `JniGen` config +// End-to-end pipeline snapshot: drive a representative `JniGenBuilder` config // through `write_rust` + `write_kotlin` and assert on the generated Rust and // Kotlin. Mirrors `cbindgen`'s `tests.rs` behavioural-assertion style (the // authoritative byte-for-byte check is the `zenoh-flat-jni` consumer diff); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs index b3ef1f72..9259cd89 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs @@ -1,4 +1,5 @@ use super::*; +use crate::api::core::registry::RegistryBuilder; /// Emit the Kotlin surface for a `sealed_class!`-declared sum, optionally /// with a per-variant rename, and return the single generated file's text. @@ -32,22 +33,24 @@ fn sealed_kotlin(rename_labeled: Option<&str>) -> String { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); let mut sealed = crate::sealed_class!(Reading); if let Some(n) = rename_labeled { sealed = sealed.variant(crate::variant!(Labeled).name(n)); } - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::enum_class!(Priority)) - .class(sealed), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::enum_class!(Priority)) + .class(sealed), + ); let dir = unique_test_dir("jnigen_sealed"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); paths .iter() @@ -156,15 +159,15 @@ fn declarators_do_not_accept_each_others_shape() { let emit = |item: syn::Item, decl: crate::lang::ClassDecl, tag: &str| { let registry = - Registry::::from_items(declare_referenced(vec![(item, loc.clone())])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(item, loc.clone())])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(decl)); let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let _ = gen.write_kotlin(&dir.join("kotlin")); }; @@ -189,7 +192,7 @@ fn declarators_do_not_accept_each_others_shape() { fn unknown_variant_is_an_error() { let loc = myflat_loc(); let boom = || { - let registry = Registry::::from_items(declare_referenced(vec![( + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -199,14 +202,16 @@ fn unknown_variant_is_an_error() { loc.clone(), )])) .expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Reading).variant(crate::variant!(Nope).name("X"))), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading).variant(crate::variant!(Nope).name("X"))), + ); let dir = unique_test_dir("sealed_unknown_variant"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let _ = gen.write_kotlin(&dir.join("kotlin")); }; assert!(std::panic::catch_unwind(std::panic::AssertUnwindSafe(boom)).is_err()); @@ -218,7 +223,7 @@ fn unknown_variant_is_an_error() { #[test] fn reopened_sealed_class_merges_variant_names() { let loc = myflat_loc(); - let registry = Registry::::from_items(declare_referenced(vec![( + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -229,7 +234,7 @@ fn reopened_sealed_class_merges_variant_names() { loc.clone(), )])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!().class( @@ -246,7 +251,7 @@ fn reopened_sealed_class_merges_variant_names() { let dir = unique_test_dir("sealed_reopen"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let kt: String = paths .iter() @@ -285,9 +290,10 @@ fn reopened_ptr_class_keeps_gc_managed() { )] }; let gc_managed_of = |first: crate::lang::PtrClassDecl, second: crate::lang::PtrClassDecl| { - let registry = - Registry::::from_items(declare_referenced(items())).expect("index items"); - let jni = JniGen::new() + let registry: RegistryBuilder = + crate::api::test_util::reg_from_items(declare_referenced(items())) + .expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(first).class(second)); drop(registry); @@ -342,9 +348,10 @@ fn a_type_gets_one_class_declarator() { ] }; let declare = |first: crate::lang::ClassDecl, second: crate::lang::ClassDecl| { - let registry = - Registry::::from_items(declare_referenced(items())).expect("index items"); - let _ = JniGen::new() + let registry: RegistryBuilder = + crate::api::test_util::reg_from_items(declare_referenced(items())) + .expect("index items"); + let _ = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(first).class(second)); drop(registry); @@ -444,15 +451,15 @@ fn variant_cannot_take_a_name_the_interface_body_already_uses() { // Resolve is where `validate_symbols` runs, so the error surfaces before // any artifact writer touches disk. let resolve_err = |decl: crate::lang::SealedClassDecl, item: syn::ItemEnum| -> String { - let registry = Registry::::from_items(declare_referenced(vec![( + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( syn::Item::Enum(item), loc.clone(), )])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(decl)); - match registry.resolve(jni) { + match jni.build_with(registry) { Ok(_) => String::new(), Err(e) => e.to_string(), } @@ -513,18 +520,18 @@ fn variant_cannot_take_a_name_the_interface_body_already_uses() { fn variant_named_companion_moves_the_companion_not_the_variant() { let loc = myflat_loc(); let emit = |decl: crate::lang::SealedClassDecl, item: syn::ItemEnum, tag: &str| -> String { - let registry = Registry::::from_items(declare_referenced(vec![( + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( syn::Item::Enum(item), loc.clone(), )])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(decl)); let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_kotlin(&dir.join("kotlin")) .expect("write_kotlin") .iter() @@ -587,7 +594,7 @@ fn variant_named_companion_moves_the_companion_not_the_variant() { fn payload_without_output_converter_is_an_error() { let loc = myflat_loc(); let boom = || { - let registry = Registry::::from_items(declare_referenced(vec![( + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -599,13 +606,13 @@ fn payload_without_output_converter_is_an_error() { loc.clone(), )])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(crate::sealed_class!(Reading))); let dir = unique_test_dir("sealed_unmapped_payload"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let _ = gen.write_kotlin(&dir.join("kotlin")); }; let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(boom)).expect_err("must fail"); @@ -625,7 +632,7 @@ fn payload_without_output_converter_is_an_error() { #[test] fn sum_is_its_own_type_kind() { let loc = myflat_loc(); - let registry = Registry::::from_items(declare_referenced(vec![( + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![( syn::Item::Enum(syn::parse_quote!( pub enum Reading { Missing, @@ -635,7 +642,7 @@ fn sum_is_its_own_type_kind() { loc.clone(), )])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(crate::sealed_class!(Reading))); let ty: syn::Type = syn::parse_quote!(Reading); @@ -666,7 +673,7 @@ fn vec_of_sum_is_rejected_as_a_struct_field() { unimplemented!() } ); - let registry = Registry::::from_items(declare_referenced(vec![ + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![ ( syn::Item::Enum(syn::parse_quote!( pub enum Reading { @@ -680,17 +687,19 @@ fn vec_of_sum_is_rejected_as_a_struct_field() { (syn::Item::Fn(f), loc.clone()), ])) .expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Reading)) - .class(crate::data_class!(Holder)) - .fun(crate::fun!(holder_new)), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading)) + .class(crate::data_class!(Holder)) + .fun(crate::fun!(holder_new)), + ); let dir = unique_test_dir("sealed_vec_field"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .map(|g| g.write_rust(dir.join("g.rs"))); }; @@ -734,24 +743,25 @@ fn recursive_sum_shapes_fail_deterministically() { unimplemented!() } ); - let registry = Registry::::from_items(declare_referenced(vec![ + let registry = crate::api::test_util::reg_from_items(declare_referenced(vec![ (syn::Item::Enum(e), loc.clone()), (syn::Item::Struct(st), loc.clone()), (syn::Item::Fn(f), loc.clone()), ])) .expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Node)) - .class(crate::data_class!(Holder)) - .fun(crate::fun!(holder_new)), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Node)) + .class(crate::data_class!(Holder)) + .fun(crate::fun!(holder_new)), + ); let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - registry - .resolve(jni) + jni.build_with(registry) .map(|g| g.write_rust(dir.join("g.rs"))) .map(|_| ()) .map_err(|e| e.to_string()) @@ -880,26 +890,28 @@ fn sum_returns(tag: &str) -> (String, String) { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::enum_class!(Priority)) - .class(crate::sealed_class!(Reading)) - .class(crate::sealed_class!(Lookup)) - .class(crate::ptr_class!(Probe)) - .fun(crate::fun!(read_one)) - .fun(crate::fun!(read_maybe)) - .fun(crate::fun!(read_all)) - .fun(crate::fun!(look_up)) - .fun(crate::fun!(read_each)) - .fun(crate::fun!(read_borrowed)) - .fun(crate::fun!(read_borrowed_maybe)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::enum_class!(Priority)) + .class(crate::sealed_class!(Reading)) + .class(crate::sealed_class!(Lookup)) + .class(crate::ptr_class!(Probe)) + .fun(crate::fun!(read_one)) + .fun(crate::fun!(read_maybe)) + .fun(crate::fun!(read_all)) + .fun(crate::fun!(look_up)) + .fun(crate::fun!(read_each)) + .fun(crate::fun!(read_borrowed)) + .fun(crate::fun!(read_borrowed_maybe)), + ); let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let kotlin = gen @@ -1197,18 +1209,20 @@ fn a_data_class_field_may_be_a_sum_carrying_a_handle() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::ptr_class!(Probe)) - .class(crate::sealed_class!(Lookup)) - .class(crate::data_class!(Holder)) - .fun(crate::fun!(holder_new)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(Probe)) + .class(crate::sealed_class!(Lookup)) + .class(crate::data_class!(Holder)) + .fun(crate::fun!(holder_new)), + ); let dir = unique_test_dir("jnigen_sum_handle_field"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); let kotlin = gen @@ -1290,18 +1304,20 @@ fn two_sum_callback_args_keep_their_own_selectors() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Reading)) - .class(crate::sealed_class!(Lookup)) - .class(crate::ptr_class!(Probe)) - .fun(crate::fun!(read_pair)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading)) + .class(crate::sealed_class!(Lookup)) + .class(crate::ptr_class!(Probe)) + .fun(crate::fun!(read_pair)), + ); let dir = unique_test_dir("jnigen_two_sum_cb"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let kotlin = gen .write_kotlin(&dir.join("kotlin")) .expect("write_kotlin") @@ -1370,15 +1386,17 @@ fn sum_in_result_ok_position_is_rejected_with_its_reason() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Reading)) - .class(crate::ptr_class!(Probe)) - .fun(crate::fun!(read_try)), - ); - let err = registry - .resolve(jni) + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading)) + .class(crate::ptr_class!(Probe)) + .fun(crate::fun!(read_try)), + ); + let err = jni + .build_with(registry) .expect_err("must be rejected") .to_string(); assert!( @@ -1421,14 +1439,16 @@ fn undeclared_sum_in_result_error_position_is_rejected() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Reading)) - .fun(crate::fun!(read_try)), - ); - let err = registry - .resolve(jni) + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading)) + .fun(crate::fun!(read_try)), + ); + let err = jni + .build_with(registry) .expect_err("must be rejected") .to_string(); assert!( @@ -1474,14 +1494,16 @@ fn the_diagnostic_names_the_whole_error_type_where_it_must() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Reading)) - .fun(crate::fun!(read_try)), - ); - let err = registry - .resolve(jni) + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading)) + .fun(crate::fun!(read_try)), + ); + let err = jni + .build_with(registry) .expect_err("must be rejected") .to_string(); let compact: String = err.split_whitespace().collect(); @@ -1531,8 +1553,8 @@ fn declared_sum_in_result_error_position_resolves() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .expand(crate::expand_return!(Reading).field(crate::fun!(reading_code))) .package( @@ -1540,8 +1562,7 @@ fn declared_sum_in_result_error_position_resolves() { .class(crate::sealed_class!(Reading)) .fun(crate::fun!(read_try)), ); - registry - .resolve(jni) + jni.build_with(registry) .expect("a declared error deconstructor is the supported shape"); } @@ -1576,14 +1597,16 @@ fn slice_of_sum_callback_arg_is_rejected_with_its_reason() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::sealed_class!(Reading)) - .fun(crate::fun!(read_batch)), - ); - let err = registry - .resolve(jni) + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading)) + .fun(crate::fun!(read_batch)), + ); + let err = jni + .build_with(registry) .expect_err("must be rejected") .to_string(); assert!( diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs b/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs index 23274500..85bb0abc 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/snapshots.rs @@ -44,9 +44,9 @@ fn snapshot_pipeline() -> (String, std::collections::BTreeMap) { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -64,7 +64,7 @@ fn snapshot_pipeline() -> (String, std::collections::BTreeMap) { let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); @@ -244,8 +244,8 @@ fn handler_interfaces_carry_split_contract_kdoc() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("ops").fun(crate::fun!(z_fallible))) .expand(crate::expand_return!(ZErr).field(crate::fun!(z_err_message).name("message"))); @@ -253,7 +253,7 @@ fn handler_interfaces_carry_split_contract_kdoc() { let dir = unique_test_dir("jnigen_handler_kdoc"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let typed = paths @@ -305,19 +305,21 @@ fn box_string_field_maps_to_nullable_kotlin_string() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("payload") - .class(crate::data_class!(Payload)) - .fun(crate::fun!(payload_get)) - .fun(crate::fun!(payload_put)), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("payload") + .class(crate::data_class!(Payload)) + .fun(crate::fun!(payload_get)) + .fun(crate::fun!(payload_put)), + ); let dir = unique_test_dir("jnigen_boxstr"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -383,19 +385,21 @@ fn slice_input_builds_vec_handle() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("foo") - .class(crate::data_class!(Foo)) - .fun(crate::fun!(put_slice)) - .fun(crate::fun!(put_vec)), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("foo") + .class(crate::data_class!(Foo)) + .fun(crate::fun!(put_slice)) + .fun(crate::fun!(put_vec)), + ); let dir = unique_test_dir("jnigen_slice_vec_handle"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -505,9 +509,9 @@ fn native_symbols_are_jni_escaped() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.example.my_pkg") .set_harness_name_mangle(|_| "Native_Harness".to_string()) .package( @@ -521,7 +525,7 @@ fn native_symbols_are_jni_escaped() { let dir = unique_test_dir("jnigen_symbol_escaping"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -574,9 +578,9 @@ fn jni_native_init_emits_init_block() { loc.clone(), )]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_jni_native_init("io.test.jni.NativeLibrary.ensureLoaded()") .package(crate::package!("thing").fun(crate::fun!(z_ping))); @@ -584,7 +588,7 @@ fn jni_native_init_emits_init_block() { let dir = unique_test_dir("jnigen_native_init"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); let native = paths diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs b/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs index 19817a46..ab87c383 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/symbols.rs @@ -4,13 +4,18 @@ //! surfaced before any file is written. use super::*; +use crate::api::core::registry::RegistryBuilder; /// Resolve the binding and return the result — `validate_resolved` (and thus /// `validate_symbols`) now runs inside `resolve`, so an invalid binding fails -/// here and no `Generation` is produced (nothing can be written). On success, +/// here and no `JniGen` is produced (nothing can be written). On success, /// a real `write_rust` confirms the valid binding also emits. -fn resolve_result(tag: &str, registry: Registry, jni: JniGen) -> Result<(), String> { - match registry.resolve(jni) { +fn resolve_result( + tag: &str, + registry: RegistryBuilder, + jni: JniGenBuilder, +) -> Result<(), String> { + match jni.build_with(registry) { Ok(gen) => { let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); @@ -23,10 +28,13 @@ fn resolve_result(tag: &str, registry: Registry, jni: JniGen) -> Res } } -fn one_fn(src: &str) -> Registry { +fn one_fn(src: &str) -> RegistryBuilder { let f: syn::ItemFn = syn::parse_str(src).unwrap(); - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), myflat_loc())])) - .expect("index") + crate::api::test_util::reg_from_items(declare_referenced(vec![( + syn::Item::Fn(f), + myflat_loc(), + )])) + .expect("index") } /// A `.name()` override that isn't a legal Kotlin identifier is a hard error @@ -34,7 +42,7 @@ fn one_fn(src: &str) -> Registry { #[test] fn invalid_name_override_is_error() { let registry = one_fn("pub fn z_do_thing() -> i64 { 0 }"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_do_thing).name("when"))); let err = resolve_result("jni_sym_name", registry, jni).expect_err("invalid .name()"); @@ -47,7 +55,7 @@ fn invalid_name_override_is_error() { #[test] fn invalid_hook_output_is_error() { let registry = one_fn("pub fn z_do_thing() -> i64 { 0 }"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_fun_name_mangle(|_pkg, _name| "1bad".to_string()) .package(crate::package!("thing").fun(crate::fun!(z_do_thing))); @@ -62,7 +70,7 @@ fn invalid_hook_output_is_error() { #[test] fn valid_default_names_pass() { let registry = one_fn("pub fn z_do_thing() -> i64 { 0 }"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("thing").fun(crate::fun!(z_do_thing))); resolve_result("jni_sym_ok", registry, jni).expect("valid names must pass"); @@ -83,11 +91,11 @@ fn duplicate_native_symbol_is_error() { myflat_loc(), ), ]; - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); // The JNINative extern method name (which the `Java_…` symbol derives // from) goes through the method hook; collapsing it onto one name for // every function forces two distinct fns to share a native symbol. - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_method_name_mangle(|_pkg, _class, _name| "collide".to_string()) .package( @@ -120,17 +128,19 @@ fn keyword_struct_field_is_sanitized_not_error() { myflat_loc(), ), ]; - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .class(crate::data_class!(Payload)) - .fun(crate::fun!(make)), - ); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .class(crate::data_class!(Payload)) + .fun(crate::fun!(make)), + ); // The keyword field is sanitized (mangle → `object_`), not rejected. let dir = unique_test_dir("jni_sym_field"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")) .expect("keyword field sanitized, not an error"); let paths = gen.write_kotlin(&dir.join("kotlin")).expect("write_kotlin"); @@ -155,7 +165,7 @@ fn keyword_struct_field_is_sanitized_not_error() { #[test] fn class_interface_collision_is_error() { let registry = one_fn("pub fn z_thing_new() -> ZThing { unimplemented!() }"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .set_interface_name_mangle(|_pkg, n| n.to_string()) // identity → iface == class .package( @@ -187,13 +197,15 @@ fn same_name_same_signature_functions_collide() { myflat_loc(), ), ]; - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); // Both forced to Kotlin name `combine`; both take one `Long` → same sig. - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .fun(crate::fun!(z_alpha).name("combine")) - .fun(crate::fun!(z_beta).name("combine")), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .fun(crate::fun!(z_alpha).name("combine")) + .fun(crate::fun!(z_beta).name("combine")), + ); let err = resolve_result("jni_ov_collide", registry, jni).expect_err("overload clash"); assert!(err.contains("conflicting Kotlin overload"), "{err}"); assert!(err.contains("combine"), "{err}"); @@ -213,13 +225,15 @@ fn same_name_distinct_signature_functions_allowed() { myflat_loc(), ), ]; - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); // Same name `combine`, but one takes Long and the other Boolean. - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .fun(crate::fun!(z_alpha).name("combine")) - .fun(crate::fun!(z_beta).name("combine")), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .fun(crate::fun!(z_alpha).name("combine")) + .fun(crate::fun!(z_beta).name("combine")), + ); resolve_result("jni_ov_ok", registry, jni).expect("distinct signatures are valid overloads"); } @@ -237,14 +251,16 @@ fn method_and_factory_same_name_do_not_collide() { myflat_loc(), ), ]; - let registry = Registry::::from_items(declare_referenced(items)).expect("index"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing").class( - crate::ptr_class!(Thing) - .method(crate::fun!(thing_size).name("of")) - .constructor(crate::fun!(thing_make).name("of")), - ), - ); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing").class( + crate::ptr_class!(Thing) + .method(crate::fun!(thing_size).name("of")) + .constructor(crate::fun!(thing_make).name("of")), + ), + ); // Instance method `of()` and companion factory `of()` are distinct scopes. resolve_result("jni_ov_scopes", registry, jni).expect("method vs factory don't collide"); } diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index eb0ae712..255371c0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -71,12 +71,12 @@ fn value_form_items() -> Vec<(syn::Item, crate::SourceLocation)> { ] } -/// Build the fixture through `JniGen`, letting the caller adjust the +/// Build the fixture through `JniGenBuilder`, letting the caller adjust the /// `ZSample` boundary decl. Returns the generated Rust + the joined Kotlin. fn value_form_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> (String, String) { - let registry = Registry::::from_items(declare_referenced(value_form_items())) + let registry = crate::api::test_util::reg_from_items(declare_referenced(value_form_items())) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -95,7 +95,7 @@ fn value_form_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> (String, St let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); let kotlin = gen @@ -219,8 +219,8 @@ fn deriving_matches_the_equivalent_hand_written_list() { let mut all = items.clone(); all.extend(extra); let registry = - Registry::::from_items(declare_referenced(all)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(all)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -233,7 +233,7 @@ fn deriving_matches_the_equivalent_hand_written_list() { ) .expand(crate::expand_return!(ZKeyExpr).field(crate::fun!(z_keyexpr_as_str))) .expand(decl); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.registry() .callback_arg_plans .values() @@ -396,8 +396,8 @@ fn sum_field_gen(tag: &str) -> (String, String) { loc, )); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -411,7 +411,7 @@ fn sum_field_gen(tag: &str) -> (String, String) { let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); let kotlin = gen @@ -494,8 +494,8 @@ fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -507,8 +507,8 @@ fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { let dir = unique_test_dir("jnigen_vf_sum_reject"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .map(|g| g.write_rust(dir.join("g.rs"))); }; @@ -536,9 +536,10 @@ fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { #[test] fn an_adjustment_naming_an_unknown_field_is_an_error() { let build = |decl: crate::lang::FieldsDecl| { - let registry = Registry::::from_items(declare_referenced(value_form_items())) - .expect("index"); - let jni = JniGen::new() + let registry = + crate::api::test_util::reg_from_items(declare_referenced(value_form_items())) + .expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -554,8 +555,8 @@ fn an_adjustment_naming_an_unknown_field_is_an_error() { let dir = unique_test_dir("jnigen_vf_unknown"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .map(|g| g.write_rust(dir.join("g.rs"))); }; @@ -645,8 +646,8 @@ fn a_single_leaf_value_form_delivers_an_owned_field() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -657,7 +658,7 @@ fn a_single_leaf_value_form_delivers_an_owned_field() { let dir = unique_test_dir("jnigen_vf_single"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -705,8 +706,8 @@ fn a_single_leaf_consuming_value_form_moves_its_field() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -717,7 +718,7 @@ fn a_single_leaf_consuming_value_form_moves_its_field() { let dir = unique_test_dir("jnigen_vf_single_consume"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -771,8 +772,8 @@ fn a_handle_field_of_a_consuming_value_form_moves() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -788,7 +789,7 @@ fn a_handle_field_of_a_consuming_value_form_moves() { let dir = unique_test_dir("jnigen_vf_handle_field_consume"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -842,8 +843,8 @@ fn a_sole_handle_field_of_a_consuming_value_form_moves() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -859,7 +860,7 @@ fn a_sole_handle_field_of_a_consuming_value_form_moves() { let dir = unique_test_dir("jnigen_vf_sole_handle_consume"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -914,8 +915,8 @@ fn an_optional_handle_field_of_a_consuming_value_form_moves() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -931,7 +932,7 @@ fn an_optional_handle_field_of_a_consuming_value_form_moves() { let dir = unique_test_dir("jnigen_vf_optional_handle_consume"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -985,8 +986,8 @@ fn a_sole_optional_handle_field_takes_callback_delivery() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1002,7 +1003,7 @@ fn a_sole_optional_handle_field_takes_callback_delivery() { let dir = unique_test_dir("jnigen_vf_sole_optional_handle"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1052,8 +1053,8 @@ fn an_owned_root_identity_moves_without_any_value_form() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1065,7 +1066,7 @@ fn an_owned_root_identity_moves_without_any_value_form() { let dir = unique_test_dir("jnigen_vf_root_identity"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1084,9 +1085,10 @@ fn an_owned_root_identity_moves_without_any_value_form() { #[test] fn a_per_field_override_must_name_the_field_s_own_type() { let build = || { - let registry = Registry::::from_items(declare_referenced(value_form_items())) - .expect("index"); - let jni = JniGen::new() + let registry = + crate::api::test_util::reg_from_items(declare_referenced(value_form_items())) + .expect("index"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1108,8 +1110,8 @@ fn a_per_field_override_must_name_the_field_s_own_type() { let dir = unique_test_dir("jnigen_vf_ovr_ty"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .map(|g| g.write_rust(dir.join("g.rs"))); }; let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(build)) @@ -1181,8 +1183,8 @@ fn a_nested_value_form_is_hoisted_too() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1195,7 +1197,7 @@ fn a_nested_value_form_is_hoisted_too() { let dir = unique_test_dir("jnigen_vf_nested"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); let kotlin = gen @@ -1297,9 +1299,9 @@ fn a_nested_consuming_value_form_moves_the_parent_s_field() { ), ] { let registry = - Registry::::from_items(declare_referenced(items(outer_by_value))) + crate::api::test_util::reg_from_items(declare_referenced(items(outer_by_value))) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1314,7 +1316,7 @@ fn a_nested_consuming_value_form_moves_the_parent_s_field() { let dir = unique_test_dir(&format!("jnigen_vf_nested_consume_{tag}")); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1384,8 +1386,8 @@ fn nested_review_items() -> Vec<(syn::Item, crate::SourceLocation)> { ] } -fn nested_review_jni(outer: crate::lang::ExpandReturnDecl) -> JniGen { - JniGen::new() +fn nested_review_jni(outer: crate::lang::ExpandReturnDecl) -> JniGenBuilder { + JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1407,12 +1409,12 @@ fn nested_review_jni(outer: crate::lang::ExpandReturnDecl) -> JniGen { /// `Option` it cannot unwrap. #[test] fn an_optional_nested_value_form_is_rejected_before_emission() { - let registry = Registry::::from_items(declare_referenced(nested_review_items())) + let registry = crate::api::test_util::reg_from_items(declare_referenced(nested_review_items())) .expect("index items"); let jni = nested_review_jni( crate::expand_return!(ZReviewOuter).fields(crate::fields!(z_review_outer_to_struct)), ); - let err = match registry.resolve(jni) { + let err = match jni.build_with(registry) { Ok(_) => panic!("an optional nested value form must be rejected"), Err(e) => e, }; @@ -1452,8 +1454,8 @@ fn a_value_form_under_an_optional_accessor_is_hoisted_conditionally() { ), ]); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1466,7 +1468,9 @@ fn a_value_form_under_an_optional_accessor_is_hoisted_conditionally() { let dir = unique_test_dir("jnigen_vf_conditional"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("a conditional hoist resolves"); + let gen = jni + .build_with(registry) + .expect("a conditional hoist resolves"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1529,8 +1533,8 @@ fn conditional_owned_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> Stri ), ]); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1543,7 +1547,7 @@ fn conditional_owned_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> Stri let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust") } @@ -1584,8 +1588,8 @@ fn an_owned_optional_payload_is_borrowed_for_the_steps_after_it() { ), ]); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1600,7 +1604,7 @@ fn an_owned_optional_payload_is_borrowed_for_the_steps_after_it() { let dir = unique_test_dir("jnigen_vf_cond_owned_chain"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1656,8 +1660,8 @@ fn a_rebased_hoist_projects_its_leading_fields_past_a_sibling_move() { ), ]); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1672,7 +1676,7 @@ fn a_rebased_hoist_projects_its_leading_fields_past_a_sibling_move() { let dir = unique_test_dir("jnigen_vf_sibling_move"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1719,8 +1723,8 @@ fn a_consuming_value_form_keeps_its_by_value_boundary_behind_accessors() { ), ]); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1733,7 +1737,7 @@ fn a_consuming_value_form_keeps_its_by_value_boundary_behind_accessors() { let dir = unique_test_dir("jnigen_vf_consume_behind_acc"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1787,8 +1791,8 @@ fn an_owned_intermediate_result_is_borrowed_for_the_next_step() { ), ]); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1805,7 +1809,7 @@ fn an_owned_intermediate_result_is_borrowed_for_the_next_step() { let dir = unique_test_dir("jnigen_vf_cond_owned_middle"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1901,8 +1905,8 @@ fn a_sum_field_of_a_conditional_value_form_stays_inside_the_arm() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -1916,7 +1920,7 @@ fn a_sum_field_of_a_conditional_value_form_stays_inside_the_arm() { let dir = unique_test_dir("jnigen_vf_conditional_sum"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); @@ -1954,7 +1958,7 @@ fn a_sum_field_of_a_conditional_value_form_stays_inside_the_arm() { fn a_vec_field_override_must_name_the_whole_vec_type() { let build = || { let registry = - Registry::::from_items(declare_referenced(nested_review_items())) + crate::api::test_util::reg_from_items(declare_referenced(nested_review_items())) .expect("index items"); let jni = nested_review_jni( crate::expand_return!(ZReviewOuter).fields( @@ -1962,7 +1966,7 @@ fn a_vec_field_override_must_name_the_whole_vec_type() { .field("items", crate::expand_return!(ZReviewInner).field_self()), ), ); - let _ = registry.resolve(jni); + let _ = jni.build_with(registry); }; let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(build)) @@ -2024,9 +2028,9 @@ fn consuming_items() -> Vec<(syn::Item, crate::SourceLocation)> { } fn consuming_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> String { - let registry = Registry::::from_items(declare_referenced(consuming_items())) + let registry = crate::api::test_util::reg_from_items(declare_referenced(consuming_items())) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -2037,7 +2041,7 @@ fn consuming_gen(tag: &str, decl: crate::lang::ExpandReturnDecl) -> String { let dir = unique_test_dir(tag); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust") } @@ -2102,8 +2106,8 @@ fn a_borrowed_plan_clones_before_consuming() { loc, )); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -2116,7 +2120,7 @@ fn a_borrowed_plan_clones_before_consuming() { let dir = unique_test_dir("jnigen_vf_consume_ref"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(gen.write_rust(dir.join("gen.rs")).expect("write_rust")) .expect("read rust"); assert!( @@ -2167,9 +2171,9 @@ fn a_consuming_value_form_rejects_a_plain_field_sibling() { #[test] fn the_declarator_and_the_accessor_s_receiver_must_agree() { let build = |decl: crate::lang::ExpandReturnDecl| -> String { - let registry = Registry::::from_items(declare_referenced(consuming_items())) + let registry = crate::api::test_util::reg_from_items(declare_referenced(consuming_items())) .expect("index"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!() @@ -2177,7 +2181,7 @@ fn the_declarator_and_the_accessor_s_receiver_must_agree() { .fun(crate::fun!(zc_sub)), ) .expand(decl); - match registry.resolve(jni) { + match jni.build_with(registry) { Ok(_) => String::new(), Err(e) => e.to_string(), } diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs index 99758e22..263d2bc8 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs @@ -16,8 +16,8 @@ fn bounded_duration_option_uses_u64_niche_without_boxing() { (item, loc.clone()) }) .collect(); - let registry = Registry::::from_items(declare_referenced(items)).unwrap(); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).unwrap(); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert( crate::convert!(Duration) @@ -29,7 +29,7 @@ fn bounded_duration_option_uses_u64_niche_without_boxing() { let dir = unique_test_dir("jnigen_bounded_duration"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).unwrap(); + let generation = jni.build_with(registry).unwrap(); let rust_path = generation.write_rust(dir.join("gen.rs")).unwrap(); let rust = std::fs::read_to_string(rust_path).unwrap(); let paths = generation.write_kotlin(&dir.join("kotlin")).unwrap(); @@ -109,8 +109,8 @@ fn flattened_field_composes_bounded_conversion_stages() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert( crate::convert!(Duration) @@ -127,7 +127,7 @@ fn flattened_field_composes_bounded_conversion_stages() { let dir = unique_test_dir("jnigen_flat_staged_field"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).expect("resolve"); + let generation = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap(); let kotlin = generation .write_kotlin(&dir.join("kotlin")) @@ -178,17 +178,17 @@ fn duration_requires_an_explicit_conversion() { let function: syn::ItemFn = syn::parse_str("pub fn duration_echo(v: Duration) -> Duration { unimplemented!() }") .unwrap(); - let registry = Registry::::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (alias, myflat_loc()), (syn::Item::Fn(function), myflat_loc()), ])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!("time").fun(crate::fun!(duration_echo))); - let error = registry - .resolve(jni) + let error = jni + .build_with(registry) .expect_err("Duration must not have an implicit unchecked converter") .to_string(); assert!(error.contains("Duration"), "{error}"); @@ -209,8 +209,8 @@ fn conversion_domain_must_match_the_representation() { (item, loc.clone()) }) .collect(); - let registry = Registry::::from_items(declare_referenced(items)).unwrap(); - let jni = JniGen::new() + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).unwrap(); + let jni = JniGenBuilder::new() .convert( crate::convert!(Duration) .input(crate::fun!(duration_from_millis)) @@ -218,7 +218,7 @@ fn conversion_domain_must_match_the_representation() { ) .package(crate::package!("time").fun(crate::fun!(duration_use))); - let _ = registry.resolve(jni); + let _ = jni.build_with(registry); } /// Phase 4: a bare `Option` / `Option` **input** parameter @@ -251,9 +251,9 @@ fn option_scalar_param_crosses_as_present_value_pair() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package(crate::package!().class(crate::enum_class!(Mode))) .package(crate::package!("cfg").fun(crate::fun!(z_set_timeout))); @@ -261,7 +261,7 @@ fn option_scalar_param_crosses_as_present_value_pair() { let dir = unique_test_dir("jnigen_optscalar"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -349,19 +349,21 @@ fn vec_of_handle_output_folds_kotlin_side() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("thing") - .class(crate::ptr_class!(ZThing)) - .fun(crate::fun!(thing_list)) - .fun(crate::fun!(thing_list_opt)), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("thing") + .class(crate::ptr_class!(ZThing)) + .fun(crate::fun!(thing_list)) + .fun(crate::fun!(thing_list_opt)), + ); let dir = unique_test_dir("jnigen_vec_handle_out"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -433,18 +435,20 @@ fn option_scalar_struct_field_flattens() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::data_class!(Opts)) - .fun(crate::fun!(opts_put)), - ); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::data_class!(Opts)) + .fun(crate::fun!(opts_put)), + ); let dir = unique_test_dir("jnigen_optfield"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -549,9 +553,9 @@ fn recursive_data_class_input_flattens_nested_and_optional_fields() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .package( crate::package!("model") @@ -568,7 +572,7 @@ fn recursive_data_class_input_flattens_nested_and_optional_fields() { let dir = unique_test_dir("jnigen_fromparts_optbox"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -666,19 +670,21 @@ fn jobject_input_is_an_explicit_hybrid_leaf_escape_hatch() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::data_class!(FlatChild)) - .class(crate::data_class!(ObjectChild).jobject_input()) - .class(crate::data_class!(Hybrid)) - .fun(crate::fun!(hybrid_use)) - .fun(crate::fun!(hybrid_optional)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::data_class!(FlatChild)) + .class(crate::data_class!(ObjectChild).jobject_input()) + .class(crate::data_class!(Hybrid)) + .fun(crate::fun!(hybrid_use)) + .fun(crate::fun!(hybrid_optional)), + ); let dir = unique_test_dir("jnigen_hybrid_jobject_input"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).expect("resolve"); + let generation = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap(); let kotlin = generation .write_kotlin(&dir.join("kotlin")) @@ -740,17 +746,19 @@ fn recursive_flattened_owned_handles_join_lock_and_consume_scaffold() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::ptr_class!(Token)) - .class(crate::data_class!(Envelope)) - .fun(crate::fun!(envelope_use)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(Token)) + .class(crate::data_class!(Envelope)) + .fun(crate::fun!(envelope_use)), + ); let dir = unique_test_dir("jnigen_recursive_handles"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).expect("resolve"); + let generation = jni.build_with(registry).expect("resolve"); let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap(); let kotlin = generation .write_kotlin(&dir.join("kotlin")) @@ -796,18 +804,18 @@ fn recursive_flattening_rejects_jvm_parameter_slot_overflow() { } ); let loc = myflat_loc(); - let registry = Registry::::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(wide.clone()), loc.clone()), (syn::Item::Fn(use_wide.clone()), loc.clone()), ])) .expect("index items"); - let jni = JniGen::new().package( + let jni = JniGenBuilder::new().package( crate::package!() .class(crate::data_class!(Wide)) .fun(crate::fun!(use_wide)), ); - let error = registry - .resolve(jni) + let error = jni + .build_with(registry) .expect_err("256 JVM slots must fail") .to_string(); assert!(error.contains("uses 256 JVM parameter slots"), "{error}"); @@ -816,18 +824,18 @@ fn recursive_flattening_rejects_jvm_parameter_slot_overflow() { // The explicit object boundary keeps the same public Kotlin data class, // but the native method receives it in one slot and performs the legacy // whole-object field decode instead of producing an illegal signature. - let registry = Registry::::from_items(declare_referenced([ + let registry = crate::api::test_util::reg_from_items(declare_referenced([ (syn::Item::Struct(wide), loc.clone()), (syn::Item::Fn(use_wide), loc), ])) .expect("index marked items"); - let jni = JniGen::new().package( + let jni = JniGenBuilder::new().package( crate::package!() .class(crate::data_class!(Wide).jobject_input()) .fun(crate::fun!(use_wide)), ); - let generation = registry - .resolve(jni) + let generation = jni + .build_with(registry) .expect("JObject boundary must bypass the flattened slot limit"); assert!(generation.report().contains("input `JObject` opt-in")); } @@ -852,16 +860,16 @@ fn output_only_convert_resolves_without_input_twin() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert(crate::convert!(Len).output(crate::fun!(len_value))) .package(crate::package!("len").fun(crate::fun!(len_of))); let dir = unique_test_dir("jnigen_outonly_convert"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry - .resolve(jni) + let gen = jni + .build_with(registry) .expect("an output-only convert type must not require an input twin"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); @@ -897,16 +905,16 @@ fn convert_fn_qualifies_with_origin_crate() { "my-helpers", )]; let registry = - Registry::::from_items(declare_referenced(flat.into_iter().chain(helpers))) + crate::api::test_util::reg_from_items(declare_referenced(flat.into_iter().chain(helpers))) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert(crate::convert!(Len).output(crate::fun!(len_value))) .package(crate::package!("len").fun(crate::fun!(len_of))); let dir = unique_test_dir("jnigen_convert_origin"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -934,15 +942,15 @@ fn convert_input_target_mismatch_rejected() { }) .collect(); let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new() + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() .convert(crate::convert!(Len).input(crate::fun!(from_long))) .package(crate::package!("len").fun(crate::fun!(use_len))); let dir = unique_test_dir("jnigen_convert_mismatch"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = registry - .resolve(jni) + let _ = jni + .build_with(registry) .and_then(|gen| gen.write_rust(dir.join("gen.rs"))); } @@ -955,9 +963,9 @@ fn convert_via_trait_impls() { let f: syn::ItemFn = syn::parse_str("pub fn temp_double(c: Celsius) -> Celsius { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert( crate::convert!(Celsius) @@ -968,7 +976,7 @@ fn convert_via_trait_impls() { let dir = unique_test_dir("jnigen_convert_trait"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -991,16 +999,16 @@ fn convert_via_try_from_is_fallible() { let f: syn::ItemFn = syn::parse_str("pub fn pct_use(p: Percent) -> i32 { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert(crate::convert!(Percent).input(crate::try_from!(i32))) .package(crate::package!("m").fun(crate::fun!(pct_use))); let dir = unique_test_dir("jnigen_convert_tryfrom"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -1026,9 +1034,9 @@ fn option_composition_normalizes_fallible_stage_errors() { ) .unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert( crate::convert!(Percent) @@ -1042,7 +1050,7 @@ fn option_composition_normalizes_fallible_stage_errors() { let dir = unique_test_dir("jnigen_option_fallible_stages"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -1064,9 +1072,9 @@ fn convert_via_local_fns() { let f: syn::ItemFn = syn::parse_str("pub fn label_id(l: Label) -> Label { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert( crate::convert!(Label) @@ -1077,7 +1085,7 @@ fn convert_via_local_fns() { let dir = unique_test_dir("jnigen_convert_local"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -1133,9 +1141,9 @@ fn convert_via_local_try_fn_is_fallible() { let f: syn::ItemFn = syn::parse_str("pub fn label_id(l: Label) -> Label { unimplemented!() }").unwrap(); let registry = - Registry::::from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) + crate::api::test_util::reg_from_items(declare_referenced(vec![(syn::Item::Fn(f), loc)])) .expect("index items"); - let jni = JniGen::new() + let jni = JniGenBuilder::new() .set_package_prefix("io.test.jni") .convert( crate::convert!(Label) @@ -1149,7 +1157,7 @@ fn convert_via_local_try_fn_is_fallible() { let dir = unique_test_dir("jnigen_convert_local_try"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); let rust_path = gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let rust = std::fs::read_to_string(&rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); @@ -1192,18 +1200,20 @@ fn data_class_members_reenter_as_field_leaves() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!().class( - crate::data_class!(Point) - .method(crate::fun!(point_norm).name("norm")) - .constructor(crate::fun!(point_origin).name("origin")), - ), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!().class( + crate::data_class!(Point) + .method(crate::fun!(point_norm).name("norm")) + .constructor(crate::fun!(point_origin).name("origin")), + ), + ); let dir = unique_test_dir("jnigen_data_members"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let gen = registry.resolve(jni).expect("resolve"); + let gen = jni.build_with(registry).expect("resolve"); gen.write_rust(dir.join("gen.rs")).expect("write_rust"); let kdir = dir.join("kotlin"); @@ -1292,19 +1302,21 @@ fn unsigned_scalars_use_lossless_kotlin_surface_and_raw_jni_wires() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::data_class!(Unsigned)) - .fun(crate::fun!(unsigned_round_trip)) - .fun(crate::fun!(unsigned_data_maybe)) - .fun(crate::fun!(unsigned_callback)) - .fun(crate::fun!(unsigned_result)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::data_class!(Unsigned)) + .fun(crate::fun!(unsigned_round_trip)) + .fun(crate::fun!(unsigned_data_maybe)) + .fun(crate::fun!(unsigned_callback)) + .fun(crate::fun!(unsigned_result)), + ); let dir = unique_test_dir("jnigen_unsigned"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).expect("resolve"); + let generation = jni.build_with(registry).expect("resolve"); let rust_path = generation .write_rust(dir.join("gen.rs")) .expect("write_rust"); @@ -1421,21 +1433,23 @@ fn data_class_properties_match_their_from_parts_params() { ), ]; let registry = - Registry::::from_items(declare_referenced(items)).expect("index items"); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!() - .class(crate::ptr_class!(Handle)) - .class(crate::data_class!(Child)) - .class(crate::enum_class!(Level)) - .class(crate::data_class!(Bag)) - .fun(crate::fun!(bag_make)) - .fun(crate::fun!(bag_take)), - ); + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(Handle)) + .class(crate::data_class!(Child)) + .class(crate::enum_class!(Level)) + .class(crate::data_class!(Bag)) + .fun(crate::fun!(bag_make)) + .fun(crate::fun!(bag_take)), + ); let dir = unique_test_dir("jnigen_data_class_props"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).expect("resolve"); + let generation = jni.build_with(registry).expect("resolve"); let kotlin = generation .write_kotlin(&dir.join("kotlin")) .unwrap() @@ -1465,7 +1479,7 @@ fn data_class_properties_match_their_from_parts_params() { /// and a `const fn` CALL — is qualified against its origin module, and that /// rewrite reaches ONLY the length, never a converter body's locals. /// -/// None of the three owners is declared to JniGen: each is a compile-time +/// None of the three owners is declared to JniGenBuilder: each is a compile-time /// namespace, not a boundary type, so qualification must not depend on a /// Kotlin class existing for it. /// @@ -1517,16 +1531,18 @@ fn check_array_length_qualification(loc: SourceLocation, module: &str) { )), loc.clone(), )); - let registry = Registry::::from_items(declare_referenced(items)).unwrap(); - let jni = JniGen::new().set_package_prefix("io.test.jni").package( - crate::package!("blob") - .class(crate::data_class!(Blob)) - .fun(crate::fun!(blob_echo)), - ); + let registry = crate::api::test_util::reg_from_items(declare_referenced(items)).unwrap(); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("blob") + .class(crate::data_class!(Blob)) + .fun(crate::fun!(blob_echo)), + ); let dir = unique_test_dir(&format!("jnigen_array_len_const_{module}")); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let generation = registry.resolve(jni).unwrap(); + let generation = jni.build_with(registry).unwrap(); let rust_path = generation.write_rust(dir.join("gen.rs")).unwrap(); let rust = std::fs::read_to_string(rust_path).unwrap(); let rc: String = rust.split_whitespace().collect(); diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 80211279..cc1f4e67 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -1,10 +1,11 @@ -//! [`Prebindgen`] implementation for [`JniGen`] plus its converter- +//! [`Prebindgen`] implementation for [`JniGenBuilder`] plus its converter- //! selector / exception-routing helpers. //! //! Carved from the former monolithic JNI module; shares the `jni` //! namespace via `use super::*`. use super::*; +use crate::api::core::registry::{Building, Conversions, Crossing, RegistryBuilder}; /// The `#[allow(...)]` carried by every generated converter `fn`. /// @@ -35,7 +36,7 @@ fn generated_converter_attr() -> syn::Attribute { // and consuming-crate wrapper exts like ZenohJniExt). // ────────────────────────────────────────────────────────────────────── -impl JniGen { +impl JniGenBuilder { /// Build the standard JNI input-converter `fn`. Body assumes in-scope /// `env: &mut JNIEnv` and `v: &` (or `v: ` for raw-pointer /// wires); produces a value of `rust`. Returned function has its name @@ -378,7 +379,7 @@ impl JniGen { // owner is a compile-time namespace, not a boundary type. Requiring it // to be declared would force an otherwise-unused Kotlin class into // existence just to make the generated Rust compile, and would be - // asymmetric with consts, which qualify whether or not JniGen declared + // asymmetric with consts, which qualify whether or not JniGenBuilder declared // them. // EVERY named item the registry indexes. A length is an arbitrary const // expression, so it can name a const, the type owning an associated @@ -547,7 +548,7 @@ pub(crate) fn build_signal_domain_error_item() -> syn::Item { /// whose declare/undeclare fns are `#[cfg]`'d out of the scan) from /// producing destructors that reference types not in scope. pub(crate) fn build_handle_destructor_items( - ext: &JniGen, + ext: &JniGenBuilder, registry: &Registry, ) -> Vec { let mut named: Vec<(String, syn::Item)> = Vec::new(); @@ -613,12 +614,12 @@ pub(crate) fn build_handle_destructor_items( /// Per-shape **input** wrapper converter builders (`&`/`Option<&>`/`Vec`/ /// `Option`). Each returns `Some(ConverterImpl)` only for the wildcard pattern -/// it claims; [`JniGen::input_wrapper_shape`] chains them in priority order. +/// it claims; [`JniGenBuilder::input_wrapper_shape`] chains them in priority order. /// Because [`pat_match`] is an exact match, the patterns are disjoint — except /// the two `Option<_>` sub-cases (direct-handle-by-value vs general), which -/// share a pattern and so live together in [`JniGen::input_option`] to keep +/// share a pattern and so live together in [`JniGenBuilder::input_option`] to keep /// their original fall-through. -impl JniGen { +impl JniGenBuilder { /// `& _` / `& mut _` borrow: share T's resolved converter — `&T`'s entry /// points at the same `ItemFn` (the fn returns owned `T`; the call site in /// `emit_jni_function_wrapper` adds `&decoded`). Exists so the @@ -627,7 +628,7 @@ impl JniGen { &self, pat: &syn::Type, t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { if !(pat_match(pat, "& _") || pat_match(pat, "& mut _")) { return None; @@ -673,7 +674,7 @@ impl JniGen { &self, pat: &syn::Type, t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { if !(pat_match(pat, "Option < & _ >") || pat_match(pat, "Option < & mut _ >")) { return None; @@ -734,7 +735,7 @@ impl JniGen { &self, pat: &syn::Type, t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { if !pat_match(pat, "Vec < _ >") { return None; @@ -798,7 +799,7 @@ impl JniGen { &self, pat: &syn::Type, t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { if pat_match(pat, "Option < _ >") { let inner = registry.input_entry(t1)?; @@ -923,69 +924,167 @@ impl JniGen { // Prebindgen impl // ────────────────────────────────────────────────────────────────────── -impl Prebindgen for JniGen { - /// Cross-language extras every JNI converter carries — currently - /// the Kotlin value-context type name. Filled by the rank-N - /// handlers at the same point they build the wire/body; the - /// resolver propagates it into [`crate::api::core::registry::TypeEntry::metadata`]; - /// the Kotlin emitter reads it back to drive every wrapper / - /// typed-handle / `JNIWrappers` signature. - type Metadata = KotlinMeta; +impl JniGenBuilder { + /// State this binding into `registry`: what it exports, what crosses, and + /// what it defines itself. + /// + /// **Push, not pull.** The registry does not call back to ask — the build + /// script calls this, and the registry stays a passive recorder. That is + /// what makes "a converter reads a half-built registry" unrepresentable + /// rather than merely avoided. + /// + /// Order-independent, and idempotent apart from the local-fn collision + /// check: every method it calls records rather than derives. + /// State this binding into `registry`, then resolve it. + /// + /// The pair is always used together, and the generator is what knows both + /// halves — so it drives, and the registry never calls back. Becomes the + /// body of `generate(..)` once emission moves here too (#251 phase E). + /// Read the source, resolve every crossing, and hand back the binding. + /// + /// Runs the whole pipeline a build script used to run by hand: parse the + /// declared sources into a model, describe this binding over it, answer + /// each crossing in dependency order, and check the set is complete. A + /// `Flat` and a `Registry` exist inside — they are simply not this caller's + /// problem. + pub fn build(self) -> Result { + let flat = self + .sources + .clone() + .build() + .map_err(crate::core::ScanError::from)?; + let registry = crate::core::Registry::builder(flat)?; + self.build_with(registry) + } - // ── Structural type resolution ────────────────────────────────────── - // Try the terminal categories, then the user-wrapper table (`match_user_*`, - // any depth, specificity-ordered), then the built-in wrapper shapes — peel - // `ty`'s outermost layer and dispatch to `{input,output}_wrapper_shape` with - // the reconstructed canonical pattern. `subs` = the captured inner(s). + /// [`Self::build`] over a registry that was described elsewhere. + /// + /// The seam tests use to feed synthetic items without a source directory; + /// `build` is this with the model read from [`Self::source`]. + pub(crate) fn build_with( + self, + registry: crate::api::core::registry::RegistryBuilder, + ) -> Result { + let registry = self + .declare_into(registry)? + .validate_with(&self)? + .convert_with(|crossing, built| self.convert_crossing(crossing, built))? + .build()?; + // Post-resolve invariants, run once here so the writers are pure reads + // and a `JniGen` is valid by construction. + self.validate_resolved(®istry) + .map_err(|message| crate::core::ScanError::AdapterInvariant { message })?; + Ok(JniGen { + gen: self, + registry, + }) + } - fn on_input_type( + /// Build the conversion for one crossing, against what is already built. + /// + /// `None` is *cannot*, never *not yet*: `crossings` hands them out + /// inner-first, so everything this could compose from is already in `built`. + fn convert_crossing( &self, - ty: &syn::Type, - registry: &Registry, + crossing: &Crossing, + built: &Building<'_, KotlinMeta>, ) -> Option> { - self.select_input_type(ty, registry) + let (dir, key) = crossing; + let ty = key.to_type(); + match dir { + Direction::Input => self.select_input_type(&ty, built).or_else(|| { + // `impl Fn(args)` that nothing else claimed. Callback args cross + // in the OPPOSITE direction, which is why their required-ness + // rides `immediate_edges` rather than this converter's `subs`. + let args = crate::api::core::flat::extract_fn_trait_args(&ty)?; + self.dispatch_fn_input(&args, built) + }), + Direction::Output => self.select_output_type(&ty, built), + } } - fn on_output_type( + pub fn declare_into( &self, - ty: &syn::Type, - registry: &Registry, - ) -> Option> { - self.select_output_type(ty, registry) - } + mut registry: RegistryBuilder, + ) -> Result, crate::core::ScanError> { + // Binding-local fns first: they become model, and everything below may + // name one. + for (item_fn, origin) in self.collect_local_functions() { + registry = registry.local_function(item_fn, origin)?; + } - /// Hand the registry this back-end's constructor-expansion declarations so - /// `write_rust` can resolve `.expand`s into fold plans before resolution. - /// Assembled on demand from the per-fn overrides plus the raw type-level - /// [`ExpandParamDecl`]s (see [`JniGen::build_expansions`]). - fn expansions(&self) -> Option { - Some(self.build_expansions()) - } + for ident in self.declared_functions() { + registry = registry.export(&ident); + } + for ident in self.helper_functions() { + registry = registry.reference(&ident); + } + // JniGenBuilder HAS a const mechanism, so const emission is declared-only even + // when nothing is declared. + registry = registry.declares_consts(); + for ident in self.declared_consts().into_iter().flatten() { + registry = registry.export_const(&ident); + } + for key in self.declared_types() { + registry = registry.export_type(key); + } + for ident in self.accessor_functions() { + registry = registry.accessor(&ident); + } + for (ident, receiver) in self.method_receivers() { + registry = registry.method_receiver(&ident, receiver); + } - /// Hand the registry this back-end's output-expansion declarations so - /// `write_rust` can resolve them into unfold plans before resolution. - /// Assembled on demand — field names (member inheritance) resolve here, - /// against the complete declaration set (see - /// [`JniGen::build_deconstructors`]). - fn deconstructors( - &self, - registry: &Registry, - ) -> Option { - Some(self.build_deconstructors(registry)) + // An expression constant's value type has no captured item to scan. + for ty in self.required_output_types() { + registry = registry.cross(Direction::Output, &ty); + } + // The other-side type of every `convert!` conversion, in the + // conversion's direction: an input fn's parameter type needs its own + // input converter for the composed body to chain through; an output + // fn's return type needs the output twin. + let mut convert_edges: Vec<(Crossing, Crossing)> = Vec::new(); + for decl in &self.convert_decls { + if let Some((ty, _, _)) = self.convert_input_body(&decl.key, ®istry) { + registry = registry.cross(Direction::Input, &ty); + // The target's conversion chains through this one, and nothing + // about the target type says so. + convert_edges.push(( + (Direction::Input, decl.key.clone()), + (Direction::Input, TypeKey::from_type(&ty)), + )); + } + if let Some((ty, _, _)) = self.convert_output_body(&decl.key, ®istry) { + registry = registry.cross(Direction::Output, &ty); + convert_edges.push(( + (Direction::Output, decl.key.clone()), + (Direction::Output, TypeKey::from_type(&ty)), + )); + } + } + for (from, on) in convert_edges { + registry = registry.depends(from, on); + } + // How composites cross in pieces. Every one of these reads only the + // model, which is what lets them be stated here rather than asked for + // mid-resolve. + let decompositions = crate::core::Decompositions { + expansions: Some(self.build_expansions()), + deconstructors: Some(self.build_deconstructors(®istry)), + value_structs: self.build_value_struct_decons(®istry), + sums: self.build_sum_decons(®istry), + leaf_vec_elements: self.build_leaf_vec_fold_elements(®istry), + replaces: self.boundary_only_types(), + }; + registry = registry.decompose(decompositions); + Ok(registry) } +} - /// Synthesize a field-decomposition for every `.data_class` type whose - /// fields the fixed builder can forward verbatim (see - /// [`synth_value_struct_leaves`]). The result drives - /// [`crate::api::core::unfold::apply_value_structs`] so such a struct - /// crosses Rust→Kotlin as decoupled leaves (reassembled by the generated - /// `fromParts` builder singleton) instead of a `JObject` built on the Rust - /// side via `call_static_method`. Types the synthesizer declines (enums / - /// projections / `Option`/`Vec`-nested) keep the whole-value - /// [`struct_output_body`] path. - fn value_struct_decons( +impl JniGenBuilder { + pub(crate) fn build_value_struct_decons( &self, - registry: &Registry, + registry: &impl Conversions, ) -> Vec { let mut out = Vec::new(); for (ident, item_struct) in registry.flat().types().filter_map(|t| match t { @@ -1023,18 +1122,9 @@ impl Prebindgen for JniGen { out } - /// Synthesize the tag-plus-groups decomposition of every `sealed_class` - /// type, so a function whose own return (or callback argument) IS the sum - /// delivers it as a tag plus one leaf group per variant — the same wire - /// layout a sum-typed struct field already gets, reassembled by the hoisted - /// builder singleton instead of by the parent's `fromParts`. - /// - /// Emitted for every declared sum, not only the ones currently returned: a - /// decomposition with no matching function wires no plan, and an unused - /// `DeconSpec` emits nothing. - fn sum_decons( + pub(crate) fn build_sum_decons( &self, - registry: &Registry, + registry: &impl Conversions, ) -> Vec { let mut keys: Vec<&TypeKey> = self.types.keys().collect(); keys.sort_by(|a, b| a.as_str().cmp(b.as_str())); @@ -1059,16 +1149,10 @@ impl Prebindgen for JniGen { out } - /// Nominate every **single-leaf** element type that appears in a `Vec` / - /// `Option>` return or an `impl Fn(&[T])` callback arg, so - /// [`crate::api::core::unfold::apply_leaf_vec_folds`] routes the collection - /// through a foreign-built fold (no Rust `ArrayList`). A single-leaf element - /// is an opaque handle (→ a `jlong` pointer the Kotlin folder wraps into its - /// typed handle class) or a non-`data_class` - /// builtin with a JObject-shaped output wire (e.g. String). Multi-field - /// `data_class` elements are excluded — they go through - /// [`Self::value_struct_decons`]. - fn leaf_vec_fold_elements(&self, registry: &Registry) -> Vec { + pub(crate) fn build_leaf_vec_fold_elements( + &self, + registry: &impl Conversions, + ) -> Vec { let mut seen = std::collections::HashSet::new(); let mut out = Vec::new(); let mut consider = |bare: syn::Type| { @@ -1105,118 +1189,62 @@ impl Prebindgen for JniGen { } out } +} - /// Union of every `.fun(...)` list across all - /// [`Self::package`] subpackage contexts. Each entry is a - /// `#[prebindgen]` fn ident the user explicitly hooked into the - /// binding; functions not in this set are skipped by the registry's - /// signature scan and by the per-item emitter. - fn declared_functions(&self) -> std::collections::HashSet { - let mut out = std::collections::HashSet::new(); - for pkg in self.packages.values() { - for m in &pkg.functions { - out.insert(m.rust_ident.clone()); - } - // Function-backed constants (`constant_fun`) are ordinary - // declared functions on the Rust/extern side; only their Kotlin - // surface differs (an eagerly-initialized top-level `val`). - for m in &pkg.constant_functions { - out.insert(m.rust_ident.clone()); - } - } - // Class members (accessor/method/constructor) are declared via - // `.accessor`/`.method`/`.constructor` (not `.fun`) but are still real - // `#[prebindgen]` wrappers: they need a Rust extern + JNINative - // `external fun` + JSONL inclusion. Only their Kotlin surface differs - // (an instance method or companion factory instead of a free fn). - out.extend( - self.class_members - .values() - .flatten() - .map(|m| m.rust_ident.clone()), - ); - out - } - - /// Functions ever referenced as a named `.field(fun!(...))` in any - /// `expand_return!` decl, type-level or per-fn — see - /// [`JniGen::field_accessor_fns`]. Usage-derived, not tied to `.method()` - /// class-member declarations: a function need not also be exposed as an - /// instance method to be referenced this way. - fn accessor_functions(&self) -> std::collections::HashSet { - self.field_accessor_fns() - } - - /// Binding-local fns to synthesize into the registry, from both entry - /// forms — path-built `fun!(crate::f).sig(…)` decls (full stated - /// signature) and `field!("name").with(ty, path)` output fields - /// (signature `fn f(v: &Target) -> Ty`). One fn may back several - /// declarations only with an identical synthesized signature. - fn local_functions(&self) -> Vec<(syn::ItemFn, String)> { - self.collect_local_functions() - } - - /// Methods (`.method`) — their fn ident mapped to the owning class's - /// `TypeKey`, so input-flattening can skip the receiver parameter. - fn method_receivers(&self) -> std::collections::HashMap { - self.class_members - .iter() - .flat_map(|(key, ms)| { - ms.iter() - .filter(|m| m.kind == MemberKind::Method) - .map(move |m| (m.rust_ident.clone(), key.clone())) - }) - .collect() - } - - /// Every type registered via one of the **class declarators** - /// (`ptr_class!` / `enum_class!` / `sealed_class!` / `data_class!`) - /// — i.e. every entry in the type table, whose only - /// writer is `JniGen::register_class`. These are the only structs/enums - /// the per-item emitter walks, and the scan requires them in BOTH - /// directions (their converters always resolve both ways). Wrapper - /// registrations live in their own tables and are deliberately excluded: a - /// wrapper type is required per **usage** direction, so an output-only - /// wrapper needs no input twin. - fn declared_types(&self) -> std::collections::HashSet { - self.types.keys().cloned().collect() +impl JniGenBuilder { + fn dispatch_fn_input( + &self, + args: &[syn::Type], + registry: &impl Conversions, + ) -> Option> { + let outer_ty = build_fn_type(args); + let (wire, body) = callback_input(self, args, registry)?; + let niches = default_niches_for_wire(&wire); + // `impl Fn(...)` crosses the extern tier as the erased lambda object + // (`Any`) — same as the unfold builder / error-sink params. The typed + // wrapper-level lambda signature is computed at render time from the + // arg types' callback plans, not carried in metadata. + Some(ConverterImpl { + subs: vec![], + pre_stages: vec![], + function: self.build_input_fn(&outer_ty, &wire, &body, None), + destination: wire, + niches, + metadata: self.framework_meta(Some(kt::KtType::any())), + }) } +} - /// Union of every `.constant(...)` list across all - /// [`Self::package`] subpackage contexts. `Some` even when empty — JniGen - /// HAS a const declaration mechanism, so const emission is declared-only - /// and undeclared consts get the skip warning (see - /// [`Prebindgen::declared_consts`]). - /// The declared value types of every expression constant - /// (`ConstDecl::expr`) — they have no `#[prebindgen]` item to - /// scan, so the resolver is told directly to produce their output - /// converters. - fn required_output_types(&self) -> Vec { - self.packages - .values() - .flat_map(|p| p.constant_exprs.iter().map(|e| e.ty.clone())) - .collect() - } +impl Prebindgen for JniGenBuilder { + /// Cross-language extras every JNI converter carries — currently + /// the Kotlin value-context type name. Filled by the rank-N + /// handlers at the same point they build the wire/body; the + /// resolver propagates it into [`crate::api::core::registry::TypeEntry::metadata`]; + /// the Kotlin emitter reads it back to drive every wrapper / + /// typed-handle / `JNIWrappers` signature. + type Metadata = KotlinMeta; - fn declared_consts(&self) -> Option> { - let mut out = std::collections::HashSet::new(); - for pkg in self.packages.values() { - for c in &pkg.constants { - out.insert(c.rust_ident.clone()); - } - } - Some(out) - } + // ── Structural type resolution ────────────────────────────────────── + // Try the terminal categories, then the `Result` peel, then the built-in + // wrapper shapes — peel + // `ty`'s outermost layer and dispatch to `{input,output}_wrapper_shape` with + // the reconstructed canonical pattern. `subs` = the captured inner(s). /// Member-shape invariants (N5), checked against registry signatures — /// the earliest possible moment. Without this, a receiver-less `.method()` /// member would silently emit a method that ignores `this`, and a /// wrong-return `.constructor()` a factory of the wrong type. - fn validate(&self, registry: &Registry) -> Result<(), String> { + fn validate(&self, binding: &Building<'_, Self::Metadata>) -> Result<(), String> { + // Report what this binding left unclaimed. Here because it is the + // earliest generator-owned hook that sees the model, and it runs + // exactly where the binding used to print these itself. Moves into + // `JniGenBuilder::generate` once that exists (prebindgen#251 phase E). + crate::core::warn_unclaimed(binding.flat(), &self.claimed()); + for (key, members) in &self.class_members { for m in members { - // A registry-absent fn already hard-errored in the scan. - let Some(item_fn) = registry + // A binding-absent fn already hard-errored in the scan. + let Some(item_fn) = binding .flat() .function(&m.rust_ident) .map(|func| &func.origin.syntax) @@ -1287,7 +1315,7 @@ impl Prebindgen for JniGen { // something that must not exist. Reject them here, where the message // can say what is actually unsupported and what to write instead. for ident in self.declared_functions() { - let Some(item_fn) = registry + let Some(item_fn) = binding .flat() .function(&ident) .map(|func| &func.origin.syntax) @@ -1302,7 +1330,7 @@ impl Prebindgen for JniGen { if let syn::ReturnType::Type(_, ret) = &item_fn.sig.output { if let Some(ok) = crate::api::core::types_util::result_ok_type(ret) { let core = crate::api::core::types_util::peel_ref_option_vec(&ok); - if matches!(self.type_kind(registry, &core), TypeKind::Sum) { + if matches!(self.type_kind(binding, &core), TypeKind::Sum) { return Err(format!( "fn `{ident}`: `Result<{}, _>` — a sealed_class value is not \ supported in the success position of a fallible return. A sum \ @@ -1340,7 +1368,7 @@ impl Prebindgen for JniGen { .return_expand_decls .iter() .any(|d| d.key == TypeKey::from_type(&err_ty)); - if !declared && matches!(self.type_kind(registry, &core), TypeKind::Sum) { + if !declared && matches!(self.type_kind(binding, &core), TypeKind::Sum) { return Err(format!( "fn `{ident}`: `Result<_, {}>` — `{}` is declared `sealed_class!`, \ but nothing decomposes it in the error position, so it would be \ @@ -1390,7 +1418,7 @@ impl Prebindgen for JniGen { syn::Type::Reference(r) => (*r.elem).clone(), other => other.clone(), }; - if matches!(self.type_kind(registry, &elem), TypeKind::Sum) { + if matches!(self.type_kind(binding, &elem), TypeKind::Sum) { return Err(format!( "fn `{ident}`: `impl Fn(&[{}])` — a slice of a sealed_class value \ is not supported as a callback argument. A sum crosses as a tag \ @@ -1418,42 +1446,6 @@ impl Prebindgen for JniGen { validate_bindings(self, registry) } - /// Consts acknowledged-but-unexposed via [`JniGen::ignore`]. - fn ignored_consts(&self) -> std::collections::HashSet { - self.ignored_const_idents.clone() - } - - /// Fns acknowledged-but-unbound via [`JniGen::ignore`] — suppresses - /// the registry's "skipping undeclared" warning, emits nothing. - fn ignored_functions(&self) -> std::collections::HashSet { - self.ignored_fns.clone() - } - - /// Bulk name-family ignores from [`JniGen::ignore`] + - /// [`matching`](crate::lang::matching). - fn ignored_name_predicates(&self) -> Vec { - self.ignored_name_predicates.clone() - } - - /// Framework-called fns that get no extern of their own: `convert!` - /// conversion fns (called by generated converter bodies) and fns - /// referenced only inside boundary decls (`expand_return!` accessors / - /// `expand_param!` ctors, called by the generated fold/unfold code). - /// Routing both through the *helper* channel — not the ignore channel — - /// makes a typo'd `fun!(…)` inside a decl a hard scan error - /// (`ScanError::DeclaredNotFound`) instead of a stale-ignore - /// warning. - /// Declared functions are subtracted: a fn that is also a real - /// member/package fn keeps its extern. Type requirements come through - /// [`Self::extra_required_types`], not a signature scan. - fn helper_functions(&self) -> std::collections::HashSet { - let declared = self.declared_functions(); - self.convert_fns() - .chain(self.boundary_referenced_fns()) - .filter(|f| !declared.contains(f)) - .collect() - } - /// The other-side type of every `convert!` conversion, in the /// conversion's direction: an input fn's parameter type (peeled of `&`) /// must have its own **input** converter for the composed rank-0 body to @@ -1461,51 +1453,6 @@ impl Prebindgen for JniGen { /// Signatures are read from the registry (missing fns are reported by /// the scan's helper-function warning; the body derivation later /// hard-errors with the precise decl). - fn extra_required_types( - &self, - registry: &Registry, - ) -> Vec<(crate::api::core::registry::Direction, syn::Type)> { - use crate::api::core::registry::Direction; - let mut out = Vec::new(); - for decl in &self.convert_decls { - if let Some((ty, _, _)) = self.convert_input_body(&decl.key, registry) { - out.push((Direction::Input, ty)); - } - if let Some((ty, _, _)) = self.convert_output_body(&decl.key, registry) { - out.push((Direction::Output, ty)); - } - } - out - } - - /// Types acknowledged-but-undeclared via [`JniGen::ignore`]. - fn ignored_types(&self) -> std::collections::HashSet { - self.ignored_class_types.clone() - } - - /// **Rust-side-only** types: boundary decls (`expand_param!` / - /// `expand_return!`) whose type has no class declaration. They never - /// materialize in Kotlin — only their ingredients (fold) and fields - /// (unfold / error channel) cross the boundary — so the registry - /// acknowledges them and drops their direct converter requirements once - /// the plans are in place. - fn boundary_only_types(&self) -> std::collections::HashSet { - // A `sealed_class!`-declared sum has no single wire: it crosses as a - // tag plus one leaf group per variant, so a direct converter for the - // value itself is genuinely not needed. Declaring it boundary-only - // drops that requirement while keeping the type scanned (its payload - // types register and resolve, which is what the Kotlin surface reads - // its field types from). - self.rust_side_only_types() - .chain( - self.types - .iter() - .filter(|(_, c)| c.sum().is_some()) - .map(|(k, _)| k.clone()), - ) - .collect() - } - /// Emit the `OwnedObject` borrow wrapper used by /// [`Self::opaque_handle_input`] into the destination file. /// The struct is referenced by an unqualified `OwnedObject` from @@ -1613,43 +1560,21 @@ impl Prebindgen for JniGen { #wrapper } } - - fn dispatch_fn_input( - &self, - args: &[syn::Type], - registry: &Registry, - ) -> Option> { - let outer_ty = build_fn_type(args); - let (wire, body) = callback_input(self, args, registry)?; - let niches = default_niches_for_wire(&wire); - // `impl Fn(...)` crosses the extern tier as the erased lambda object - // (`Any`) — same as the unfold builder / error-sink params. The typed - // wrapper-level lambda signature is computed at render time from the - // arg types' callback plans, not carried in metadata. - Some(ConverterImpl { - subs: vec![], - pre_stages: vec![], - function: self.build_input_fn(&outer_ty, &wire, &body, None), - destination: wire, - niches, - metadata: self.framework_meta(Some(kt::KtType::any())), - }) - } } /// Structural converter builders — the rank-0 terminal chains and the rank-1 /// wrapper-shape handlers, now inherent helpers called by the structural /// [`Prebindgen::on_input_type`] / [`Prebindgen::on_output_type`]. -impl JniGen { +impl JniGenBuilder { // ── Input converters ───────────────────────────────────────────── - /// Whole-type **input** terminal categories (opaque handle, enum, the - /// rank-0 user table, `str`, primitive, struct) — depends on - /// nothing, `subs` empty. + /// Whole-type **input** terminal categories (opaque handle, enum, + /// `convert!`, `str`, primitive, struct) — depends on nothing, `subs` + /// empty. pub(crate) fn input_terminal( &self, ty: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { // Structured-config overrides first (opaque handles, then user- // registered rank-0 wrappers, then built-ins). @@ -1703,7 +1628,7 @@ impl JniGen { } } } - if let Some(conv) = self.lookup_input(ty, &[], registry) { + if let Some(conv) = self.lookup_input(ty, registry) { return Some(conv); } // `str` is unsized, so converters can't return it directly. @@ -1840,18 +1765,15 @@ impl JniGen { } /// **Input** wrapper shape (`pat` = the reconstructed canonical pattern, - /// `t1` = its captured inner): the rank-1 user table, then the built-in - /// `&`/`Option<&>`/`Vec`/`Option` handlers. + /// `t1` = its captured inner): the built-in `&`/`Option<&>`/`Vec`/`Option` + /// handlers. pub(crate) fn input_wrapper_shape( &self, pat: &syn::Type, t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { - if let Some(conv) = self.lookup_input(pat, std::slice::from_ref(t1), registry) { - return Some(conv); - } - // Disjoint wildcard patterns (see the `impl JniGen` block above), tried + // Disjoint wildcard patterns (see the `impl JniGenBuilder` block above), tried // in priority order. The borrow/option-ref/vec patterns are exact and // mutually exclusive; the two `Option<_>` sub-cases share a method. self.input_borrow(pat, t1, registry) @@ -1868,10 +1790,9 @@ impl JniGen { pub(crate) fn output_terminal( &self, ty: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { - // Structured-config overrides first (opaque handles, then the - // unified user-registered wrapper table, then built-ins). + // Structured-config overrides first (opaque handles, then built-ins). let key = TypeKey::from_type(ty); if let Some(cfg) = self.types.get(&key) { if cfg.is_opaque() { @@ -1921,7 +1842,7 @@ impl JniGen { } } } - if let Some(conv) = self.lookup_output(ty, &[], registry) { + if let Some(conv) = self.lookup_output(ty, registry) { return Some(conv); } // `str` is unsized, so it has no by-value output converter — but it is @@ -2024,17 +1945,14 @@ impl JniGen { } /// **Output** wrapper shape (the dual of [`Self::input_wrapper_shape`]): - /// the rank-1 user table, then the built-in `&Handle`/`&str`/`Option`/`Vec` - /// handlers. An `Option<&Handle>` resolves via the shallow `Option<_>`. + /// the built-in `&Handle`/`&str`/`Option`/`Vec` handlers. An + /// `Option<&Handle>` resolves via the shallow `Option<_>`. pub(crate) fn output_wrapper_shape( &self, pat: &syn::Type, t1: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { - if let Some(conv) = self.lookup_output(pat, std::slice::from_ref(t1), registry) { - return Some(conv); - } // Borrowed opaque-handle output (`&T` / `&'static T` where `T` is a // declared opaque handle). Canonical zenoh-flat's `z_*` accessors // return *borrowed* handles for the C tier's zero-copy borrows, but @@ -2078,11 +1996,9 @@ impl JniGen { return Some(self.str_ref_output()); } } - // `Result<_, _>` is handled as a built-in rank-2 wrapper registered - // in `JniGen::new`. Bindings just declare the Err type via - // `.throwable()`. Per-error overrides are possible by registering a - // more specific rank-1 `output_wrapper(Result<_, ConcreteErr>, …)` - // — rank-1 fires before rank-2 in resolve and short-circuits here. + // `Result` is peeled by the selector, off the model's + // `TypeKind::Fallible`. Bindings declare the `Err` type via + // `.throwable()`. if pat_match(pat, "Option < _ >") { let outer_ty: syn::Type = syn::parse_quote!(Option<#t1>); let (wire, body, niches) = option_output(t1, registry)?; @@ -2207,7 +2123,7 @@ impl JniGen { pub(crate) fn output_slice( &self, elem: &syn::Type, - registry: &Registry, + registry: &impl Conversions, ) -> Option> { let inner = registry.output_entry(elem)?; // A `&[opaque-handle]` callback arg is delivered by the Kotlin-side leaf @@ -2260,3 +2176,177 @@ impl JniGen { }) } } + +/// The declaration surface, stated once. +/// +/// These were trait methods the registry called back into the adapter from +/// inside `resolve`. They are the adapter's own business now, gathered into the +/// one value the registry is constructed from. +impl JniGenBuilder { + /// Union of every `.fun(...)` list across all + /// [`Self::package`] subpackage contexts. Each entry is a + /// `#[prebindgen]` fn ident the user explicitly hooked into the + /// binding; functions not in this set are skipped by the registry's + /// signature scan and by the per-item emitter. + pub(crate) fn declared_functions(&self) -> std::collections::HashSet { + let mut out = std::collections::HashSet::new(); + for pkg in self.packages.values() { + for m in &pkg.functions { + out.insert(m.rust_ident.clone()); + } + // Function-backed constants (`constant_fun`) are ordinary + // declared functions on the Rust/extern side; only their Kotlin + // surface differs (an eagerly-initialized top-level `val`). + for m in &pkg.constant_functions { + out.insert(m.rust_ident.clone()); + } + } + // Class members (accessor/method/constructor) are declared via + // `.accessor`/`.method`/`.constructor` (not `.fun`) but are still real + // `#[prebindgen]` wrappers: they need a Rust extern + JNINative + // `external fun` + JSONL inclusion. Only their Kotlin surface differs + // (an instance method or companion factory instead of a free fn). + out.extend( + self.class_members + .values() + .flatten() + .map(|m| m.rust_ident.clone()), + ); + out + } + /// Functions ever referenced as a named `.field(fun!(...))` in any + /// `expand_return!` decl, type-level or per-fn — see + /// [`JniGenBuilder::field_accessor_fns`]. Usage-derived, not tied to `.method()` + /// class-member declarations: a function need not also be exposed as an + /// instance method to be referenced this way. + pub(crate) fn accessor_functions(&self) -> std::collections::HashSet { + self.field_accessor_fns() + } + /// Methods (`.method`) — their fn ident mapped to the owning class's + /// `TypeKey`, so input-flattening can skip the receiver parameter. + pub(crate) fn method_receivers(&self) -> std::collections::HashMap { + self.class_members + .iter() + .flat_map(|(key, ms)| { + ms.iter() + .filter(|m| m.kind == MemberKind::Method) + .map(move |m| (m.rust_ident.clone(), key.clone())) + }) + .collect() + } + /// Fns acknowledged-but-unbound via [`JniGenBuilder::ignore`] — suppresses + /// the registry's "skipping undeclared" warning, emits nothing. + pub(crate) fn ignored_functions(&self) -> std::collections::HashSet { + self.ignored_fns.clone() + } + /// Bulk name-family ignores from [`JniGenBuilder::ignore`] + + /// [`matching`](crate::lang::matching). + pub(crate) fn ignored_name_predicates( + &self, + ) -> Vec { + self.ignored_name_predicates.clone() + } + /// Framework-called fns that get no extern of their own: `convert!` + /// conversion fns (called by generated converter bodies) and fns + /// referenced only inside boundary decls (`expand_return!` accessors / + /// `expand_param!` ctors, called by the generated fold/unfold code). + /// Routing both through the *helper* channel — not the ignore channel — + /// makes a typo'd `fun!(…)` inside a decl a hard scan error + /// (`ScanError::DeclaredNotFound`) instead of a stale-ignore + /// warning. + /// Declared functions are subtracted: a fn that is also a real + /// member/package fn keeps its extern. Type requirements come through + /// [`Self::extra_required_types`], not a signature scan. + pub(crate) fn helper_functions(&self) -> std::collections::HashSet { + let declared = self.declared_functions(); + self.convert_fns() + .chain(self.boundary_referenced_fns()) + .filter(|f| !declared.contains(f)) + .collect() + } + pub(crate) fn declared_consts(&self) -> Option> { + let mut out = std::collections::HashSet::new(); + for pkg in self.packages.values() { + for c in &pkg.constants { + out.insert(c.rust_ident.clone()); + } + } + Some(out) + } + /// Consts acknowledged-but-unexposed via [`JniGenBuilder::ignore`]. + pub(crate) fn ignored_consts(&self) -> std::collections::HashSet { + self.ignored_const_idents.clone() + } + /// Union of every `.constant(...)` list across all + /// [`Self::package`] subpackage contexts. `Some` even when empty — JniGenBuilder + /// HAS a const declaration mechanism, so const emission is declared-only + /// and undeclared consts get the skip warning (see + /// [`Prebindgen::declared_consts`]). + /// The declared value types of every expression constant + /// (`ConstDecl::expr`) — they have no `#[prebindgen]` item to + /// scan, so the resolver is told directly to produce their output + /// converters. + pub(crate) fn required_output_types(&self) -> Vec { + self.packages + .values() + .flat_map(|p| p.constant_exprs.iter().map(|e| e.ty.clone())) + .collect() + } + /// Every type registered via one of the **class declarators** + /// (`ptr_class!` / `enum_class!` / `sealed_class!` / `data_class!`) + /// — i.e. every entry in the type table, whose only + /// writer is `JniGenBuilder::register_class`. These are the only structs/enums + /// the per-item emitter walks, and the scan requires them in BOTH + /// directions (their converters always resolve both ways). Wrapper + /// registrations live in their own tables and are deliberately excluded: a + /// wrapper type is required per **usage** direction, so an output-only + /// wrapper needs no input twin. + pub(crate) fn declared_types(&self) -> std::collections::HashSet { + self.types.keys().cloned().collect() + } + /// Types acknowledged-but-undeclared via [`JniGenBuilder::ignore`]. + pub(crate) fn ignored_types(&self) -> std::collections::HashSet { + self.ignored_class_types.clone() + } + /// What this binding claimed, for the unclaimed-item report. A helper is + /// claimed even though it is never emitted, and a boundary-only type even + /// though it never crosses whole: both are deliberate, so neither is a + /// skip worth reporting. + pub(crate) fn claimed(&self) -> crate::core::Claimed { + let mut functions = self.declared_functions(); + functions.extend(self.helper_functions()); + let mut types = self.declared_types(); + types.extend(self.boundary_only_types()); + crate::core::Claimed { + functions, + types, + consts: self.declared_consts(), + ignored_functions: self.ignored_functions(), + ignored_types: self.ignored_types(), + ignored_consts: self.ignored_consts(), + ignored_name_predicates: self.ignored_name_predicates(), + } + } + /// **Rust-side-only** types: boundary decls (`expand_param!` / + /// `expand_return!`) whose type has no class declaration. They never + /// materialize in Kotlin — only their ingredients (fold) and fields + /// (unfold / error channel) cross the boundary — so the registry + /// acknowledges them and drops their direct converter requirements once + /// the plans are in place. + pub(crate) fn boundary_only_types(&self) -> std::collections::HashSet { + // A `sealed_class!`-declared sum has no single wire: it crosses as a + // tag plus one leaf group per variant, so a direct converter for the + // value itself is genuinely not needed. Declaring it boundary-only + // drops that requirement while keeping the type scanned (its payload + // types register and resolve, which is what the Kotlin surface reads + // its field types from). + self.rust_side_only_types() + .chain( + self.types + .iter() + .filter(|(_, c)| c.sum().is_some()) + .map(|(k, _)| k.clone()), + ) + .collect() + } +} diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index b20d2425..85e46ec9 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -1,4 +1,4 @@ -//! JNI / Kotlin language adapter — the [`JniGen`] back-end. +//! JNI / Kotlin language adapter — the [`JniGenBuilder`] back-end. //! //! Sibling of [`crate::api::lang::cbindgen`]: it implements the //! language-agnostic [`crate::api::core::prebindgen::Prebindgen`] trait to @@ -6,18 +6,18 @@ //! wrappers plus a fan-out of generated Kotlin sources. //! //! Pipeline: -//! 1. [`crate::api::core::registry::Registry::from_items`] scans a stream of +//! 1. [`crate::api::core::registry::Registry::builder`] describes a binding over a model built from //! `(syn::Item, SourceLocation)` (typically `source.items_all()`). //! 2. [`crate::api::core::registry::Registry::write_rust`] resolves every -//! required type via a configured [`JniGen`] and writes the generated +//! required type via a configured [`JniGenBuilder`] and writes the generated //! Rust bindings file. -//! 3. [`jni::JniGen::write_kotlin`] walks the resolved registry to emit the +//! 3. [`jni::JniGenBuilder::write_kotlin`] walks the resolved registry to emit the //! secondary Kotlin artifacts (typed-handle classes, data/enum classes, //! exception classes, the centralized `JNINative` holder). //! //! # Fixed-width unsigned integers //! -//! JniGen exposes Rust's fixed-width unsigned scalars without narrowing their +//! JniGenBuilder exposes Rust's fixed-width unsigned scalars without narrowing their //! domain at the Kotlin boundary: //! //! | Rust | Kotlin surface | JNI wire | @@ -43,8 +43,8 @@ pub use jni::{ decode_byte_array, decode_string, encode_byte_array, encode_string, matching, null_byte_array, null_string, CachedIfaceMethod, ClassDecl, ConstDecl, ConvertDecl, ConvertSourceDecl, DataClassDecl, EnumClassDecl, ExpandDecl, ExpandParamDecl, ExpandReturnDecl, FieldsDecl, - FunctionDecl, IgnoreDecl, JniBindingError, JniGen, PackageDecl, PtrClassDecl, SealedClassDecl, - VariantDecl, + FunctionDecl, IgnoreDecl, JniBindingError, JniGen, JniGenBuilder, PackageDecl, PtrClassDecl, + SealedClassDecl, VariantDecl, }; // Kotlin emission types now live in the standalone generator module diff --git a/prebindgen/src/api/record.rs b/prebindgen/src/api/record.rs index bddf5298..82dad28e 100644 --- a/prebindgen/src/api/record.rs +++ b/prebindgen/src/api/record.rs @@ -35,7 +35,7 @@ pub struct SourceLocation { /// consumers under a name only they know) — `Source` stamps it while /// parsing records, so every item stream carries its origin and streams /// from different sources can be `chain`ed into one - /// `Registry::from_items` call without losing per-item origins. + /// `Flat::builder().items(..)` call without losing per-item origins. #[serde(default, skip_serializing_if = "Option::is_none")] pub crate_name: Option, } diff --git a/prebindgen/src/api/source.rs b/prebindgen/src/api/source.rs index 58b02d92..20c77661 100644 --- a/prebindgen/src/api/source.rs +++ b/prebindgen/src/api/source.rs @@ -131,7 +131,7 @@ impl Source { // captured JSONL doesn't carry it (the proc-macro runs // inside the crate), but from here on the item streams // are self-describing — streams from several sources can - // be chained into one `Registry::from_items` call + // be chained into one `Flat::builder().items(..)` call // without losing per-item origins. let (item, mut loc) = r.parse(); loc.crate_name = Some(crate_name.clone()); diff --git a/prebindgen/src/api/test_util.rs b/prebindgen/src/api/test_util.rs index c1c6cda7..ee259d62 100644 --- a/prebindgen/src/api/test_util.rs +++ b/prebindgen/src/api/test_util.rs @@ -6,16 +6,18 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, }; -use crate::api::core::registry::{Registry, TypeCell, TypeEntry, TypeKey, TypeSubject}; +use crate::api::core::registry::{Registry, RegistryBuilder, TypeCell, TypeEntry, TypeSubject}; /// A type-table cell for a fixture. /// /// The subject is always [`TypeSubject::Adapter`]: a hand-built table has no /// `Flat` behind it, so no key in one has a source reading. A test that cares /// about the `Source` side builds its registry from items instead. -pub(crate) fn cell(key: &TypeKey, root: bool, entry: Option>) -> TypeCell { +/// +/// Takes no key: `Adapter` carries nothing, since nothing ever read it back. +pub(crate) fn cell(root: bool, entry: Option>) -> TypeCell { TypeCell { - subject: TypeSubject::Adapter(key.to_type()), + subject: TypeSubject::Adapter, root, entry, } @@ -27,7 +29,7 @@ pub(crate) fn cell(key: &TypeKey, root: bool, entry: Option>) -> /// Whatever it does *not* declare is supplied by [`declare_referenced`], because /// these fixtures exist to exercise plan shapes and a handle declaration is noise /// in them. -pub(crate) fn reg_with(sources: &[&str]) -> Registry<()> { +pub(crate) fn reg_with(sources: &[&str]) -> RegistryBuilder { let items = sources .iter() .map(|src| { @@ -35,7 +37,27 @@ pub(crate) fn reg_with(sources: &[&str]) -> Registry<()> { (item, crate::SourceLocation::default()) }) .collect::>(); - Registry::from_items(declare_referenced(items)).expect("index") + reg_from_items(declare_referenced(items)).expect("index") +} + +/// A **scanned** registry from item sources — for tests that drive `expand` / +/// `unfold` directly and need the type tables populated, without going through +/// a generator's conversion loop. +pub(crate) fn scanned_with(sources: &[&str]) -> Registry { + reg_with(sources).scanned().expect("scan") +} + +/// Build a `Registry` from an item stream, the way `Registry::from_items` used +/// to before reading captured output became `FlatBuilder`'s job alone. +/// +/// Test-only sugar: the two steps are one line each in a build script, but they +/// appear in dozens of fixtures here. +pub(crate) fn reg_from_items(items: I) -> Result, crate::core::ScanError> +where + I: IntoIterator, +{ + let flat = crate::core::Flat::builder().items(items).build()?; + Registry::builder(flat) } /// Append a marked type alias for every nominal type the stream names but never diff --git a/prebindgen/src/lib.rs b/prebindgen/src/lib.rs index 630f2055..2406307d 100644 --- a/prebindgen/src/lib.rs +++ b/prebindgen/src/lib.rs @@ -19,7 +19,7 @@ //! //! `prebindgen` solves this by generating language-specific proxy code from a common //! Rust library crate. The supported 0.5 surface is the language-neutral -//! [`core`] pipeline and the JNI/Kotlin [`lang::JniGen`] adapter. +//! [`core`] pipeline and the JNI/Kotlin [`lang::JniGenBuilder`] adapter. //! //! The C / cbindgen adapter is an experimental proof of concept. It is available //! only with the non-default `unstable-cbindgen` feature and is not covered by @@ -35,7 +35,7 @@ //! ### Stable core and JNI/Kotlin path //! //! The supported workflow reads captured items with [`Source`], resolves them -//! through [`core::Registry`], and configures [`lang::JniGen`] to emit Rust JNI +//! through [`core::Registry`], and configures [`lang::JniGenBuilder`] to emit Rust JNI //! wrappers plus Kotlin sources. The `covertest-kotlin` and `perftest-kotlin` //! workspace examples are the maintained references for that path. //! @@ -74,7 +74,7 @@ //! ### 2. Experimental C binding crate (`unstable-cbindgen`) //! //! Depend on the common FFI library (as both a normal and a build dependency) and -//! drive the experimental `lang::Cbindgen` adapter from `build.rs`: +//! drive the experimental `lang::CbindgenBuilder` adapter from `build.rs`: //! //! ```toml //! # example-cbindgen/Cargo.toml @@ -98,7 +98,7 @@ //! let source = prebindgen::Source::new(example_flat::PREBINDGEN_OUT_DIR); //! //! // Configure the C adapter: declare which items to export and how to name them. -//! let cbindgen = prebindgen::lang::Cbindgen::new() +//! let cbindgen = prebindgen::lang::CbindgenBuilder::new() //! .source_module(pq!(example_flat)) //! .free_memory_function("example_free") //! .mangle_type_name(|base| format!("{base}_t")) @@ -109,10 +109,12 @@ //! .function(pq!(calculator_get_value)).panic(); //! //! // Resolve types, then write the Rust file of `extern "C"` wrappers. -//! let generation = prebindgen::core::Registry::from_items(source.items_all()) -//! .unwrap() -//! .resolve(cbindgen) +//! let flat = prebindgen::core::Flat::builder() +//! .items(source.items_all()) +//! .build() //! .unwrap(); +//! let builder = prebindgen::core::Registry::builder(flat).unwrap(); +//! let generation = cbindgen.resolve(builder).unwrap(); //! let bindings_file = generation.write_rust("example_flat.rs").unwrap(); //! //! // Pass the generated file to cbindgen for C header generation. @@ -133,7 +135,7 @@ //! fall into two groups. //! //! **Declaration macros** construct a typed [`lang`] `*Decl` from bare Rust -//! syntax — the domain vocabulary you compose and hand to [`lang::JniGen`]: +//! syntax — the domain vocabulary you compose and hand to [`lang::JniGenBuilder`]: //! //! - Kotlin surface: [`package!`](crate::package), [`ptr_class!`](crate::ptr_class), //! [`data_class!`](crate::data_class), [`enum_class!`](crate::enum_class) @@ -242,15 +244,14 @@ macro_rules! ident { /// /// # The plug-in point /// -/// Implement the [`Prebindgen`](core::Prebindgen) trait once per destination -/// language. The trait teaches the pipeline two things: +/// Write one generator per destination language. It does two things: /// -/// * **How the language represents Rust types on the wire** — the -/// `on_input_type_rank_0..3` / `on_output_type_rank_0..3` methods return a +/// * **Says how the language represents Rust types on the wire** — it builds a /// [`ConverterImpl`](core::ConverterImpl) (a generated converter fn plus its -/// wire type) for each required type. -/// * **What wrapper code to emit per item** — `on_function` / `on_struct` / -/// `on_enum` / `on_const`. +/// wire type) for each crossing the registry hands it, and gives them all +/// back through [`Registry::supply`](core::Registry::supply). +/// * **Emits the wrapper code per item** — `on_function` / `on_struct` / +/// `on_enum` / `on_const` on the [`Prebindgen`](core::Prebindgen) trait. /// /// Everything language-specific that must travel through the pipeline rides in /// the back-end's chosen [`Metadata`](core::Prebindgen::Metadata) type (a JNI @@ -261,14 +262,32 @@ macro_rules! ident { /// /// # Flow /// -/// 1. [`Registry::from_items`](core::Registry::from_items) indexes the -/// `(syn::Item, SourceLocation)` stream (typically [`Source::items_all`]). -/// 2. [`Registry::resolve`](core::Registry::resolve) resolves every required -/// type via your back-end, yielding a [`Generation`](core::Generation); -/// its `write_rust` (and adapter-specific `write_*`) methods emit the -/// artifacts. -/// 3. The back-end produces any secondary artifacts (C headers, Kotlin sources, -/// …) by walking the resolved [`Registry`](core::Registry). +/// A build script sees one type — the generator — and never names a `Flat` or a +/// `Registry`: +/// +/// ```ignore +/// let jni = JniGen::builder() +/// .package(package!("io.zenoh")) +/// .fun(fun!(session_open)) +/// .source(zenoh_flat::PREBINDGEN_OUT_DIR) +/// .build()?; +/// jni.write_rust(&rust_dest)?; +/// jni.write_kotlin(&kotlin_root)?; +/// ``` +/// +/// Inside `build()`, the generator does what it alone knows how to do: +/// +/// 1. [`Flat::builder`](core::Flat::builder) parses the declared sources into +/// the model, and [`Registry::builder`](core::Registry::builder) starts +/// describing a binding over it. +/// 2. The generator states that binding, then +/// [`Registry::crossings`](core::Registry::crossings) hands over every +/// crossing needing a conversion — inner types first, so each one can be +/// built from those already done. `convert_with` answers them and +/// `build` names any gap. +/// 3. The resolved registry becomes a field of the built generator, whose +/// `write_*` methods emit the artifacts — Rust wrappers, and whatever else +/// that language needs (a C header, Kotlin sources, …). /// /// # Universality, by example /// @@ -283,7 +302,7 @@ macro_rules! ident { /// info lives in that back-end's `Metadata`). /// /// The supported JNI / Kotlin adapter ships in [`mod@lang`] as -/// [`lang::JniGen`]. The C / cbindgen proof of concept is available separately +/// [`lang::JniGenBuilder`]. The C / cbindgen proof of concept is available separately /// with the `unstable-cbindgen` feature. pub mod core { /// The **flat API**: the parser from captured `#[prebindgen]` records to the @@ -294,9 +313,10 @@ pub mod core { /// a build script names, and the rest of the model stays in [`mod@flat`] /// where an adapter reaches for it. pub use crate::api::core::{ - ConverterImpl, Direction, DomainScalar, Element, Flat, Generation, Gravestone, NicheSlot, - Niches, Prebindgen, Registry, RegistryBuilder, RepresentationDomain, ScalarValue, - ScanError, Stage, Transmute, TypeCell, TypeEntry, TypeKey, TypeSubject, WriteRustError, + warn_unclaimed, Building, Claimed, Conversions, ConverterImpl, Crossing, Decompositions, + Direction, DomainScalar, DuplicateNameError, Element, Flat, Gravestone, NicheSlot, Niches, + NotExpressibleEntry, Prebindgen, Registry, RepresentationDomain, ScalarValue, ScanError, + Stage, Transmute, TypeEntry, TypeKey, TypeKeyParseError, WriteRustError, }; } @@ -313,24 +333,25 @@ pub use crate::api::lang::jnigen::matching; /// Destination-language adapters implementing [`core::Prebindgen`]. /// -/// With the non-default `unstable-cbindgen` feature, `Cbindgen` is an +/// With the non-default `unstable-cbindgen` feature, `CbindgenBuilder` is an /// experimental C / cbindgen adapter. Its API is not covered by the 0.5 semver /// guarantee. /// -/// [`lang::JniGen`] is the JNI / Kotlin adapter: it turns a flat +/// [`lang::JniGenBuilder`] is the JNI / Kotlin adapter: it turns a flat /// `#[prebindgen]` library into a Rust file of JNI `extern "C"` wrappers plus /// a fan-out of generated Kotlin sources (typed-handle classes, data/enum /// classes, exception classes). pub mod lang { #[cfg(feature = "unstable-cbindgen")] - pub use crate::api::lang::cbindgen::{snake_case, Cbindgen}; + pub use crate::api::lang::cbindgen::{snake_case, Cbindgen, CbindgenBuilder}; pub use crate::api::lang::jnigen::{ box_jboolean, box_jbyte, box_jchar, box_jdouble, box_jfloat, box_jint, box_jlong, box_jshort, decode_byte_array, decode_string, encode_byte_array, encode_string, matching, null_byte_array, null_string, CachedIfaceMethod, ClassDecl, ConstDecl, ConvertDecl, ConvertSourceDecl, DataClassDecl, EnumClassDecl, ExpandDecl, ExpandParamDecl, ExpandReturnDecl, FieldsDecl, FunctionDecl, IgnoreDecl, JniBindingError, JniGen, - KotlinFile, PackageDecl, PtrClassDecl, SealedClassDecl, VariantDecl, WriteKotlinError, + JniGenBuilder, KotlinFile, PackageDecl, PtrClassDecl, SealedClassDecl, VariantDecl, + WriteKotlinError, }; } From 19a2b771eac7b116ba3ade8dc4b1d2ff0c72f91d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 00:54:41 +0200 Subject: [PATCH 12/52] The ledger started falling, and a merged stack is missing from the map (#254) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things the map got wrong since #247 synced it. The ledger is 167, not 202. #248 deleted the pattern engine and took 35 sites with it — types_util 40 to 14, jnigen/builder 13 to 4 — so L2 is in progress, not "not started", and the claim that the ledger has not begun falling is false. Recorded with the distinction that matters: those 35 went away because their code went away, which is deletion rather than migration, and the 45 that remain in api/core are the ones that have to start reading elements. The #249-#253 stack is merged but not on this branch. It landed PR-into-PR onto flat-drop-pattern-engine, of which only #248's commit ever reached language-integration, leaving 28 commits — the registry and generator API redesign tracked by #251 — invisible to the map. Recorded as L1.75 for the same reason L1.5 is recorded: the map should show where the program went. --- docs/language-integration.md | 105 ++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index a72e287d..46e80fdc 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -88,19 +88,22 @@ scoreboard for this whole program. Seeded by L0 at **202 classification sites** outside the frontend. The second population it was seeded alongside — **113** reads of the registry's `syn`-keyed item maps — is **gone**: L1.5 deleted those maps, so every one of those reads now -goes through the model. What remains is the ledger. - -| Area | Ledger sites | Stage | -|---|---:|---| -| `api/core` (`types_util` 40, `unfold` 16, `registry` 11, `expand` 4) | 71 | L2 | -| `api/lang/cbindgen` | 25 | L3 | -| `api/lang/jnigen` | 106 | L4 | -| **total** | **202** | | - -Still 202 in total, and that is the honest number: L1.5 moved reads off the -deleted maps but took only two classifiers off the ledger (−2 in `registry`, -`unfold` +1 elsewhere). **The ledger has not started falling yet** — L2 is where -it does. +goes through the model. What remains is the ledger, and it now stands at **167**. + +| Area | Ledger sites | Seeded | Stage | +|---|---:|---:|---| +| `api/core` (`unfold` 16, `types_util` 14, `registry/walk` 9, `registry/scan` 2, `expand` 4) | 45 | 71 | L2 | +| `api/lang/cbindgen` | 25 | 25 | L3 | +| `api/lang/jnigen` | 97 | 106 | L4 | +| **total** | **167** | **202** | | + +**The ledger has started falling.** The whole −35 is +[#248](https://github.com/milyin/prebindgen/pull/248), which deleted the pattern +engine: `types_util` 40 → 14, `jnigen/builder` 13 → 4, and `registry` 11 split +into `walk` 9 + `scan` 2. Not one of those sites was migrated to read an element — +they went away because the code that held them went away, which is the cheaper +half of L2 and the reason it was done first. L1.5 before it moved reads off the +deleted maps but took only two classifiers off the ledger. Not every site must go: some inspect types the adapter itself *synthesized* — wire types, converter signatures — which is legitimately the adapter's business. @@ -116,9 +119,10 @@ moves it. | L0.5 | `Flat`: the model, indexed and resolved | **done** — this branch | | L1 | `Registry` consumes elements | **done** — [#238](https://github.com/milyin/prebindgen/pull/238) | | L1.5 | The model is the only index | **done** — #239–#246 | -| L2 | `api/core` stops classifying source syntax | not started | +| L1.75 | The registry becomes describable | **merged, not yet on this branch** — #249–#253 | +| L2 | `api/core` stops classifying source syntax | **in progress** — [#248](https://github.com/milyin/prebindgen/pull/248) took 35 of 71 | | L3 | `Cbindgen` consumes elements | not started | -| L4 | `JniGen` consumes elements *(the long pole — 106 sites)* | not started | +| L4 | `JniGen` consumes elements *(the long pole — 97 sites)* | not started | | L5 | Close the seam: the public contract stops being `syn` | not started | ### L0 — the parser — **done** (#227) @@ -262,19 +266,67 @@ because the map should show where the program actually went. **What is left in `Registry` is now genuinely its own**: the two type tables (adapter answers plus roots) and the five adapter-declared plan maps. -### L2 — `api/core` stops classifying source syntax - -- [ ] `types_util` — 40 sites, the largest single file. `normalize_type`, - `immediate_pattern_children`, `match_pattern`, the `is_*` predicates -- [ ] `registry::immediate_subtype_positions` — near-duplicate of - `immediate_pattern_children`, and the two already diverge on `Type::Path` -- [ ] `unfold` (16) and `expand` (4) read element types +### L1.75 — the registry becomes describable — **merged, not yet on this branch** + +Also not planned as a stage, and it moves no ledger sites — the count is 167 +before it and 167 after. It is here for the same reason L1.5 is: once L1.5 made +the registry a projection with nothing of its own to hide, its API could be +closed, and closing it is what makes a generator for a fourth language writable +by someone who has not read `resolve`. Tracked by +[#251](https://github.com/milyin/prebindgen/issues/251). + +- [x] **The caller states its declarations; the registry stops asking** + ([#249](https://github.com/milyin/prebindgen/pull/249)) — the five + decomposition callbacks become one handed-over value +- [x] **Say what the registry is for** + ([#250](https://github.com/milyin/prebindgen/pull/250)): *which type + conversions a binding needs, and whether it has them all.* Its module doc + had been a list of fields, and a stale one since #243 deleted them +- [x] **State the shape, then build it** + ([#252](https://github.com/milyin/prebindgen/pull/252)): `RegistryBuilder` + and `Registry` are two types because being-described and finished are two + states. 13 `Prebindgen` hooks called from 9 points inside `resolve` become + `describe, hand over the answers, read it`. **Nothing calls back into the + generator** — not by trait hook, and not by a `next_request`/`supply` pull + loop, which is the same protocol with the arrow reversed +- [x] **The generator owns the model and the registry** + ([#253](https://github.com/milyin/prebindgen/pull/253)): a build script + names one type. `JniGen::builder().source(..).build()` replaces the + `Flat::builder()` → `Registry::builder()` → `resolve` → `write_*` dance; + `Flat` and `Registry` stop being names a `build.rs` has to know + +**Merged into `flat-drop-pattern-engine`, not into this branch.** The stack landed +PR-into-PR, so the only commit of it that reached `language-integration` is #248's. +Re-merging that branch here is the next mechanical step, and nothing below should +be started on top of a branch that is 28 commits behind it. + +### L2 — `api/core` stops classifying source syntax — **in progress** + +- [x] **The pattern engine is deleted** + ([#248](https://github.com/milyin/prebindgen/pull/248)): `match_pattern`, + `unify`, `immediate_pattern_children`, `substitute_wildcards`, both rank + tables. The general machinery composed converters for any parametrized type; + its tables held **one** entry in the whole crate, `Result<_, _>`, which the + model already names `TypeKind::Fallible`. 592 deletions against 124 + insertions, and the `ConverterImpl` tail extracted verbatim rather than + rewritten. Ledger 202 → 167 +- [ ] `unfold` (16) — now the largest single file on the ledger +- [ ] `types_util` (14) — `normalize_type` and the `is_*` predicates, which is + what survived the engine's deletion +- [ ] `registry::walk::immediate_subtype_positions` (9) — the near-duplicate + outlived the original it duplicated, so there is no longer a divergence to + reconcile, only a walk to move onto the model +- [ ] `registry/scan` (2) and `expand` (4) read element types - [ ] `TypeKey` derivable from a `TypeRef` so a lookup stops routing through a spelling. L1.5 got the first half — `TypeKey` and the model's type index now share one canonicalization (`types_util::canonical_type`) - [ ] Ledger down by the migrated count; every entry that *stays* is justified in the PR as adapter-synthesized +**#248 is deletion, not migration**, and the distinction is worth keeping visible: +35 sites left because their code left. The 45 that remain are the ones that have +to actually start reading elements, so the rate so far is not the rate to expect. + ### L3 — `Cbindgen` consumes elements - [ ] `builder` (8), `trait_impl` (6), `emit` (5), `mod` (5), `convert` (1) @@ -286,11 +338,12 @@ because the map should show where the program actually went. ### L4 — `JniGen` consumes elements -The long pole. Split by area, each PR independently green. +The long pole — 97 sites, down from 106 because #248 took `jni/builder` from 13 to +4 with the rank tables. Split by area, each PR independently green. -- [ ] `emit/names` (17), `jni/builder` (13), `jni/trait_impl` (11), - `emit/wrapper` (11), `emit/flat_input` (10), `render` (8), `selector` (7), - `iface` (5), and the rest +- [ ] `emit/names` (17), `jni/trait_impl` (11), `emit/wrapper` (11), + `emit/flat_input` (10), `render` (8), `selector` (7), `iface` (5), + `jni/builder` (4), and the rest - [ ] `classify.rs` — a whole classifier with **zero** watched sites, so the ledger cannot see it: it must be migrated on its own merit - [ ] `prim_array_of` reads `ArrayExtent` instead of re-matching `Type::Array` From b3c793c100d580ad677b819d8b6b4ca6536e7b6b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 01:05:11 +0200 Subject: [PATCH 13/52] L1.75 is on this branch; I read the commit count instead of the trees (#255) #254 recorded the #249-#253 stack as merged elsewhere and pending a re-merge. It is not pending: #248 squash-merged flat-drop-pattern-engine AFTER #249 landed the stack into it, so d845c8f carries the registry and generator redesign under a title naming only the pattern engine. flat-drop-pattern-engine still reports 28 commits ahead because a squash records no ancestry. The trees differ by nothing, which is the check that should have been run: declare.rs, run.rs, view.rs and order.rs are present, convert_with is present, Registry::supply is gone, and both adapters expose builder(). Says so in the section, since the same misreading is available to anyone who opens the log. --- docs/language-integration.md | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 46e80fdc..2c8c529f 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -119,7 +119,7 @@ moves it. | L0.5 | `Flat`: the model, indexed and resolved | **done** — this branch | | L1 | `Registry` consumes elements | **done** — [#238](https://github.com/milyin/prebindgen/pull/238) | | L1.5 | The model is the only index | **done** — #239–#246 | -| L1.75 | The registry becomes describable | **merged, not yet on this branch** — #249–#253 | +| L1.75 | The registry becomes describable | **done** — #249–#253, squashed into #248's commit | | L2 | `api/core` stops classifying source syntax | **in progress** — [#248](https://github.com/milyin/prebindgen/pull/248) took 35 of 71 | | L3 | `Cbindgen` consumes elements | not started | | L4 | `JniGen` consumes elements *(the long pole — 97 sites)* | not started | @@ -266,7 +266,7 @@ because the map should show where the program actually went. **What is left in `Registry` is now genuinely its own**: the two type tables (adapter answers plus roots) and the five adapter-declared plan maps. -### L1.75 — the registry becomes describable — **merged, not yet on this branch** +### L1.75 — the registry becomes describable — **done** Also not planned as a stage, and it moves no ledger sites — the count is 167 before it and 167 after. It is here for the same reason L1.5 is: once L1.5 made @@ -295,10 +295,12 @@ by someone who has not read `resolve`. Tracked by `Flat::builder()` → `Registry::builder()` → `resolve` → `write_*` dance; `Flat` and `Registry` stop being names a `build.rs` has to know -**Merged into `flat-drop-pattern-engine`, not into this branch.** The stack landed -PR-into-PR, so the only commit of it that reached `language-integration` is #248's. -Re-merging that branch here is the next mechanical step, and nothing below should -be started on top of a branch that is 28 commits behind it. +**All of it is on this branch, in one commit.** The stack landed PR-into-PR onto +`flat-drop-pattern-engine`, and #248 squash-merged that branch afterwards, so +`d845c8f` — titled for the pattern engine — carries the registry and generator +redesign too. Do not read the commit log as the inventory: `flat-drop-pattern-engine` +still reports 28 commits ahead of `language-integration` because a squash records +no ancestry, while the trees differ by nothing. Diff the content, not the history. ### L2 — `api/core` stops classifying source syntax — **in progress** From baf0daea90f3d0cd173431b36fabd769adf90a98 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 02:25:22 +0200 Subject: [PATCH 14/52] L2a: the scan walks the model's edges, not the syntax (#257) registry/walk.rs is deleted. immediate_edges takes its structural children from TypeKind instead of taking a syn::Type apart, and spells each edge from the child's own origin.syntax -- classify off kind, spell off syntax. Three of the deleted arms were dead rather than migrated: lower_type refuses non-unit tuples and raw pointers, and Group/Paren are transparent in the model, so nothing the frontend accepts could reach them. The Type::Path generic-args arm is dead for a reason already written down in named() -- generic arguments are lowered but not retained, because no declaration takes type parameters. A composed type is ADMITTED to the model, not classified on the fly. The walk needs a reading for every type it is handed, and expansion composes spellings the source never wrote -- an Option around a T it found. My first attempt gave Flat a query that lowered on an index miss and answered without recording anything. That was wrong, and the tree already says so: add_local_function lowers a binding-local sig!(..) through the same grammar and ADMITS it, because otherwise the "one index" #243 established is a lie the moment a binding composes something. So Flat::admit_type is that function's peer, and ensure_entry -- the one place a cell is born, and therefore the one place a type enters the pipeline -- is where it is called. immediate_edges goes back to a plain index read, because by the time the walk reaches a type the cell for it already exists. Every later lookup gets the same answer from the same place. Measured after the change: ZERO types are refused by the grammar, across every in-tree example and all 523 tests. TypeSubject::Adapter is therefore unreachable, which is L2e's precondition -- left in place, with the evidence, for the PR that deletes it. Two more changes the walk forced, both improvements: The field lookup takes the type's NAME from TypeKind::Named rather than from bare_path_ident on the spelling. That is what makes a transparent wrapper work: Box classifies as Named { Node }, so it reaches Node's fields, where asking the syntax for a bare ident answered None and dead-ended. A declared type the source never mentions is now classified-but-placeless rather than unreadable. Foreign is a name, and the grammar can say that much about any spelling that parses; what is genuinely absent is a file and line. That is the reading-vs-position distinction L1.5 drew, applied to the case that shows why it matters. No production behaviour moves -- location() was already None for it, and kind() is test-only. registry/scan.rs keeps its two sites, with the reason in the code: they inspect a key a BUILD SCRIPT AUTHOR wrote, to diagnose that spelling. No source type is being classified, so there is no element to read instead. That is the map's "legitimately the adapter's business" case, and the first entry to actually land in it -- so L2a is 9 sites, not the 11 planned. Ledger 167 -> 158. Reported: regen-check drifts ONE file -- perftest-kotlin loses 50 lines, nothing added. They are JString_to_String_c7f3ca43 and its output twin, and they were provably dead: the committed file mentions that hash exactly twice, both definitions, zero call sites. Box IS String in the model, so the old syntactic walk registered a plain-String cell that nothing ever used. Explained: dead generated code stops being generated; no live converter, signature or Kotlin file moved. Asserted: every structural edge in the scan now comes from a classification, and every type in the table has one. cbindgen::type_contains_vec goes with it -- its one call site already held the TypeRef and was digging the syntax back out. TypeKind::Sequence is the whole question, since Cow<'_, [T]> lowers to it just as Vec does, so the two spellings it tested separately are one classification. is_vec and cow_slice_elem stay; they have other callers. --- .../perftest-kotlin/src/generated_bindings.rs | 50 --------- prebindgen/src/api/core/flat/boundary.ledger | 3 +- prebindgen/src/api/core/flat/mod.rs | 43 ++++++++ prebindgen/src/api/core/registry/mod.rs | 5 +- prebindgen/src/api/core/registry/scan.rs | 103 ++++++++++++++---- prebindgen/src/api/core/registry/tests.rs | 80 ++++++++++---- prebindgen/src/api/core/registry/walk.rs | 42 ------- prebindgen/src/api/lang/cbindgen/emit.rs | 11 +- prebindgen/src/api/lang/cbindgen/mod.rs | 10 -- 9 files changed, 197 insertions(+), 150 deletions(-) delete mode 100644 prebindgen/src/api/core/registry/walk.rs diff --git a/examples/perftest-kotlin/src/generated_bindings.rs b/examples/perftest-kotlin/src/generated_bindings.rs index 159c4acd..f82ee799 100644 --- a/examples/perftest-kotlin/src/generated_bindings.rs +++ b/examples/perftest-kotlin/src/generated_bindings.rs @@ -1109,32 +1109,6 @@ pub(crate) unsafe fn JString_to_Option_Box_String_071e4c8c<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] -pub(crate) unsafe fn JString_to_String_c7f3ca43<'env, 'v>( - env: &mut jni::JNIEnv<'env>, - v: &jni::objects::JString<'v>, -) -> ::core::result::Result { - Ok({ - let s = env - .get_string(v) - .map_err(|e| { - <__JniErr as ::core::convert::From< - String, - >>::from(format!("decode_string: {}", e)) - })?; - s.into() - }) -} -#[allow( - non_snake_case, - unused_mut, - unused_variables, - unused_braces, - dead_code, - clippy::needless_question_mark, - clippy::let_and_return, - clippy::nonminimal_bool, - clippy::eq_op -)] pub(crate) unsafe fn JString_to_std_boxed_Box_std_string_String_cfbab680<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JString<'v>, @@ -2734,30 +2708,6 @@ pub(crate) unsafe fn Storage_to_jlong_1b233abd<'a>( clippy::nonminimal_bool, clippy::eq_op )] -pub(crate) unsafe fn String_to_JString_c7f3ca43<'a>( - env: &mut jni::JNIEnv<'a>, - v: String, -) -> ::core::result::Result, __JniErr> { - Ok({ - env.new_string(v.as_str()) - .map_err(|e| { - <__JniErr as ::core::convert::From< - String, - >>::from(format!("encode_string: {}", e)) - })? - }) -} -#[allow( - non_snake_case, - unused_mut, - unused_variables, - unused_braces, - dead_code, - clippy::needless_question_mark, - clippy::let_and_return, - clippy::nonminimal_bool, - clippy::eq_op -)] pub(crate) unsafe fn TokenGc_to_jlong_5e58352a<'a>( env: &mut jni::JNIEnv<'a>, v: perftest_flat::TokenGc, diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 34a8796e..8063ef0d 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -46,7 +46,6 @@ 4 api/core/expand.rs 2 api/core/registry/scan.rs -9 api/core/registry/walk.rs 14 api/core/types_util.rs 16 api/core/unfold.rs 8 api/lang/cbindgen/builder.rs @@ -74,4 +73,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 167 +# total: 158 diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index ac8f70b9..c9b4ced0 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -650,6 +650,49 @@ impl Flat { .get(&crate::api::core::types_util::canonical_spelling(ty)) } + /// Admit `ty` to the model: lower it through the grammar, index the reading, + /// and answer with it. + /// + /// The peer of [`Self::add_local_function`], for the same reason and by the + /// same rule. A binding composes type *spellings* the source never wrote — an + /// `Option` around a `T` expansion found, a split overload's nullable arm — + /// and those are ordinary types in this language; they are simply not in an + /// index of what the source wrote. Both entry points therefore do the same + /// thing: lower through the one grammar, then **record the result**, so the + /// model still owns the only index of what a type means. + /// + /// Recording is the whole point, and is what separates this from classifying + /// on demand. Answering a caller and forgetting would make every later lookup + /// re-derive the same reading, and would leave the index disagreeing with what + /// the pipeline is actually working with — the "one index" #243 established + /// would be a lie the moment a binding composed anything. + /// + /// Idempotent: an already-indexed type is returned untouched, so the first + /// reading of a spelling wins, exactly as during ingestion. + /// + /// `Err` means the composed spelling is outside the accepted grammar. That is a + /// real diagnosis about a type the *binding* built, not a cache miss. + pub(crate) fn admit_type(&mut self, ty: &syn::Type) -> Result<&TypeRef, UnsupportedType> { + let key = crate::api::core::types_util::canonical_spelling(ty); + if !self.by_type.contains_key(&key) { + // Rebuilt rather than kept, for the reason `lower_signature` gives: a + // stored index would be a second copy of what `constants()` says. + let consts = ConstIndex::new(self.constants().map(|c| { + ( + c.name.to_string(), + (*c.origin.syntax.expr).clone(), + c.origin.crate_name().map(str::to_owned), + ) + })); + // No file wrote this one; `has_position` already gates what a + // diagnostic prints for a positionless location. + let at = Rc::new(SourceLocation::default()); + let reading = lower_type(ty, &consts, &at)?; + self.by_type.insert(key.clone(), reading); + } + Ok(self.by_type.get(&key).expect("just inserted")) + } + /// Index every type the element at `pos` writes. Idempotent per key: the /// first mention in element order wins. fn index_types_of(&mut self, pos: usize) { diff --git a/prebindgen/src/api/core/registry/mod.rs b/prebindgen/src/api/core/registry/mod.rs index 0e9a90e4..8c61b1f2 100644 --- a/prebindgen/src/api/core/registry/mod.rs +++ b/prebindgen/src/api/core/registry/mod.rs @@ -178,7 +178,6 @@ mod order; mod run; mod scan; mod view; -mod walk; pub use self::{ cell::{Direction, TypeEntry}, @@ -186,8 +185,10 @@ pub use self::{ error::{DuplicateNameError, NotExpressibleEntry, ScanError, WriteRustError}, key::{TypeKey, TypeKeyParseError}, view::{Building, Conversions, Crossing}, - walk::{extract_fn_trait_args, immediate_subtype_positions}, }; +/// The callback grammar, which the source language owns — re-exported here for the +/// call sites that have not yet reached L2–L4 of #229. +pub use crate::api::core::flat::extract_fn_trait_args; /// Single owner of everything parsed from the prebindgen source stream. /// diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index 481c5626..7476683e 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -20,6 +20,15 @@ impl Registry { // match — the flat namespace keys are bare) or names a genuinely // foreign type (supported verbatim; warned about below only when it // shadows a captured item's name — the likely-mistake heuristic). + // + // The two syntax matches below **stay** as this file's boundary-ledger + // entries, and the reason is what they look at: `declared.types` are keys a + // *build script author* wrote, and this is a diagnostic about the spelling + // they wrote — is it path-qualified, and does its tail shadow a captured + // item? No source type is being classified, so there is no element to read + // instead; asking the model would answer about a type rather than about the + // declaration. This is the "legitimately the adapter's business" case the + // integration map (L2, #229) predicts, not a migration still owed. let mut qualified: Vec<(String, String)> = Vec::new(); let mut probed: HashSet<&TypeKey> = HashSet::new(); for key in declared @@ -241,14 +250,26 @@ impl Registry { /// Create the cell for `ty` in `dir` if it has none, and mark it a root when /// the binding asked for it directly. /// - /// The one place a cell is born, which is what lets the subject be decided - /// once: the model's reading if the flat API mentions this type, an - /// adapter-authored type otherwise. + /// The one place a cell is born, and therefore the one place a type **enters + /// the pipeline** — so it is where a type the source never wrote is admitted to + /// the model. Expansion composes such spellings (an `Option` around a `T` it + /// found) and hands them straight here via `require_input` / `require_output`. + /// + /// Admitting rather than classifying on the fly is the rule + /// [`Flat::add_local_function`](crate::api::core::flat::Flat::add_local_function) + /// already set for a binding-local `sig!(..)`: lower through the one grammar, + /// then record it, so the model keeps owning the only index of what a type + /// means. Every later lookup — this scan, the resolver, an adapter — then gets + /// the same answer from the same place. + /// + /// A spelling the grammar refuses leaves the cell subject-less. Nothing in tree + /// reaches that (measured: every composed type lowers), and #229's L2e is where + /// it is re-measured and the variant deleted. pub(super) fn ensure_entry(&mut self, dir: Direction, ty: &syn::Type, root: bool) { let key = TypeKey::from_type(ty); - let subject = match self.flat.type_ref(ty) { - Some(t) => TypeSubject::Source(Box::new(t.clone())), - None => TypeSubject::Adapter, + let subject = match self.flat.admit_type(ty) { + Ok(t) => TypeSubject::Source(Box::new(t.clone())), + Err(_) => TypeSubject::Adapter, }; let cell = self .type_table_mut(dir) @@ -261,36 +282,74 @@ impl Registry { cell.root |= root; } - /// Enumerate the immediate type-graph edges out of `(dir, ty)`: - /// generic args / Fn args / tuple elements / ref/array/slice/ptr targets, - /// plus — if `ty` is the bare ident of an indexed struct or enum — the - /// field types of that struct/enum. + /// Enumerate the immediate type-graph edges out of `(dir, ty)`: the model's + /// own children of this type, plus — if `ty` names a declared struct or sum — + /// the field types of that item. + /// + /// A callback's argument types flow with `dir.flip()`, because an argument the + /// binding *hands to* a callback crosses the other way; everything else + /// inherits `dir`. Used by both `register_type_inner` (during scan) and the + /// unresolved-descendants BFS in `resolve` (for diagnostics). + /// + /// The children come from [`TypeKind`], not from taking the syntax apart, and + /// the difference is load-bearing rather than cosmetic. `&mut MaybeUninit` + /// is `Ref { mode: Out, inner: T }` — the model absorbed the `MaybeUninit`, so + /// the edge lands on `T` directly instead of on an intermediate + /// `MaybeUninit` that no source ever wrote and no adapter can convert. + /// Each edge is still *spelled* from the child's own `origin.syntax`, which is + /// what the caller keys the table by. /// - /// `impl Fn(args)` arg types flow with `dir.flip()`; everything else - /// inherits `dir`. Used by both `register_type_inner` (during scan) and - /// the unresolved-descendants BFS in `resolve` (for diagnostics). + /// A plain index read: `ensure_entry` admitted this type to the model before + /// the walk reached it, so the reading is already there — including for a + /// spelling the binding composed. No reading means the grammar refused the + /// type, and a refused type has no structure to walk. pub(crate) fn immediate_edges( &self, dir: Direction, ty: &syn::Type, ) -> Vec<(Direction, syn::Type)> { + use crate::api::core::flat::TypeKind; + let mut out: Vec<(Direction, syn::Type)> = Vec::new(); - let (positions, child_dir) = if let Some(args) = extract_fn_trait_args(ty) { - (args, dir.flip()) - } else { - (immediate_subtype_positions(ty), dir) - }; - for sub in positions { - out.push((child_dir, sub)); + if let Some(reading) = self.flat.type_ref(ty) { + let (children, child_dir): (Vec<&crate::api::core::flat::TypeRef>, Direction) = + match &reading.kind { + TypeKind::Optional(t) + | TypeKind::Sequence(t) + | TypeKind::Ref { inner: t, .. } => (vec![t], dir), + TypeKind::Array { elem, .. } => (vec![elem], dir), + TypeKind::Fallible { ok, err } => (vec![ok, err], dir), + TypeKind::Callback { args } => (args.iter().collect(), dir.flip()), + // A name is a leaf in the type graph: its generic arguments are + // lowered but not retained, because no declaration takes type + // parameters. Its *fields* are the edges, and they come off the + // element below. + TypeKind::Named { .. } + | TypeKind::Scalar(_) + | TypeKind::Str + | TypeKind::Unit => (Vec::new(), dir), + }; + for child in children { + out.push((child_dir, child.origin.syntax.clone())); + } } // A declared type's own fields, read off the element rather than off its // `syn::Fields`: a positional field is an ordinary `Field` there, so the // named-only asymmetry the syntax walk had does not arise. An `Enum` has // no fields and an `Extern` declares none, which is what makes both // contribute nothing here. - if let Some(name) = bare_path_ident(ty) { + // + // The **name comes from the classification**, not from taking the spelling + // apart, and that is what makes a transparent wrapper work: `Box` is + // `Named { id: Node }` — `Box` **is** `T` in this language — so it + // reaches `Node`'s fields, where asking the syntax for a bare ident would + // have answered `None` and dead-ended the walk. + if let Some(name) = self.flat.type_ref(ty).and_then(|r| match &r.kind { + TypeKind::Named { id } => Some(id.name.clone()), + _ => None, + }) { use crate::api::core::flat::{Field, Type}; - let fields: Vec<&Field> = match self.flat.declared_type(&name) { + let fields: Vec<&Field> = match self.flat.declared_type(name.as_str()) { Some(Type::Struct(s)) => s.fields.iter().collect(), Some(Type::Variant(v)) => v .alternatives diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 0c15aff7..b58871a1 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -797,11 +797,24 @@ fn a_source_type_cell_carries_the_models_typeref() { assert!(matches!(inner.subject.kind(), Some(TypeKind::Scalar(_)))); } -/// A type only the binding authored has **no** reading and no location — a fact -/// about it, not information that went missing. Declaring a type the source -/// never mentions is the ordinary way to reach this state. +/// A type only the binding authored is **classified but placeless**: it has a +/// reading, because it is a type in this language, and no location, because no +/// source wrote it. Declaring a type the source never mentions is the ordinary +/// way to reach this state. +/// +/// The two are separate facts, and this is the case that shows why. `Foreign` is +/// a name — the frontend can say that much about any spelling that parses, +/// whether or not it can *resolve* it — so refusing to classify it would be +/// throwing away an answer the grammar has. What is genuinely absent is a file +/// and line, and only that. +/// +/// So the cell gets its reading from `ensure_entry`, which admits a composed or +/// declared type to the model on the way in, the same way `add_local_function` +/// admits a binding-local `sig!(..)`. #[test] -fn an_adapter_authored_type_cell_has_no_source_reading() { +fn an_adapter_authored_type_cell_is_classified_but_placeless() { + use crate::api::core::flat::TypeKind; + let items = vec![fn_item("fn f(x: u64) -> u64 { x }")]; let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); @@ -816,9 +829,15 @@ fn an_adapter_authored_type_cell_has_no_source_reading() { let cell = ®.input_types[&TypeKey::parse("Foreign").expect("test type")]; assert!(cell.root, "the binding asked for it directly"); - assert!(matches!(cell.subject, TypeSubject::Adapter)); - assert!(cell.subject.kind().is_none()); - assert_eq!(cell.subject.location(), None); + assert!( + matches!(cell.subject.kind(), Some(TypeKind::Named { id }) if id.name == "Foreign"), + "a declared name is a name, and the grammar can say so" + ); + assert_eq!( + cell.subject.location(), + None, + "nothing wrote it, so there is no position to report" + ); } // ── The projection itself ────────────────────────────────────────────── @@ -1460,21 +1479,40 @@ fn a_recursive_type_is_handed_out_once_and_terminates() { } } - // And `Node` really is a cycle: it reaches itself. + // And the fixture really is cyclic — otherwise "terminates" above says nothing. + // Walked with the registry's own edges, so this exercises the graph the cycle + // guard walks rather than a second walk that could drift from it. + // + // Stated as "some key repeats" rather than "`Node` reaches `Node`", because the + // loop does not pass back through that spelling: the fixture's field is + // `Option>`, `Box` **is** `T` in this language, and the model + // therefore keeps the `Box` spelling while classifying it `Named { Node }`. + // The loop is `Option>` → `Box` → `Option>`. Which + // spellings sit in the cycle is a modelling detail; that there *is* one is the + // fixture property this test needs. let node = TypeKey::parse("Node").expect("test type"); - let reaches_self = reg - .immediate_edges(Direction::Output, &node.to_type()) - .into_iter() - .any(|(_, t)| { - crate::api::core::registry::immediate_subtype_positions(&t) - .into_iter() - .any(|inner| { - crate::api::core::registry::immediate_subtype_positions(&inner) - .into_iter() - .any(|i2| TypeKey::from_type(&i2) == node) - }) - }); - assert!(reaches_self, "fixture must actually be recursive"); + let mut seen_keys: Set = Set::new(); + let mut frontier = vec![node.to_type()]; + let mut revisited = false; + for _ in 0..8 { + let mut next = Vec::new(); + for t in frontier { + if !seen_keys.insert(TypeKey::from_type(&t)) { + revisited = true; + break; + } + next.extend( + reg.immediate_edges(Direction::Output, &t) + .into_iter() + .map(|(_, sub)| sub), + ); + } + if revisited { + break; + } + frontier = next; + } + assert!(revisited, "fixture must actually be recursive"); } /// A built `Registry` has no route back into being described. diff --git a/prebindgen/src/api/core/registry/walk.rs b/prebindgen/src/api/core/registry/walk.rs deleted file mode 100644 index fae39092..00000000 --- a/prebindgen/src/api/core/registry/walk.rs +++ /dev/null @@ -1,42 +0,0 @@ -//! Structural type-graph helpers, shared by the scan and the diagnostics BFS. - -// ────────────────────────────────────────────────────────────────────── -// Helpers -// ────────────────────────────────────────────────────────────────────── - -/// Immediate child type positions of `ty` (one level deep). -pub fn immediate_subtype_positions(ty: &syn::Type) -> Vec { - match ty { - syn::Type::Path(p) => { - if let Some(last) = p.path.segments.last() { - if let syn::PathArguments::AngleBracketed(ab) = &last.arguments { - return ab - .args - .iter() - .filter_map(|a| { - if let syn::GenericArgument::Type(t) = a { - Some(t.clone()) - } else { - None - } - }) - .collect(); - } - } - vec![] - } - syn::Type::Reference(r) => vec![(*r.elem).clone()], - syn::Type::Tuple(t) => t.elems.iter().cloned().collect(), - syn::Type::Array(a) => vec![(*a.elem).clone()], - syn::Type::Slice(s) => vec![(*s.elem).clone()], - syn::Type::Ptr(p) => vec![(*p.elem).clone()], - syn::Type::Group(g) => immediate_subtype_positions(&g.elem), - syn::Type::Paren(p) => immediate_subtype_positions(&p.elem), - syn::Type::ImplTrait(_) => extract_fn_trait_args(ty).unwrap_or_default(), - _ => vec![], - } -} - -/// The callback grammar, which the source language owns — re-exported here for -/// the existing call sites until they consume elements (stages L2–L4 of #229). -pub use crate::api::core::flat::extract_fn_trait_args; diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index bb61d8ec..c9470d58 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -282,6 +282,10 @@ impl CbindgenBuilder { /// Whether any declared function returns a `Vec<_>` (possibly nested under /// `Result`/`Option`), so the array builder/freer prelude must be emitted. + /// + /// `TypeKind::Sequence` is the whole question: it is what `Vec` lowers to, + /// and — since `Cow<'_, T>` **is** `T` — what `Cow<'_, [T]>` lowers to as well, + /// so the two spellings this used to test separately are one classification. pub(super) fn produces_array(&self, registry: &Registry<()>) -> bool { self.functions.keys().any(|orig| { registry @@ -289,7 +293,12 @@ impl CbindgenBuilder { .function(&orig) // The model already decided that an elided return and `-> ()` // are one thing, so there is no second arm to write here. - .map(|f| type_contains_vec(&f.ret.origin.syntax)) + .map(|f| { + f.ret + .walk() + .iter() + .any(|t| matches!(t.kind, crate::api::core::flat::TypeKind::Sequence(_))) + }) .unwrap_or(false) }) } diff --git a/prebindgen/src/api/lang/cbindgen/mod.rs b/prebindgen/src/api/lang/cbindgen/mod.rs index 7b68ad30..a70a149d 100644 --- a/prebindgen/src/api/lang/cbindgen/mod.rs +++ b/prebindgen/src/api/lang/cbindgen/mod.rs @@ -649,16 +649,6 @@ fn is_scalar(ty: &syn::Type) -> bool { .unwrap_or(false) } -/// Whether an array-producing output appears anywhere in `ty` (including nested -/// under `Result`/`Option`/references). -fn type_contains_vec(ty: &syn::Type) -> bool { - is_vec(ty) - || cow_slice_elem(ty).is_some() - || crate::api::core::registry::immediate_subtype_positions(ty) - .iter() - .any(type_contains_vec) -} - /// If `ty` is `Cow<'_, [E]>` with scalar `E`, return `E`. fn cow_slice_elem(ty: &syn::Type) -> Option { let syn::Type::Path(tp) = ty else { From 66ac79915a2e2f7fd2ed048ef9992cfea20f388c Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 10:03:40 +0200 Subject: [PATCH 15/52] L2e + L2d: every cell carries a reading, and spelling moves home (#258) * L2e + L2d: every cell carries a reading, and spelling moves home Two stages of L2 (#229), plus a finding that a third does not exist. L2e -- TypeSubject is deleted. The enum had two variants: a type the flat API contains, and "a type only the binding authored", which carried no reading on the theory that a declared wire type or an unfold leaf had none to give. It did. Those are ordinary types in this language; they were only absent from an index of what the SOURCE wrote. L2a made ensure_entry admit them, and the re-measurement promised there found ZERO grammar refusals across every in-tree example and all 523 tests -- so the second variant had no members left. TypeCell::subject is now the TypeRef itself. A spelling the grammar really does refuse is a ScanError::NotExpressible naming it, rather than a cell that quietly means less than its neighbours: ensure_entry returns Result, and every caller already did. Only an entry point can reach that error at all -- a type the walk found came from an existing reading's origin.syntax, so it lowered once already. location() and kind() go with the enum. The has_position() gate moves to the two resolve.rs sites that were its only production readers, unchanged: a reading and a reportable position stay two facts, which is the whole reason a binding-local sig!(..) does not report `:0:0:`. L2d -- spelling moves into core::flat. type_from_ident, canonical_type, canonical_spelling, normalize_type, normalize_item_types, reduce_flat_path, constructor_key and Normalization become core::flat::spelling. They decide what spelling a type HAS before anything keys on it, which is the same authority that decides what a type MEANS, and they were sitting in types_util next to the classifiers looking like one of them. Checked rather than assumed: all eight have ZERO callers outside api/core, and normalize_* is called only by the frontend itself. That is what makes them movable while the rest of types_util waits on L3/L4 -- option_inner_type has 40 adapter callers, bare_path_ident 22, is_unit 18. A move, not a migration, and labelled as one: the four ledger sites leave api/core because the code left, exactly as #248's did. types_util 14 -> 10. L2c does not exist. api/core has ZERO production callers of enum_shape, first_payload_variant or enum_discriminant_values -- the three I counted when planning were all in types_util/tests.rs, which my grep excluded types_util.rs but not its test module. Every real caller is in an adapter, so the whole stage is L3/L4 work and there is nothing here to do. L2b is NOT in this PR. It is 20 sites, and only about half are the Option/Vec/ref fold that a layer read on TypeRef replaces. The rest ask different questions -- a slice element, a callback argument, is_nominated on a peeled element -- and migrating the easy half would leave unfold.rs holding both idioms, which is worse than either and moves the ledger barely at all. It gets its own PR, with the helper's shape driven by all 20 sites rather than the first two. I wrote that helper, found it did not survive contact with the harder sites, and removed it rather than ship an unused public API. Ledger 158 -> 154. Reported: regen-check is byte-identical on every committed artifact. It also reports two UNTRACKED example_flat_aarch64_unstable.{rs,h} files -- verified identical on language-integration with the same clean+rebuild, so that is the pre-existing #219 disagreement, not this change. Explained: no generated output moved, and none should have -- both stages are internal. Asserted: every type-table cell carries the frontend's reading of its type, and there is no second kind that carries nothing. * Consult the grammar, do not extend the model Correcting the shape L2a introduced and this branch carried further. ensure_entry called Flat::admit_type, which LOWERED a composed spelling and then inserted it into Flat's by_type index. That index is documented as "every type the API mentions", and an Option the binding composed is not mentioned by any API -- so the write destroyed the one thing the index is good for, and made Flat stop being what the source said. The reasoning that got me there was that add_local_function sets a precedent for admitting. It does not. A binding-local sig!(..) is an API ITEM -- a function the binding declares as if it had been marked -- so it belongs in the model. A composed type is not an item; it is an intermediate in one binding's crossing graph, and the table that tracks crossings is where it belongs. So: Flat::classify replaces admit_type. It answers from the index when the source wrote the type, lowers when it did not, and REMEMBERS NOTHING. Flat has no &mut method left except add_local_function and its private indexer. The registry keeps the answer, because the registry is what asked. A cell's reading is taken once, when the cell is born, and lives in the cell -- so the classification happens at registry-building time and every type the pipeline works with has its reading in the table by the time the builder is done. ensure_entry now also checks the table first, so a repeat registration does no work at all rather than re-lowering and discarding. immediate_edges reads that table instead of Flat's index. A spelling the binding composed is then answered exactly like one the source wrote, without asking the model about a type it never saw. Same for the field lookup that takes a declared type's name off TypeKind::Named. The test fixture stops fabricating a Flat. test_util::cell built an EMPTY Flat purely to borrow its grammar, which is the same misuse in miniature: Flat is the marked flat library, not a classifier one can instantiate for parts. It existed only because the fixtures poked cells straight into the type table. Registry::insert_crossing replaces it -- a cfg(test) method that goes through ensure_entry and then attaches the conversion. A fixture table is now reached the same way a real one is, a hand-written key is held to the same grammar, and there is no second construction path for a cell to drift along. No behaviour change: 523 tests pass and regen-check is byte-identical on every committed artifact. * A not-expressible report omits a position it does not have Review catch on #258, and correct: ensure_entry builds its NotExpressibleEntry with SourceLocation::default(), which the report renders as `:0:0` -- a position that reads as real. The codebase already names this exact hazard in has_position's own doc, and I reintroduced it. Fixed in the renderer rather than at my call site, because the other producer has the same latent fault: declare.rs takes the location from the element's origin, and a hand-built stream carries the default there too. One guard where both route through, and the crate name still prints when it is known even if the file is not -- with no position that is the only thing left identifying the offender. Not the placeholder file the review suggested. "" would still occupy the position slot, and this codebase already answered the question: print nothing rather than something that looks like a place. The reason string already names the offending type, so name: None loses nothing -- UnsupportedType's Display carries `offending`. Test included, and sabotage-checked: forcing the guard true fails it. It is separate from the unresolved-type test that pins the same rule, because this is a separate Display arm -- the two were written months apart and only one had the guard, which is how this got through. * Name the offender, not a `#[prebindgen]` item; record the API break Three review findings, all real on this head. 1. The report called every offender a `#[prebindgen]` item. Two populations reach ScanError::NotExpressible and only one is a marked item. The other is a type the BINDING put on the boundary -- a declared crossing, or a spelling expansion composed -- which no source crate wrote and whose author would go looking for a `#[prebindgen]` that is not there. The path is newly reachable: such a type used to become a cell with no reading and no complaint, so the diagnostic never ran. Header now reads "the flat language cannot express N of this binding's items and types", and each entry's own line says which it is. The `:0:0` half of this finding was fixed in e491355; this is the half that was left. Tested end-to-end rather than by hand-building the error: declaring `*const u8` as a crossing now produces the flat language cannot express 1 of this binding's items and types: type `* const u8` is a form the prebindgen source language does not accept -- no `#[prebindgen]`, no invented position, and the offending type named. 2. The spelling move is an API break, and was not stated. `prebindgen::core::types_util` is a `pub mod`, so these were reachable: type_from_ident, canonical_type, canonical_spelling -> MOVED, still public, now prebindgen::core::flat::{..} normalize_type, normalize_item_types, Normalization -> MOVED AND NARROWED to pub(crate) The narrowing is deliberate. They are ingest-time internals -- Normalization::from_items runs once inside FlatBuilder::build, and a consumer is handed already-normalized spellings -- so nothing outside the crate has a reason to call them, and leaving them public would advertise a second way to canonicalize that must agree with the model's. No compatibility re-exports, per the 0.5 policy that this is a fully new API rather than one carrying shims. The break is stated here and in the PR because there is no changelog to state it in. 3. The doc still described deleted machinery. docs/language-integration.md named `TypeSubject` as a live two-variant type and `types_util::canonical_type` as a live path, and its L2 checklist still listed the walker and spelling work as outstanding. Both descriptions are now historical where they are about what a stage found, and the L2 section records what landed and points at #229, which is where stage state is edited. --- docs/language-integration.md | 55 ++-- prebindgen/src/api/core/diagnostics.rs | 4 +- prebindgen/src/api/core/flat/boundary.ledger | 4 +- prebindgen/src/api/core/flat/element.rs | 2 +- prebindgen/src/api/core/flat/mod.rs | 78 +++-- prebindgen/src/api/core/flat/spelling.rs | 284 ++++++++++++++++++ .../src/api/core/flat/tests/acceptance.rs | 2 +- prebindgen/src/api/core/registry/cell.rs | 57 +--- prebindgen/src/api/core/registry/error.rs | 38 ++- prebindgen/src/api/core/registry/key.rs | 6 +- prebindgen/src/api/core/registry/mod.rs | 2 +- prebindgen/src/api/core/registry/scan.rs | 150 ++++++--- prebindgen/src/api/core/registry/tests.rs | 144 +++++++-- prebindgen/src/api/core/resolve.rs | 8 +- prebindgen/src/api/core/resolve/tests.rs | 72 ++--- prebindgen/src/api/core/types_util.rs | 268 ----------------- prebindgen/src/api/core/write/tests.rs | 68 ++--- .../src/api/lang/jnigen/jni/tests/mod.rs | 6 +- prebindgen/src/api/test_util.rs | 17 +- 19 files changed, 704 insertions(+), 561 deletions(-) create mode 100644 prebindgen/src/api/core/flat/spelling.rs diff --git a/docs/language-integration.md b/docs/language-integration.md index 2c8c529f..5efac843 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -238,9 +238,12 @@ because the map should show where the program actually went. "one index" would be a lie, since a `sig!(..)` never passed through the parser - [x] **The type table carries the reading** ([#239](https://github.com/milyin/prebindgen/pull/239)): a cell is - `TypeCell { subject, root, entry }` where `TypeSubject` is either the - frontend's `TypeRef` or an adapter-authored type. `required` stopped being - stored — it was one name over three storages — and is derived by `resolve` + `TypeCell { subject, root, entry }`, the subject being the frontend's + `TypeRef`. `required` stopped being stored — it was one name over three + storages — and is derived by `resolve`. The subject was originally a + two-variant `TypeSubject`, the second variant meaning *"a type only the + binding authored, with no reading"*; L2 found that population empty and + deleted it, so every cell now carries a reading - [x] **`const _` is a `Guard`, not a `Constant`** ([#240](https://github.com/milyin/prebindgen/pull/240)): an anonymous const has no address, so it is not API. Four sentinel `ident == "_"` checks had @@ -257,7 +260,8 @@ because the map should show where the program actually went. ([#246](https://github.com/milyin/prebindgen/pull/246)): the last index living outside its owner. `from_flat` collapses to *check expressibility, store the model*. Canonicalization becomes one definition - (`types_util::canonical_type`) that both the index and `TypeKey` derive from + (`canonical_type`, moved into `core::flat::spelling` by L2) that both the + index and `TypeKey` derive from - [x] **A reading and a reportable position are different facts**: a synthesized signature has readings but no file, so `SourceLocation::has_position` gates what diagnostics print. Fixed a pre-existing `:0:0:` for hand-built streams @@ -312,22 +316,39 @@ no ancestry, while the trees differ by nothing. Diff the content, not the histor model already names `TypeKind::Fallible`. 592 deletions against 124 insertions, and the `ConverterImpl` tail extracted verbatim rather than rewritten. Ledger 202 → 167 -- [ ] `unfold` (16) — now the largest single file on the ledger -- [ ] `types_util` (14) — `normalize_type` and the `is_*` predicates, which is - what survived the engine's deletion -- [ ] `registry::walk::immediate_subtype_positions` (9) — the near-duplicate - outlived the original it duplicated, so there is no longer a divergence to - reconcile, only a walk to move onto the model -- [ ] `registry/scan` (2) and `expand` (4) read element types -- [ ] `TypeKey` derivable from a `TypeRef` so a lookup stops routing through a - spelling. L1.5 got the first half — `TypeKey` and the model's type index - now share one canonicalization (`types_util::canonical_type`) -- [ ] Ledger down by the migrated count; every entry that *stays* is justified in - the PR as adapter-synthesized +- [x] **The scan walks the model's edges** + ([#257](https://github.com/milyin/prebindgen/pull/257)): `registry/walk.rs` + is deleted and `immediate_edges` takes its children from `TypeKind`. Three of + its arms were dead rather than migrated — the grammar refuses non-unit tuples + and raw pointers, and `Group`/`Paren` are transparent. Ledger 167 → 158 +- [x] **A composed type is classified where it enters, and the answer is kept**: + expansion builds spellings the source never wrote, so `ensure_entry` asks the + grammar once, when a cell is born, and stores the reading **in that cell**. + `Flat` is consulted, never extended — its index means *what the source + wrote*, and a wire-side intermediate is not that +- [x] **Every cell carries a reading**: with the above, the "no reading" half of + `TypeSubject` had no members left (measured: zero refusals across every + in-tree example and the whole suite), so the enum is gone. A spelling the + grammar really does refuse is now a reported error naming it, rather than a + cell that quietly means less than its neighbours +- [x] **Spelling moves to its owner**: `canonical_type`, `normalize_type`, + `type_from_ident` and the rest become `core::flat::spelling`. They decide what + spelling a type *has* before anything keys on it — the same authority that + decides what it *means*. Ledger 158 → 154 **#248 is deletion, not migration**, and the distinction is worth keeping visible: -35 sites left because their code left. The 45 that remain are the ones that have +35 sites left because their code left. The ones that remain are the ones that have to actually start reading elements, so the rate so far is not the rate to expect. +The same caveat applies to the spelling move above, which is a **move**. + +**What is left, and why it is not all of it.** Every classifying helper still in +`api/core/types_util` is called overwhelmingly from the adapters — +`option_inner_type` 40 times, `bare_path_ident` 22, `is_unit` 18 — and none takes +the model as an argument, so it cannot consult it from the inside. L2 can stop +`api/core` from *calling* them; only L3 and L4 can free them to be deleted. The +remaining migration (`unfold` 16, `expand` 4) and the running plan live in +[#229](https://github.com/milyin/prebindgen/pull/229), which is where stage state +is edited. ### L3 — `Cbindgen` consumes elements diff --git a/prebindgen/src/api/core/diagnostics.rs b/prebindgen/src/api/core/diagnostics.rs index 4c20f7e6..8b185087 100644 --- a/prebindgen/src/api/core/diagnostics.rs +++ b/prebindgen/src/api/core/diagnostics.rs @@ -12,10 +12,10 @@ use std::collections::HashSet; use crate::api::core::{ - flat::Flat, + flat::{type_from_ident, Flat}, prebindgen::NamePredicate, registry::TypeKey, - types_util::{bare_path_ident, type_from_ident}, + types_util::bare_path_ident, }; /// What a binding claimed, so everything else can be reported. diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 8063ef0d..6b8bca5e 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -46,7 +46,7 @@ 4 api/core/expand.rs 2 api/core/registry/scan.rs -14 api/core/types_util.rs +10 api/core/types_util.rs 16 api/core/unfold.rs 8 api/lang/cbindgen/builder.rs 1 api/lang/cbindgen/convert.rs @@ -73,4 +73,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 158 +# total: 154 diff --git a/prebindgen/src/api/core/flat/element.rs b/prebindgen/src/api/core/flat/element.rs index 1bb7ea81..da54ba28 100644 --- a/prebindgen/src/api/core/flat/element.rs +++ b/prebindgen/src/api/core/flat/element.rs @@ -144,7 +144,7 @@ impl Type { /// thereafter the only way to spell that type inside the flat API, and the /// qualified path stays refused. This declares a name; it is not an equivalence /// between spellings — see -/// [`normalize_type`](crate::api::core::types_util::normalize_type)'s rule 4 for +/// [`normalize_type`](crate::api::core::flat::spelling::normalize_type)'s rule 4 for /// why treating it as one is a category error. /// * `#[prebindgen] pub struct X(..);` — a tuple struct, whose fields no adapter /// has ever crossed. diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index c9b4ced0..bf3c4950 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -166,6 +166,7 @@ mod boundary; mod element; mod origin; pub mod spell; +pub(crate) mod spelling; mod ty; #[cfg(test)] @@ -179,6 +180,7 @@ pub use self::{ Struct, Type, Unsupported, Variant, }, origin::Origin, + spelling::{canonical_spelling, canonical_type, type_from_ident}, ty::{RefMode, ScalarKind, TypeId, TypeKind, TypeRef, UnsupportedType, UnsupportedTypeReason}, }; use crate::SourceLocation; @@ -329,9 +331,9 @@ impl FlatBuilder { // The consequence is deliberate and stated on `Origin`: a slice // is the spelling generation must EMIT, which is the normalized one — // the flat namespace is what the generated crate can actually name. - let normalization = crate::api::core::types_util::Normalization::from_items(&items); + let normalization = crate::api::core::flat::spelling::Normalization::from_items(&items); for (item, _) in &mut items { - crate::api::core::types_util::normalize_item_types(item, &normalization); + crate::api::core::flat::spelling::normalize_item_types(item, &normalization); } // Pass 1: the consts an array length may name. Unnamed items are @@ -647,50 +649,46 @@ impl Flat { /// captured one does. pub fn type_ref(&self, ty: &syn::Type) -> Option<&TypeRef> { self.by_type - .get(&crate::api::core::types_util::canonical_spelling(ty)) + .get(&crate::api::core::flat::canonical_spelling(ty)) } - /// Admit `ty` to the model: lower it through the grammar, index the reading, - /// and answer with it. + /// This module's reading of `ty` — the index's if the source wrote it, freshly + /// lowered if not. /// - /// The peer of [`Self::add_local_function`], for the same reason and by the - /// same rule. A binding composes type *spellings* the source never wrote — an - /// `Option` around a `T` expansion found, a split overload's nullable arm — - /// and those are ordinary types in this language; they are simply not in an - /// index of what the source wrote. Both entry points therefore do the same - /// thing: lower through the one grammar, then **record the result**, so the - /// model still owns the only index of what a type means. + /// **Answers without remembering, and that is deliberate.** The model is what + /// the source said, and stays that way: [`Self::type_ref`]'s index means *every + /// type the API mentions*, so growing it with a spelling no source wrote would + /// destroy the one thing it is good for. This is the grammar being consulted, + /// not the model being extended. /// - /// Recording is the whole point, and is what separates this from classifying - /// on demand. Answering a caller and forgetting would make every later lookup - /// re-derive the same reading, and would leave the index disagreeing with what - /// the pipeline is actually working with — the "one index" #243 established - /// would be a lie the moment a binding composed anything. + /// It is therefore **not** the peer of [`Self::add_local_function`], which does + /// extend the model — a binding-local `sig!(..)` is an API item, a function the + /// binding declares as if it had been marked. A composed type is not an API + /// item; it is an intermediate in some binding's crossing graph, and it belongs + /// in the table that tracks crossings. /// - /// Idempotent: an already-indexed type is returned untouched, so the first - /// reading of a spelling wins, exactly as during ingestion. + /// Whoever asks is expected to keep the answer. The registry does: a reading is + /// taken once when a type-table cell is born, and lives in that cell. /// - /// `Err` means the composed spelling is outside the accepted grammar. That is a - /// real diagnosis about a type the *binding* built, not a cache miss. - pub(crate) fn admit_type(&mut self, ty: &syn::Type) -> Result<&TypeRef, UnsupportedType> { - let key = crate::api::core::types_util::canonical_spelling(ty); - if !self.by_type.contains_key(&key) { - // Rebuilt rather than kept, for the reason `lower_signature` gives: a - // stored index would be a second copy of what `constants()` says. - let consts = ConstIndex::new(self.constants().map(|c| { - ( - c.name.to_string(), - (*c.origin.syntax.expr).clone(), - c.origin.crate_name().map(str::to_owned), - ) - })); - // No file wrote this one; `has_position` already gates what a - // diagnostic prints for a positionless location. - let at = Rc::new(SourceLocation::default()); - let reading = lower_type(ty, &consts, &at)?; - self.by_type.insert(key.clone(), reading); + /// `Err` means the spelling is outside the accepted grammar — a real diagnosis + /// about a type the *binding* built, not a cache miss. + pub(crate) fn classify(&self, ty: &syn::Type) -> Result { + if let Some(indexed) = self.type_ref(ty) { + return Ok(indexed.clone()); } - Ok(self.by_type.get(&key).expect("just inserted")) + // Rebuilt rather than kept, for the reason `lower_signature` gives: a + // stored index would be a second copy of what `constants()` says. + let consts = ConstIndex::new(self.constants().map(|c| { + ( + c.name.to_string(), + (*c.origin.syntax.expr).clone(), + c.origin.crate_name().map(str::to_owned), + ) + })); + // No file wrote this one; `has_position` already gates what a diagnostic + // prints for a positionless location. + let at = Rc::new(SourceLocation::default()); + lower_type(ty, &consts, &at) } /// Index every type the element at `pos` writes. Idempotent per key: the @@ -703,7 +701,7 @@ impl Flat { .collect(); for ty in refs { self.by_type - .entry(crate::api::core::types_util::canonical_spelling( + .entry(crate::api::core::flat::canonical_spelling( &ty.origin.syntax, )) .or_insert(ty); diff --git a/prebindgen/src/api/core/flat/spelling.rs b/prebindgen/src/api/core/flat/spelling.rs new file mode 100644 index 00000000..ea415488 --- /dev/null +++ b/prebindgen/src/api/core/flat/spelling.rs @@ -0,0 +1,284 @@ +//! Spelling: how a captured type is written, reduced to one canonical form. +//! +//! Ingest-time machinery, and the frontend's own — it decides what spelling a +//! type *has* before anything keys on it, which is the same authority that +//! decides what a type *means*. It lived in `api/core/types_util` until #229's +//! L2d, next to the classifiers, where it looked like one of them; it is not. +//! Nothing outside `api/core` ever called it. +//! +//! Two things key on [`canonical_type`] — this module's type index and +//! [`TypeKey`](crate::core::TypeKey) — and they must agree, which is why the +//! reduction has exactly one definition and neither spells it out itself. +//! +//! The ledger counts *constructing* a watched syn variant as well as matching +//! one, so [`type_from_ident`] is here for the same reason the rest is: writing +//! a `syn::Type::Path` is spelling, and spelling belongs to the module that owns +//! the grammar. + +use std::collections::HashMap; + +use crate::SourceLocation; + +/// The single-segment path type for a bare item ident (`Foo` → `Foo`) — +/// direct construction, no string round trip, cannot fail. +pub fn type_from_ident(ident: &syn::Ident) -> syn::Type { + syn::Type::Path(syn::TypePath { + qself: None, + path: syn::Path::from(ident.clone()), + }) +} + +/// Normalize a type to its canonical flat-namespace spelling (issue #95). +/// The COMPLETE equivalence rule set — any spelling not listed is preserved +/// verbatim: +/// +/// 1. `Type::Group` / `Type::Paren` wrappers unwrap (`(Foo)` ≡ `Foo`). +/// 2. A multi-segment path headed by `crate` / `self` reduces to its final +/// segment, keeping that segment's generic arguments (`crate::a::Foo` +/// ≡ `Foo`). Sound because the flat namespace indexes at most one +/// item per bare ident, and a `crate::` path in a captured item can only +/// denote the source crate's own item. +/// 3. A multi-segment path headed by a name in `source_modules` (the +/// `#[prebindgen]` source crates chained into the registry, +/// hyphens-as-underscores) reduces the same way (`myflat::Foo` ≡ `Foo`). +/// Pure callers pass `&[]`. +/// 4. A **prelude** path reduces to the bare name the language knows it by — +/// exactly [`Normalization::PRELUDE`], with `core`/`alloc` read as `std`. +/// Each entry names a *constructor*, so arguments are preserved: +/// `std::vec::Vec` ≡ `Vec`. +/// +/// Nothing else. `std::ffi::CString` stays qualified, and so does a +/// foreign path (`zenoh::KeyExpr`) **even when an alias names that +/// type**: a `#[prebindgen] pub type` is a one-way road, bringing a +/// foreign type into the flat API under a name that is thereafter the +/// only way to spell it. It declares +/// an [`Extern`](crate::core::flat::Extern); it is not an equivalence. +/// +/// That keeps the rule meaning-preserving, which is the whole contract +/// here: reduction may choose among spellings of ONE type, never change +/// what a type is. Treating an alias as an equivalence broke that — +/// `Vec` ≡ `Bytes` turns a sequence into an extern — and no +/// key-shape refinement fixes the category error. +/// 5. Lifetimes are NOT normalized (`&'a T` ≠ `&T`, `Foo<'static>` ≠ `Foo`) +/// — a lifetime is part of the spelling a foreign-type declaration relies +/// on (`ptr_class!(ZKeyExpr<'static>)`), so collapsing it would make two +/// distinct declarations collide. +/// +/// Idempotent; recurses through references, slices, tuples, pointers, +/// generic arguments, and `impl Trait` bounds. Paths with a qualified self +/// (`::Assoc`) are left untouched. +/// What a captured path may be reduced against: the ingested source crates' own +/// modules, and every name an alias gives to a foreign path. +/// +/// One value rather than a bare `&[String]`, because reduction has one rule and +/// two sources of aliases feeding it — see [`normalize_type`]'s rule list. +/// [`Self::default`] is the prelude alone, which is what a caller normalizing a +/// lone type (rather than an ingested stream) wants. +#[derive(Clone, Debug)] +pub struct Normalization { + /// Module name per ingested source, first-seen order. The first doubles as the + /// default module for references with no recorded origin. + pub source_modules: Vec, + /// Constructor path → the bare name the language knows it by, from + /// [`Self::PRELUDE`] alone. Matched with the use site's type arguments ignored + /// and preserved, because a prelude entry names a constructor: + /// `std::vec::Vec` is every `Vec`. + /// + /// A crate's `#[prebindgen] pub type` is deliberately **not** here — see + /// [`normalize_type`]'s rule 4. + constructors: HashMap, +} + +impl Normalization { + /// The names the language **pre-declares**, so no source crate has to write + /// them — exactly Rust's own idea of a prelude, a set of `use`s you need not + /// write. A crate need not write `use std::vec::Vec`, and need not write + /// `#[prebindgen] pub type Vec = std::vec::Vec` either, for the same reason. + /// + /// Not identical to Rust's prelude: it adds `MaybeUninit`, which the grammar + /// recognises for out-parameters, and `Cow`, which it treats as transparent. + /// Its entries are exactly the bare names + /// [`lower_path`](crate::core::flat) classifies as builtins and that have a + /// std path at all — `str` has none, and neither do the scalars. + /// + /// Written with the `std` root; `core` and `alloc` are re-exports of the same + /// items, so a leading `core`/`alloc` is read as `std` before matching. + pub const PRELUDE: &'static [(&'static str, &'static str)] = &[ + ("std::vec::Vec", "Vec"), + ("std::option::Option", "Option"), + ("std::result::Result", "Result"), + ("std::string::String", "String"), + ("std::boxed::Box", "Box"), + ("std::mem::MaybeUninit", "MaybeUninit"), + ("std::borrow::Cow", "Cow"), + ]; + + /// The prelude alone: no ingested sources, no declared aliases. + pub fn prelude() -> Self { + Self { + source_modules: Vec::new(), + constructors: Self::PRELUDE + .iter() + .map(|(path, name)| ((*path).to_string(), (*name).to_string())) + .collect(), + } + } + + /// Collect from a captured stream, before anything is normalized. + /// + /// The single entry point — `FlatBuilder::build` — builds + /// this, so they cannot normalize differently. Gathering every module and alias + /// first is what makes reduction order-independent: a signature may name a type + /// whose alias is declared later, or in another source. + pub fn from_items(items: &[(syn::Item, SourceLocation)]) -> Self { + let mut out = Self::prelude(); + for (_, loc) in items { + if let Some(crate_name) = &loc.crate_name { + let module = crate_name.replace('-', "_"); + if !out.source_modules.contains(&module) { + out.source_modules.push(module); + } + } + } + out + } + + /// The bare name the language knows this constructor by, arguments ignored. + fn constructor_of(&self, path: &syn::Path) -> Option<&str> { + self.constructors + .get(&constructor_key(path)) + .map(String::as_str) + } +} + +impl Default for Normalization { + fn default() -> Self { + Self::prelude() + } +} + +/// A path as a key: segments joined, arguments dropped, and a leading +/// `core`/`alloc` read as `std` since they re-export the same items. +/// +/// Only [`Normalization::constructors`] is keyed this way, and a constructor is +/// exactly a path without arguments — `std::vec::Vec` matches every `Vec`. +fn constructor_key(path: &syn::Path) -> String { + let mut out = String::new(); + for (i, seg) in path.segments.iter().enumerate() { + if i > 0 { + out.push_str("::"); + } + let mut ident = seg.ident.to_string(); + if i == 0 && (ident == "core" || ident == "alloc") { + ident = "std".to_string(); + } + out.push_str(&ident); + } + out +} + +/// A type reduced to the spelling everything keys on: prelude-normalized, so +/// `std::option::Option` and `Option` are one entry. +/// +/// The **single** definition of that reduction. Two things key on it — the +/// model's type index ([`Flat::type_ref`](crate::core::flat::Flat::type_ref)) +/// and [`TypeKey`](crate::core::TypeKey) — and they have to agree, so neither +/// spells it out itself. +/// +/// Deliberately `prelude()` rather than a source-module-aware normalization: a +/// key must mean the same thing before and after ingestion knows what the source +/// modules are. +pub fn canonical_type(ty: &syn::Type) -> syn::Type { + let mut t = ty.clone(); + normalize_type(&mut t, &Normalization::prelude()); + t +} + +/// [`canonical_type`] as tokens — the string form both indexes use as their key. +pub fn canonical_spelling(ty: &syn::Type) -> String { + use quote::ToTokens; + canonical_type(ty).to_token_stream().to_string() +} + +pub fn normalize_type(ty: &mut syn::Type, against: &Normalization) { + use syn::visit_mut::VisitMut; + struct Normalizer<'a> { + against: &'a Normalization, + } + impl VisitMut for Normalizer<'_> { + fn visit_type_mut(&mut self, ty: &mut syn::Type) { + // Unwrap (possibly nested) group/paren wrappers in place. + loop { + match ty { + syn::Type::Group(g) => *ty = (*g.elem).clone(), + syn::Type::Paren(p) => *ty = (*p.elem).clone(), + _ => break, + } + } + if let syn::Type::Path(tp) = ty { + if tp.qself.is_none() { + reduce_flat_path(&mut tp.path, self.against); + } + } + syn::visit_mut::visit_type_mut(self, ty); + } + } + Normalizer { against }.visit_type_mut(ty); +} + +/// Apply [`normalize_type`] to every type position inside an item — fn +/// signatures, struct fields, enum variants, const types. The ingest-time +/// pass ([`crate::api::core::flat::FlatBuilder::build`]) that makes +/// captured spellings canonical before any key is formed, so every +/// downstream `TypeKey::from_type` sees the flat spelling. +pub fn normalize_item_types(item: &mut syn::Item, against: &Normalization) { + use syn::visit_mut::VisitMut; + + struct ItemNormalizer<'a> { + against: &'a Normalization, + } + impl VisitMut for ItemNormalizer<'_> { + fn visit_type_mut(&mut self, ty: &mut syn::Type) { + // Normalizes the whole subtree; no further descent needed. + normalize_type(ty, self.against); + } + } + ItemNormalizer { against }.visit_item_mut(item); +} + +/// The path-reduction step of [`normalize_type`]: collapse a reducible +/// multi-segment path to its final segment. See the rule list there. +fn reduce_flat_path(path: &mut syn::Path, against: &Normalization) { + if path.segments.len() < 2 { + return; + } + + // A prelude entry names a CONSTRUCTOR, so arguments are ignored when matching + // and preserved when rewriting: `std::vec::Vec` is `Vec`. A crate's + // own alias is NOT consulted — see rule 4. + if let Some(name) = against.constructor_of(path) { + let mut last = path.segments.last().expect("len checked").clone(); + last.ident = syn::Ident::new(name, last.ident.span()); + path.leading_colon = None; + path.segments = std::iter::once(last).collect(); + return; + } + + // Otherwise only a prefix into the flat namespace reduces, to the final + // segment: this crate's own path, or an ingested source's module. + let head = path + .segments + .first() + .expect("len checked") + .ident + .to_string(); + let reduce = match head.as_str() { + "crate" | "self" => true, + other => against.source_modules.iter().any(|m| m == other), + }; + if reduce { + let last = path.segments.last().expect("len checked").clone(); + path.leading_colon = None; + path.segments = std::iter::once(last).collect(); + } +} diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index c0c59993..bbe41cff 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -120,7 +120,7 @@ fn a_box_classifies_as_what_it_wraps() { /// slipped through and worked only when the source happened to `use` it. #[test] fn the_prelude_reaches_every_builtin_by_either_spelling() { - use crate::api::core::types_util::Normalization; + use crate::api::core::flat::spelling::Normalization; // Each entry, bare against fully qualified. `MaybeUninit` needs a `&mut` to // mean anything, so it is checked separately below. diff --git a/prebindgen/src/api/core/registry/cell.rs b/prebindgen/src/api/core/registry/cell.rs index 3de3d14d..0d408b34 100644 --- a/prebindgen/src/api/core/registry/cell.rs +++ b/prebindgen/src/api/core/registry/cell.rs @@ -3,52 +3,21 @@ use super::*; -/// What a type-table key names. -/// -/// Two populations, and saying which is which is what keeps one origin per cell: -/// a type the flat API contains **is** a [`TypeRef`](crate::api::core::flat::TypeRef), reused whole, so its -/// classification and its source location are already there. -#[derive(Clone, Debug)] -pub(crate) enum TypeSubject { - /// A type the flat API contains — the frontend's own reading, unmodified. - Source(Box), - /// A type only the binding authored: a declared wire type with no - /// `#[prebindgen]` item behind it, an [`unfold`](crate::api::core::unfold) - /// leaf. It has no reading and no source location — a fact about it, rather - /// than information that went missing. - Adapter, -} - -impl TypeSubject { - /// Where the source wrote this type, or `None` when no source did. - pub fn location(&self) -> Option<&SourceLocation> { - match self { - // Having a reading and having a reportable position are different - // facts: a binding-local fn's types are lowered — so they have - // readings — against no file at all. Reporting `:0:0` would invent a - // position; `None` says what is true. - TypeSubject::Source(t) => Some(&*t.origin.location).filter(|l| l.has_position()), - TypeSubject::Adapter => None, - } - } - - /// The frontend's classification, or `None` for an adapter-authored type. - /// - /// Test-only: the cells carry it so a test can pin that a source reading - /// survives into the table, but no production path re-reads it. - #[cfg(test)] - pub fn kind(&self) -> Option<&crate::api::core::flat::TypeKind> { - match self { - TypeSubject::Source(t) => Some(&t.kind), - TypeSubject::Adapter => None, - } - } -} - /// One type-table cell: what the key names, and the adapter's answer for it. pub(crate) struct TypeCell { - /// The type itself, as the frontend reads it when it can. - pub subject: TypeSubject, + /// The frontend's reading of this type, reused whole — so its classification + /// and its origin are already here rather than re-derived per consumer. + /// + /// **Every** cell has one. There used to be a second variant for "a type only + /// the binding authored", on the assumption that a declared wire type or an + /// [`unfold`](crate::api::core::unfold) leaf had no reading to give. It did: + /// those are ordinary types in this language, they were simply absent from an + /// index of what the *source* wrote. `ensure_entry` takes the reading from the + /// grammar when the cell is born and stores it right here, so it is always + /// present, and a spelling the grammar genuinely refuses is a + /// [`ScanError::NotExpressible`] naming it rather than a cell that quietly means + /// less than its neighbours. + pub subject: Box, /// The binding asks for this cell **directly** — a declared fn's signature, a /// declared type, an `unfold` leaf — as opposed to reaching it through some /// converter's [`TypeEntry::subs`]. diff --git a/prebindgen/src/api/core/registry/error.rs b/prebindgen/src/api/core/registry/error.rs index 1963428f..4374013c 100644 --- a/prebindgen/src/api/core/registry/error.rs +++ b/prebindgen/src/api/core/registry/error.rs @@ -106,9 +106,15 @@ impl fmt::Display for ScanError { ) } ScanError::NotExpressible { entries } => { + // Not "`#[prebindgen]` item(s)": two populations reach this report + // and only one of them is a marked item. The other is a type the + // *binding* put on the boundary — a declared crossing, or a + // spelling expansion composed — which no source crate ever wrote + // and whose author would go looking for a `#[prebindgen]` that is + // not there. Each entry's own line says which it is. write!( f, - "{} `#[prebindgen]` item(s) the flat language cannot express:", + "the flat language cannot express {} of this binding's items and types:", entries.len() )?; for e in entries { @@ -116,15 +122,29 @@ impl fmt::Display for ScanError { // several sources, two offenders both read `src/lib.rs:..` // and the location alone says nothing about which one to fix. // Same reason the duplicate-name diagnostic carries it. - let in_crate = match &e.location.crate_name { - Some(c) => format!(" in crate `{c}`"), - None => String::new(), - }; - match &e.name { - Some(name) => { - write!(f, "\n {}{in_crate}: {name} {}", e.location, e.reason)? + // + // Gated on `has_position`, because not every offender has a + // place: a type a binding composed was never written in a file, + // and neither was an item from a hand-built stream. Rendering + // the default location anyway prints `:0:0:`, which reads as a + // real position — the fault this whole `has_position` split + // exists to prevent, and it is worse than saying nothing. + let mut prefix = String::new(); + if e.location.has_position() { + prefix.push_str(&e.location.to_string()); + } + if let Some(c) = &e.location.crate_name { + if !prefix.is_empty() { + prefix.push(' '); } - None => write!(f, "\n {}{in_crate}: {}", e.location, e.reason)?, + prefix.push_str(&format!("in crate `{c}`")); + } + if !prefix.is_empty() { + prefix.push_str(": "); + } + match &e.name { + Some(name) => write!(f, "\n {prefix}{name} {}", e.reason)?, + None => write!(f, "\n {prefix}{}", e.reason)?, } } Ok(()) diff --git a/prebindgen/src/api/core/registry/key.rs b/prebindgen/src/api/core/registry/key.rs index df32d995..0ce807b1 100644 --- a/prebindgen/src/api/core/registry/key.rs +++ b/prebindgen/src/api/core/registry/key.rs @@ -5,7 +5,7 @@ use std::fmt; use quote::ToTokens; /// Canonical type-shape key: identity is the token string of the -/// **normalized** type ([`crate::api::core::types_util::normalize_type`] — +/// **normalized** type ([`crate::api::core::flat::spelling::normalize_type`] — /// group/paren unwrap, `crate::`/`self::` and std-prelude path reduction; /// the complete equivalence rule set is documented there). The normalized /// parsed form is kept alongside the string, so [`Self::to_type`] is an @@ -79,7 +79,7 @@ impl TypeKey { pub fn from_type(ty: &syn::Type) -> Self { // Off the shared reduction, so this key and the model's type index // cannot drift apart about what a type is called. - let t = crate::api::core::types_util::canonical_type(ty); + let t = crate::api::core::flat::canonical_type(ty); Self { canon: t.to_token_stream().to_string().into(), ty: std::rc::Rc::new(t), @@ -89,7 +89,7 @@ impl TypeKey { /// Build a key for a bare item ident — infallible by construction (an /// ident IS a single-segment path type; nothing to parse or normalize). pub fn from_ident(ident: &syn::Ident) -> Self { - Self::from_type(&crate::api::core::types_util::type_from_ident(ident)) + Self::from_type(&crate::api::core::flat::type_from_ident(ident)) } /// The canonical string form. diff --git a/prebindgen/src/api/core/registry/mod.rs b/prebindgen/src/api/core/registry/mod.rs index 8c61b1f2..663e12e0 100644 --- a/prebindgen/src/api/core/registry/mod.rs +++ b/prebindgen/src/api/core/registry/mod.rs @@ -169,7 +169,7 @@ use crate::{ }; mod cell; -pub(crate) use self::cell::{TypeCell, TypeSubject}; +pub(crate) use self::cell::TypeCell; mod declare; mod error; mod key; diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index 7476683e..6024cbda 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -102,7 +102,7 @@ impl Registry { // the type is required in the output direction only. for ident in declared.consts.iter().flatten() { if let Some(item_const) = self.flat.constant(&ident).map(|c| c.origin.syntax.clone()) { - self.ensure_entry(Direction::Output, &item_const.ty, true); + self.ensure_entry(Direction::Output, &item_const.ty, true)?; } else { missing.push(("constant", ident.to_string())); } @@ -116,7 +116,7 @@ impl Registry { // Declared crossings with no element behind them (a foreign class type, // a synthesized constant's value type), each in its own direction. for (dir, ty) in &declared.crossings { - self.ensure_entry(*dir, ty, true); + self.ensure_entry(*dir, ty, true)?; } // Scan declared types. @@ -130,13 +130,13 @@ impl Registry { .map(|s| s.origin.syntax.clone()) { self.scan_struct(&s)?; - self.ensure_entry(Direction::Input, &ty, true); - self.ensure_entry(Direction::Output, &ty, true); + self.ensure_entry(Direction::Input, &ty, true)?; + self.ensure_entry(Direction::Output, &ty, true)?; matched = true; } else if let Some(e) = self.flat.enum_item(&ident).cloned() { self.scan_enum(&e)?; - self.ensure_entry(Direction::Input, &ty, true); - self.ensure_entry(Direction::Output, &ty, true); + self.ensure_entry(Direction::Input, &ty, true)?; + self.ensure_entry(Direction::Output, &ty, true)?; matched = true; } } @@ -145,8 +145,8 @@ impl Registry { // `ptr_class(ZKeyExpr<'static>)` on a re-exported // foreign type). Still mark required so the resolver // tries to produce a converter for it. - self.ensure_entry(Direction::Input, &ty, true); - self.ensure_entry(Direction::Output, &ty, true); + self.ensure_entry(Direction::Input, &ty, true)?; + self.ensure_entry(Direction::Output, &ty, true)?; } } @@ -182,9 +182,9 @@ impl Registry { pub(super) fn scan_struct(&mut self, s: &syn::ItemStruct) -> Result<(), ScanError> { // The struct itself can appear in either direction. - let ty: syn::Type = crate::api::core::types_util::type_from_ident(&s.ident); - self.ensure_entry(Direction::Input, &ty, false); - self.ensure_entry(Direction::Output, &ty, false); + let ty: syn::Type = crate::api::core::flat::type_from_ident(&s.ident); + self.ensure_entry(Direction::Input, &ty, false)?; + self.ensure_entry(Direction::Output, &ty, false)?; if let syn::Fields::Named(named) = &s.fields { for field in &named.named { @@ -196,9 +196,9 @@ impl Registry { } pub(super) fn scan_enum(&mut self, e: &syn::ItemEnum) -> Result<(), ScanError> { - let ty: syn::Type = crate::api::core::types_util::type_from_ident(&e.ident); - self.ensure_entry(Direction::Input, &ty, false); - self.ensure_entry(Direction::Output, &ty, false); + let ty: syn::Type = crate::api::core::flat::type_from_ident(&e.ident); + self.ensure_entry(Direction::Input, &ty, false)?; + self.ensure_entry(Direction::Output, &ty, false)?; for variant in &e.variants { for field in &variant.fields { @@ -239,7 +239,7 @@ impl Registry { return Ok(()); // cycle guard } - self.ensure_entry(dir, ty, is_top); + self.ensure_entry(dir, ty, is_top)?; for (child_dir, sub) in self.immediate_edges(dir, ty) { self.register_type_inner(child_dir, &sub, false, visited)?; @@ -251,35 +251,54 @@ impl Registry { /// the binding asked for it directly. /// /// The one place a cell is born, and therefore the one place a type **enters - /// the pipeline** — so it is where a type the source never wrote is admitted to - /// the model. Expansion composes such spellings (an `Option` around a `T` it - /// found) and hands them straight here via `require_input` / `require_output`. + /// the pipeline** — including a spelling the source never wrote, since expansion + /// composes those (an `Option` around a `T` it found) and hands them straight + /// here via `require_input` / `require_output`. /// - /// Admitting rather than classifying on the fly is the rule - /// [`Flat::add_local_function`](crate::api::core::flat::Flat::add_local_function) - /// already set for a binding-local `sig!(..)`: lower through the one grammar, - /// then record it, so the model keeps owning the only index of what a type - /// means. Every later lookup — this scan, the resolver, an adapter — then gets - /// the same answer from the same place. + /// The reading is taken **here, once**, and lives in the cell. The model is + /// consulted for it — [`Flat::classify`](crate::api::core::flat::Flat::classify) + /// is the grammar's one answer — but the model is not extended: a composed + /// spelling is an intermediate in *this binding's* crossing graph, not something + /// the source API mentions, and the table that tracks crossings is where it + /// belongs. So `Flat` stays what the source said, and every type the pipeline + /// works with has its reading in the table by the time the builder is finished. /// - /// A spelling the grammar refuses leaves the cell subject-less. Nothing in tree - /// reaches that (measured: every composed type lowers), and #229's L2e is where - /// it is re-measured and the variant deleted. - pub(super) fn ensure_entry(&mut self, dir: Direction, ty: &syn::Type, root: bool) { + /// A spelling the grammar refuses is reported by name, rather than becoming a + /// cell that quietly means less than its neighbours. Only an *entry point* can + /// reach that: a type the walk found came from an existing reading's + /// `origin.syntax`, so it lowered once already. + pub(super) fn ensure_entry( + &mut self, + dir: Direction, + ty: &syn::Type, + root: bool, + ) -> Result<(), ScanError> { let key = TypeKey::from_type(ty); - let subject = match self.flat.admit_type(ty) { - Ok(t) => TypeSubject::Source(Box::new(t.clone())), - Err(_) => TypeSubject::Adapter, - }; - let cell = self - .type_table_mut(dir) - .entry(key) - .or_insert_with(|| TypeCell { - subject, - root: false, + // Classify only when the cell is actually new: the reading of a given key + // cannot change, so an existing cell already holds it. + if let Some(cell) = self.type_table_mut(dir).get_mut(&key) { + cell.root |= root; + return Ok(()); + } + let subject = self + .flat + .classify(ty) + .map_err(|source| ScanError::NotExpressible { + entries: vec![NotExpressibleEntry { + name: None, + reason: source.to_string(), + location: SourceLocation::default(), + }], + })?; + self.type_table_mut(dir).insert( + key, + TypeCell { + subject: Box::new(subject), + root, entry: None, - }); - cell.root |= root; + }, + ); + Ok(()) } /// Enumerate the immediate type-graph edges out of `(dir, ty)`: the model's @@ -299,10 +318,11 @@ impl Registry { /// Each edge is still *spelled* from the child's own `origin.syntax`, which is /// what the caller keys the table by. /// - /// A plain index read: `ensure_entry` admitted this type to the model before - /// the walk reached it, so the reading is already there — including for a - /// spelling the binding composed. No reading means the grammar refused the - /// type, and a refused type has no structure to walk. + /// The reading comes from **this registry's own table**, where `ensure_entry` + /// put it before the walk reached this type — so a spelling the binding composed + /// is answered exactly like one the source wrote, without asking the model about + /// a type it never saw. No cell means the type was never registered, and an + /// unregistered type is not part of any crossing to walk. pub(crate) fn immediate_edges( &self, dir: Direction, @@ -311,7 +331,11 @@ impl Registry { use crate::api::core::flat::TypeKind; let mut out: Vec<(Direction, syn::Type)> = Vec::new(); - if let Some(reading) = self.flat.type_ref(ty) { + if let Some(reading) = self + .type_table(dir) + .get(&TypeKey::from_type(ty)) + .map(|c| &c.subject) + { let (children, child_dir): (Vec<&crate::api::core::flat::TypeRef>, Direction) = match &reading.kind { TypeKind::Optional(t) @@ -344,10 +368,14 @@ impl Registry { // `Named { id: Node }` — `Box` **is** `T` in this language — so it // reaches `Node`'s fields, where asking the syntax for a bare ident would // have answered `None` and dead-ended the walk. - if let Some(name) = self.flat.type_ref(ty).and_then(|r| match &r.kind { - TypeKind::Named { id } => Some(id.name.clone()), - _ => None, - }) { + if let Some(name) = self + .type_table(dir) + .get(&TypeKey::from_type(ty)) + .and_then(|c| match &c.subject.kind { + TypeKind::Named { id } => Some(id.name.clone()), + _ => None, + }) + { use crate::api::core::flat::{Field, Type}; let fields: Vec<&Field> = match self.flat.declared_type(name.as_str()) { Some(Type::Struct(s)) => s.fields.iter().collect(), @@ -365,6 +393,30 @@ impl Registry { out } + /// Put a crossing in the table with its conversion already decided — the + /// fixture form of "this type crosses, and here is how". + /// + /// Goes through [`Self::ensure_entry`] rather than building a cell beside it, + /// so a fixture table is reached the same way a real one is and a hand-written + /// key is held to the same grammar. A test that wants the whole scan builds its + /// registry from items instead; this is for the ones that need a specific table + /// shape and nothing else. + #[cfg(test)] + pub(crate) fn insert_crossing( + &mut self, + dir: Direction, + key: &TypeKey, + root: bool, + entry: Option>, + ) { + self.ensure_entry(dir, &key.to_type(), root) + .unwrap_or_else(|e| panic!("fixture key `{key}` is not expressible: {e}")); + self.type_table_mut(dir) + .get_mut(key) + .expect("just registered") + .entry = entry; + } + /// Register `ty` (and its nested positions) as a required **input** so /// the resolver produces a converter for it. Used by /// [`crate::api::core::expand`] to pull in the leaf types a fold needs. diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index b58871a1..96d27e11 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -785,16 +785,16 @@ fn a_source_type_cell_carries_the_models_typeref() { let cell = ®.input_types[&key]; assert!(cell.root, "a top-level parameter is a root"); assert!( - matches!(cell.subject.kind(), Some(TypeKind::Optional(_))), + matches!(cell.subject.kind, TypeKind::Optional(_)), "the frontend classified it, so the cell has that classification" ); // One location per cell, and it is the model's — not a copy the scan made. - assert_eq!(cell.subject.location(), Some(&loc)); + assert_eq!(&*cell.subject.origin.location, &loc); // The nested position is in the model too, and is not a root. let inner = ®.input_types[&TypeKey::parse("u64").expect("test type")]; assert!(!inner.root); - assert!(matches!(inner.subject.kind(), Some(TypeKind::Scalar(_)))); + assert!(matches!(inner.subject.kind, TypeKind::Scalar(_))); } /// A type only the binding authored is **classified but placeless**: it has a @@ -808,9 +808,10 @@ fn a_source_type_cell_carries_the_models_typeref() { /// throwing away an answer the grammar has. What is genuinely absent is a file /// and line, and only that. /// -/// So the cell gets its reading from `ensure_entry`, which admits a composed or -/// declared type to the model on the way in, the same way `add_local_function` -/// admits a binding-local `sig!(..)`. +/// So the cell gets its reading from `ensure_entry`, which asks the grammar once +/// when the cell is born. The model is consulted, not extended: a declared type the +/// source never mentioned is this binding's business, not a new fact about the +/// source API. #[test] fn an_adapter_authored_type_cell_is_classified_but_placeless() { use crate::api::core::flat::TypeKind; @@ -830,12 +831,11 @@ fn an_adapter_authored_type_cell_is_classified_but_placeless() { let cell = ®.input_types[&TypeKey::parse("Foreign").expect("test type")]; assert!(cell.root, "the binding asked for it directly"); assert!( - matches!(cell.subject.kind(), Some(TypeKind::Named { id }) if id.name == "Foreign"), + matches!(&cell.subject.kind, TypeKind::Named { id } if id.name == "Foreign"), "a declared name is a name, and the grammar can say so" ); - assert_eq!( - cell.subject.location(), - None, + assert!( + !cell.subject.origin.location.has_position(), "nothing wrote it, so there is no position to report" ); } @@ -1001,7 +1001,12 @@ fn not_expressible_report_names_the_crate_of_each_offender() { }; let msg = err.to_string(); - assert!(msg.contains("2 `#[prebindgen]` item(s)"), "{msg}"); + // Not "`#[prebindgen]` item(s)" — a declared crossing reaches this same report + // and is not one. These two are marked items, and their own lines say so. + assert!( + msg.contains("cannot express 2 of this binding's items and types"), + "{msg}" + ); assert!(msg.contains("in crate `myflat`"), "{msg}"); assert!(msg.contains("in crate `helpers`"), "{msg}"); // Both share a file path, so the crate is the only thing telling them apart. @@ -1354,30 +1359,24 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { .expect("a local fn's parameter type is in the model"); assert!(matches!(read.kind, TypeKind::Optional(_))); - // … and the cell scanned from that parameter carries it, rather than - // claiming the type is one the binding invented. + // … and the cell scanned from that parameter carries that same reading, + // rather than a second one made at the table. let cell = ®.input_types[&TypeKey::parse("Option").expect("test type")]; - assert!( - matches!(cell.subject, TypeSubject::Source(_)), - "the frontend read this type; the cell must not call it adapter-authored" - ); - assert!(matches!(cell.subject.kind(), Some(TypeKind::Optional(_)))); + assert!(matches!(cell.subject.kind, TypeKind::Optional(_))); } /// A type with no source position must not get an invented one. /// -/// Three facts have to stay apart: a type can have a **frontend reading** -/// (`TypeSubject::Source`), a **reportable position**, or neither. A -/// binding-local fn's parameter types have the first and not the second — -/// `lower_signature` lowers them against `SourceLocation::default()`, since -/// `Origin` needs a location and a `sig!(..)` has no file. +/// A **reading** and a **reportable position** are two facts, and every cell now +/// has the first: a binding-local fn's parameter types are lowered against +/// `SourceLocation::default()`, because `Origin` needs a location and a `sig!(..)` +/// has no file. So the reading exists and the position does not. /// -/// Indexing those types (this PR) flipped their cells from `Adapter` to -/// `Source`, and `location()` returned the default unconditionally, so the -/// diagnostic read `:0:0: error:` — a position that looks real. The same fault -/// already showed for any hand-built stream, whose captured items also carry -/// default locations; both are fixed by asking whether the location has a -/// position at all. +/// When indexing those types first gave their cells readings, the location was +/// returned unconditionally and the diagnostic read `:0:0: error:` — a position +/// that looks real. The same fault already showed for any hand-built stream, whose +/// captured items also carry default locations. Both are fixed by asking whether +/// the location has a position at all, which is the only thing that now gates it. #[test] fn an_unresolved_type_without_a_position_reports_none() { let reg: RegistryBuilder<()> = @@ -1437,6 +1436,93 @@ fn an_unresolved_type_without_a_position_reports_none() { ); } +/// A crossing the *binding* declared, which the grammar refuses, is reported as +/// what it is — not as a `#[prebindgen]` item. +/// +/// This path is **newly reachable**: such a type used to become a cell with no +/// reading and no complaint, so the diagnostic never ran. Now that it does, it must +/// not send the reader looking for a marked item that was never written — the +/// offending type is in a build script, and `*const u8` is exactly the shape a +/// binding author reaches for and the source language refuses. +#[test] +fn a_declared_crossing_the_grammar_refuses_is_not_called_a_prebindgen_item() { + let reg: RegistryBuilder<()> = + crate::api::test_util::reg_from_items(vec![fn_item("fn f(x: u64) -> u64 { x }")]).unwrap(); + + let mut ext = StubExt::default(); + ext.types + .insert(TypeKey::parse("*const u8").expect("a key can hold it; the language cannot")); + + let err = ext + .declare_into_any(reg) + .expect("declaring is not where it fails") + .scanned() + .expect_err("the scan must refuse it"); + let msg = err.to_string(); + + assert!( + !msg.contains("#[prebindgen]"), + "no source item is at fault here, and naming one sends the reader to the wrong crate:\n{msg}" + ); + assert!( + // Token spacing, not the source spelling: the reason renders the type from + // its tokens, so `*const u8` comes back as `* const u8`. + msg.contains("const u8"), + "the offending type must be named:\n{msg}" + ); + assert!( + !msg.contains(":0:0"), + "a build script's declaration has no file position:\n{msg}" + ); +} + +/// The same rule for the *not-expressible* report, which has its own renderer. +/// +/// Two producers reach it — an unsupported captured element, and a type the scan +/// could not classify — and neither is guaranteed a file: a hand-built stream and +/// a spelling a binding composed both carry `SourceLocation::default()`. Printing +/// it renders `:0:0:`, which reads as a real position. +/// +/// Pinned separately from the unresolved-type test above because it is a separate +/// `Display` arm: the two were written months apart and only one had the guard. +#[test] +fn a_not_expressible_report_omits_a_position_it_does_not_have() { + let located = SourceLocation { + file: "src/lib.rs".into(), + line: 7, + column: 1, + crate_name: Some("myflat".into()), + }; + let err: ScanError = ScanError::NotExpressible { + entries: vec![ + NotExpressibleEntry { + name: None, + reason: "type `*const u8` is a form the language does not accept".into(), + location: SourceLocation::default(), + }, + NotExpressibleEntry { + name: Some(syn::parse_str("Placed").unwrap()), + reason: "is unsupported".into(), + location: located, + }, + ], + }; + let msg = err.to_string(); + + assert!( + !msg.contains(":0:0"), + "a placeless entry must print no position:\n{msg}" + ); + assert!( + msg.contains("type `*const u8` is a form the language does not accept"), + "it is still reported, and the reason names the offender:\n{msg}" + ); + assert!( + msg.contains("src/lib.rs:7:1 in crate `myflat`: Placed is unsupported"), + "an entry that HAS a position still prints it, with its crate:\n{msg}" + ); +} + /// A self-referential type has no topological order, so `crossings` must break /// the cycle rather than loop or drop a node. /// diff --git a/prebindgen/src/api/core/resolve.rs b/prebindgen/src/api/core/resolve.rs index 9adb5d51..61b5d5ce 100644 --- a/prebindgen/src/api/core/resolve.rs +++ b/prebindgen/src/api/core/resolve.rs @@ -150,7 +150,9 @@ fn collect_unresolved_descendants( out.push(UnresolvedEntry { key: key.clone(), direction: dir, - location: cell.subject.location().cloned(), + location: Some(&*cell.subject.origin.location) + .filter(|l| l.has_position()) + .cloned(), }); enqueue_edges_from(dir, &key, &mut queue, seen); } @@ -189,7 +191,9 @@ pub(crate) fn check_complete(registry: &Registry) -> Result<(), ResolveErr entries.push(UnresolvedEntry { key: key.clone(), direction: dir, - location: cell.subject.location().cloned(), + location: Some(&*cell.subject.origin.location) + .filter(|l| l.has_position()) + .cloned(), }); } } diff --git a/prebindgen/src/api/core/resolve/tests.rs b/prebindgen/src/api/core/resolve/tests.rs index 06fffd66..a30b3d1b 100644 --- a/prebindgen/src/api/core/resolve/tests.rs +++ b/prebindgen/src/api/core/resolve/tests.rs @@ -1,5 +1,5 @@ use super::*; -use crate::api::{core::registry::TypeEntry, test_util::cell}; +use crate::api::core::registry::TypeEntry; /// Regression: when a required type is itself unresolved AND has fields /// that are also unresolved, the diagnostic must list both. Previously @@ -59,27 +59,25 @@ fn final_invariant_stops_at_resolved_nodes() { let inner_key = TypeKey::parse("Inner").expect("test type"); let unrelated_key = TypeKey::parse("Unrelated").expect("test type"); - reg.input_types.insert(outer_key.clone(), cell(true, None)); + reg.insert_crossing(Direction::Input, &outer_key, true, None); - reg.input_types.insert( - inner_key.clone(), - cell( - false, - Some(TypeEntry { - destination: syn::parse_quote!(i64), - function: syn::parse_quote!( - fn __dummy() {} - ), - pre_stages: vec![], - subs: vec![], - niches: crate::api::core::niches::Niches::empty(), - metadata: (), - }), - ), + reg.insert_crossing( + Direction::Input, + &inner_key, + false, + Some(TypeEntry { + destination: syn::parse_quote!(i64), + function: syn::parse_quote!( + fn __dummy() {} + ), + pre_stages: vec![], + subs: vec![], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), ); - reg.input_types - .insert(unrelated_key.clone(), cell(false, None)); + reg.insert_crossing(Direction::Input, &unrelated_key, false, None); let err = check_complete(®).expect_err("must surface Outer"); let ResolveError::Unresolved { entries } = err; @@ -105,10 +103,7 @@ fn final_invariant_stops_at_resolved_nodes() { /// `subs` makes it something a converter must exist for. #[test] fn a_type_reachable_only_through_subs_must_still_resolve() { - use crate::api::{ - core::registry::{Registry, TypeKey}, - test_util::cell, - }; + use crate::api::core::registry::{Registry, TypeKey}; let mut reg: Registry<()> = Registry::empty(); let outer = TypeKey::parse("Outer").expect("test type"); @@ -116,24 +111,23 @@ fn a_type_reachable_only_through_subs_must_still_resolve() { // `Outer` is a root AND resolved — so it is not itself reportable — but its // converter delegates to `Mid`. - reg.input_types.insert( - outer.clone(), - cell( - true, - Some(TypeEntry { - destination: syn::parse_quote!(i64), - function: syn::parse_quote!( - fn __outer() {} - ), - pre_stages: vec![], - subs: vec![mid.clone()], - niches: crate::api::core::niches::Niches::empty(), - metadata: (), - }), - ), + reg.insert_crossing( + Direction::Input, + &outer, + true, + Some(TypeEntry { + destination: syn::parse_quote!(i64), + function: syn::parse_quote!( + fn __outer() {} + ), + pre_stages: vec![], + subs: vec![mid.clone()], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), ); // `Mid` is present, unresolved, and NOT a root. - reg.input_types.insert(mid.clone(), cell(false, None)); + reg.insert_crossing(Direction::Input, &mid, false, None); let err = check_complete(®).expect_err("Mid must be reported"); let ResolveError::Unresolved { entries } = err; diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index d354f91f..326d1cf7 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -3,276 +3,8 @@ //! replaces the per-module copies that used to live in `core::unfold`, //! `core::expand`, and the jnigen adapter. -use std::collections::HashMap; - use proc_macro2::Span; -use crate::SourceLocation; - -/// The single-segment path type for a bare item ident (`Foo` → `Foo`) — -/// direct construction, no string round trip, cannot fail. -pub fn type_from_ident(ident: &syn::Ident) -> syn::Type { - syn::Type::Path(syn::TypePath { - qself: None, - path: syn::Path::from(ident.clone()), - }) -} - -/// Normalize a type to its canonical flat-namespace spelling (issue #95). -/// The COMPLETE equivalence rule set — any spelling not listed is preserved -/// verbatim: -/// -/// 1. `Type::Group` / `Type::Paren` wrappers unwrap (`(Foo)` ≡ `Foo`). -/// 2. A multi-segment path headed by `crate` / `self` reduces to its final -/// segment, keeping that segment's generic arguments (`crate::a::Foo` -/// ≡ `Foo`). Sound because the flat namespace indexes at most one -/// item per bare ident, and a `crate::` path in a captured item can only -/// denote the source crate's own item. -/// 3. A multi-segment path headed by a name in `source_modules` (the -/// `#[prebindgen]` source crates chained into the registry, -/// hyphens-as-underscores) reduces the same way (`myflat::Foo` ≡ `Foo`). -/// Pure callers pass `&[]`. -/// 4. A **prelude** path reduces to the bare name the language knows it by — -/// exactly [`Normalization::PRELUDE`], with `core`/`alloc` read as `std`. -/// Each entry names a *constructor*, so arguments are preserved: -/// `std::vec::Vec` ≡ `Vec`. -/// -/// Nothing else. `std::ffi::CString` stays qualified, and so does a -/// foreign path (`zenoh::KeyExpr`) **even when an alias names that -/// type**: a `#[prebindgen] pub type` is a one-way road, bringing a -/// foreign type into the flat API under a name that is thereafter the -/// only way to spell it. It declares -/// an [`Extern`](crate::core::flat::Extern); it is not an equivalence. -/// -/// That keeps the rule meaning-preserving, which is the whole contract -/// here: reduction may choose among spellings of ONE type, never change -/// what a type is. Treating an alias as an equivalence broke that — -/// `Vec` ≡ `Bytes` turns a sequence into an extern — and no -/// key-shape refinement fixes the category error. -/// 5. Lifetimes are NOT normalized (`&'a T` ≠ `&T`, `Foo<'static>` ≠ `Foo`) -/// — a lifetime is part of the spelling a foreign-type declaration relies -/// on (`ptr_class!(ZKeyExpr<'static>)`), so collapsing it would make two -/// distinct declarations collide. -/// -/// Idempotent; recurses through references, slices, tuples, pointers, -/// generic arguments, and `impl Trait` bounds. Paths with a qualified self -/// (`::Assoc`) are left untouched. -/// What a captured path may be reduced against: the ingested source crates' own -/// modules, and every name an alias gives to a foreign path. -/// -/// One value rather than a bare `&[String]`, because reduction has one rule and -/// two sources of aliases feeding it — see [`normalize_type`]'s rule list. -/// [`Self::default`] is the prelude alone, which is what a caller normalizing a -/// lone type (rather than an ingested stream) wants. -#[derive(Clone, Debug)] -pub struct Normalization { - /// Module name per ingested source, first-seen order. The first doubles as the - /// default module for references with no recorded origin. - pub source_modules: Vec, - /// Constructor path → the bare name the language knows it by, from - /// [`Self::PRELUDE`] alone. Matched with the use site's type arguments ignored - /// and preserved, because a prelude entry names a constructor: - /// `std::vec::Vec` is every `Vec`. - /// - /// A crate's `#[prebindgen] pub type` is deliberately **not** here — see - /// [`normalize_type`]'s rule 4. - constructors: HashMap, -} - -impl Normalization { - /// The names the language **pre-declares**, so no source crate has to write - /// them — exactly Rust's own idea of a prelude, a set of `use`s you need not - /// write. A crate need not write `use std::vec::Vec`, and need not write - /// `#[prebindgen] pub type Vec = std::vec::Vec` either, for the same reason. - /// - /// Not identical to Rust's prelude: it adds `MaybeUninit`, which the grammar - /// recognises for out-parameters, and `Cow`, which it treats as transparent. - /// Its entries are exactly the bare names - /// [`lower_path`](crate::core::flat) classifies as builtins and that have a - /// std path at all — `str` has none, and neither do the scalars. - /// - /// Written with the `std` root; `core` and `alloc` are re-exports of the same - /// items, so a leading `core`/`alloc` is read as `std` before matching. - pub const PRELUDE: &'static [(&'static str, &'static str)] = &[ - ("std::vec::Vec", "Vec"), - ("std::option::Option", "Option"), - ("std::result::Result", "Result"), - ("std::string::String", "String"), - ("std::boxed::Box", "Box"), - ("std::mem::MaybeUninit", "MaybeUninit"), - ("std::borrow::Cow", "Cow"), - ]; - - /// The prelude alone: no ingested sources, no declared aliases. - pub fn prelude() -> Self { - Self { - source_modules: Vec::new(), - constructors: Self::PRELUDE - .iter() - .map(|(path, name)| ((*path).to_string(), (*name).to_string())) - .collect(), - } - } - - /// Collect from a captured stream, before anything is normalized. - /// - /// The single entry point — `FlatBuilder::build` — builds - /// this, so they cannot normalize differently. Gathering every module and alias - /// first is what makes reduction order-independent: a signature may name a type - /// whose alias is declared later, or in another source. - pub fn from_items(items: &[(syn::Item, SourceLocation)]) -> Self { - let mut out = Self::prelude(); - for (_, loc) in items { - if let Some(crate_name) = &loc.crate_name { - let module = crate_name.replace('-', "_"); - if !out.source_modules.contains(&module) { - out.source_modules.push(module); - } - } - } - out - } - - /// The bare name the language knows this constructor by, arguments ignored. - fn constructor_of(&self, path: &syn::Path) -> Option<&str> { - self.constructors - .get(&constructor_key(path)) - .map(String::as_str) - } -} - -impl Default for Normalization { - fn default() -> Self { - Self::prelude() - } -} - -/// A path as a key: segments joined, arguments dropped, and a leading -/// `core`/`alloc` read as `std` since they re-export the same items. -/// -/// Only [`Normalization::constructors`] is keyed this way, and a constructor is -/// exactly a path without arguments — `std::vec::Vec` matches every `Vec`. -fn constructor_key(path: &syn::Path) -> String { - let mut out = String::new(); - for (i, seg) in path.segments.iter().enumerate() { - if i > 0 { - out.push_str("::"); - } - let mut ident = seg.ident.to_string(); - if i == 0 && (ident == "core" || ident == "alloc") { - ident = "std".to_string(); - } - out.push_str(&ident); - } - out -} - -/// A type reduced to the spelling everything keys on: prelude-normalized, so -/// `std::option::Option` and `Option` are one entry. -/// -/// The **single** definition of that reduction. Two things key on it — the -/// model's type index ([`Flat::type_ref`](crate::core::flat::Flat::type_ref)) -/// and [`TypeKey`](crate::core::TypeKey) — and they have to agree, so neither -/// spells it out itself. -/// -/// Deliberately `prelude()` rather than a source-module-aware normalization: a -/// key must mean the same thing before and after ingestion knows what the source -/// modules are. -pub fn canonical_type(ty: &syn::Type) -> syn::Type { - let mut t = ty.clone(); - normalize_type(&mut t, &Normalization::prelude()); - t -} - -/// [`canonical_type`] as tokens — the string form both indexes use as their key. -pub fn canonical_spelling(ty: &syn::Type) -> String { - use quote::ToTokens; - canonical_type(ty).to_token_stream().to_string() -} - -pub fn normalize_type(ty: &mut syn::Type, against: &Normalization) { - use syn::visit_mut::VisitMut; - struct Normalizer<'a> { - against: &'a Normalization, - } - impl VisitMut for Normalizer<'_> { - fn visit_type_mut(&mut self, ty: &mut syn::Type) { - // Unwrap (possibly nested) group/paren wrappers in place. - loop { - match ty { - syn::Type::Group(g) => *ty = (*g.elem).clone(), - syn::Type::Paren(p) => *ty = (*p.elem).clone(), - _ => break, - } - } - if let syn::Type::Path(tp) = ty { - if tp.qself.is_none() { - reduce_flat_path(&mut tp.path, self.against); - } - } - syn::visit_mut::visit_type_mut(self, ty); - } - } - Normalizer { against }.visit_type_mut(ty); -} - -/// Apply [`normalize_type`] to every type position inside an item — fn -/// signatures, struct fields, enum variants, const types. The ingest-time -/// pass ([`crate::api::core::flat::FlatBuilder::build`]) that makes -/// captured spellings canonical before any key is formed, so every -/// downstream `TypeKey::from_type` sees the flat spelling. -pub fn normalize_item_types(item: &mut syn::Item, against: &Normalization) { - use syn::visit_mut::VisitMut; - - struct ItemNormalizer<'a> { - against: &'a Normalization, - } - impl VisitMut for ItemNormalizer<'_> { - fn visit_type_mut(&mut self, ty: &mut syn::Type) { - // Normalizes the whole subtree; no further descent needed. - normalize_type(ty, self.against); - } - } - ItemNormalizer { against }.visit_item_mut(item); -} - -/// The path-reduction step of [`normalize_type`]: collapse a reducible -/// multi-segment path to its final segment. See the rule list there. -fn reduce_flat_path(path: &mut syn::Path, against: &Normalization) { - if path.segments.len() < 2 { - return; - } - - // A prelude entry names a CONSTRUCTOR, so arguments are ignored when matching - // and preserved when rewriting: `std::vec::Vec` is `Vec`. A crate's - // own alias is NOT consulted — see rule 4. - if let Some(name) = against.constructor_of(path) { - let mut last = path.segments.last().expect("len checked").clone(); - last.ident = syn::Ident::new(name, last.ident.span()); - path.leading_colon = None; - path.segments = std::iter::once(last).collect(); - return; - } - - // Otherwise only a prefix into the flat namespace reduces, to the final - // segment: this crate's own path, or an ingested source's module. - let head = path - .segments - .first() - .expect("len checked") - .ident - .to_string(); - let reduce = match head.as_str() { - "crate" | "self" => true, - other => against.source_modules.iter().any(|m| m == other), - }; - if reduce { - let last = path.segments.last().expect("len checked").clone(); - path.leading_colon = None; - path.segments = std::iter::once(last).collect(); - } -} - /// If `ty` is `Option` (by last path segment), return `Inner`. pub fn option_inner_type(ty: &syn::Type) -> Option { generic_inner(ty, "Option") diff --git a/prebindgen/src/api/core/write/tests.rs b/prebindgen/src/api/core/write/tests.rs index e0b509c9..e080222b 100644 --- a/prebindgen/src/api/core/write/tests.rs +++ b/prebindgen/src/api/core/write/tests.rs @@ -5,7 +5,7 @@ use quote::ToTokens; use super::*; use crate::{ - api::{core::registry::RegistryBuilder, test_util::cell}, + api::core::registry::{Direction, RegistryBuilder}, SourceLocation, }; @@ -47,41 +47,39 @@ fn dedup_and_sort() { let wire: syn::Type = syn::parse_quote!(i64); let wire2: syn::Type = syn::parse_quote!(*const u8); - reg.input_types.insert( - key_a.clone(), - cell( - true, - Some(TypeEntry { - destination: wire.clone(), - function: syn::parse_quote!( - fn handle_to_u64_aaaa(v: i64) -> u64 { - v as u64 - } - ), - pre_stages: vec![], - subs: vec![], - niches: crate::api::core::niches::Niches::empty(), - metadata: (), - }), - ), + reg.insert_crossing( + Direction::Input, + &key_a, + true, + Some(TypeEntry { + destination: wire.clone(), + function: syn::parse_quote!( + fn handle_to_u64_aaaa(v: i64) -> u64 { + v as u64 + } + ), + pre_stages: vec![], + subs: vec![], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), ); - reg.input_types.insert( - key_b.clone(), - cell( - true, - Some(TypeEntry { - destination: wire2.clone(), - function: syn::parse_quote!( - fn Ptr_to_Sample_bbbb(v: *const u8) -> Sample { - decode_sample(v) - } - ), - pre_stages: vec![], - subs: vec![], - niches: crate::api::core::niches::Niches::empty(), - metadata: (), - }), - ), + reg.insert_crossing( + Direction::Input, + &key_b, + true, + Some(TypeEntry { + destination: wire2.clone(), + function: syn::parse_quote!( + fn Ptr_to_Sample_bbbb(v: *const u8) -> Sample { + decode_sample(v) + } + ), + pre_stages: vec![], + subs: vec![], + niches: crate::api::core::niches::Niches::empty(), + metadata: (), + }), ); let items = collect_converter_items(®); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs index c5cf3b8e..07ece2b7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs @@ -8,7 +8,7 @@ use crate::{ niches::{NicheSlot, Niches}, registry::{Registry, TypeEntry, TypeKey}, }, - test_util::{cell, unique_test_dir}, + test_util::unique_test_dir, }, SourceLocation, }; @@ -67,7 +67,7 @@ fn install_input( e: TypeEntry, ) { let key = TypeKey::parse(ty_str).expect("test type"); - reg.input_types.insert(key.clone(), cell(true, Some(e))); + reg.insert_crossing(Direction::Input, &key, true, Some(e)); } fn install_output( @@ -77,5 +77,5 @@ fn install_output( e: TypeEntry, ) { let key = TypeKey::parse(ty_str).expect("test type"); - reg.output_types.insert(key.clone(), cell(true, Some(e))); + reg.insert_crossing(Direction::Output, &key, true, Some(e)); } diff --git a/prebindgen/src/api/test_util.rs b/prebindgen/src/api/test_util.rs index ee259d62..ddb79062 100644 --- a/prebindgen/src/api/test_util.rs +++ b/prebindgen/src/api/test_util.rs @@ -6,22 +6,7 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, }; -use crate::api::core::registry::{Registry, RegistryBuilder, TypeCell, TypeEntry, TypeSubject}; - -/// A type-table cell for a fixture. -/// -/// The subject is always [`TypeSubject::Adapter`]: a hand-built table has no -/// `Flat` behind it, so no key in one has a source reading. A test that cares -/// about the `Source` side builds its registry from items instead. -/// -/// Takes no key: `Adapter` carries nothing, since nothing ever read it back. -pub(crate) fn cell(root: bool, entry: Option>) -> TypeCell { - TypeCell { - subject: TypeSubject::Adapter, - root, - entry, - } -} +use crate::api::core::registry::{Registry, RegistryBuilder}; /// Index a `Registry` from a list of Rust item sources. /// From fbb77b6fb5c590034f0cdc86a0313bcc3a16116f Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 10:56:53 +0200 Subject: [PATCH 16/52] JniGen keeps declarations, not the builder that made them (#260) * JniGen keeps declarations, not the builder that made them Before #253 a build script held the phases apart itself: it built a Flat, made a RegistryBuilder, resolved it into a Registry, and handed that to the generator. The RegistryBuilder -> Registry split WAS the enforcement, and it lived in the caller's hands. #253 moved that inside the generator, which is a better API and lost the guarantee. build(self) consumed the builder and then stored it back inside the built object, so the type meaning "still being described" was retained by the type meaning "finished" -- with all 54 mutators reachable through a pub(crate) field. Nothing was wrong, and that is the point. .gen/.registry were touched only in mod.rs, every other file already went through the &-accessors, and no &mut JniGen existed. Convention held it. Convention is what this replaces, for the same reason #252 gave for Registry, whose supply survived two commits claiming it was gone. So JniGenBuilder splits in two: Declarations -- what was declared. No &mut self method at all. This is what a JniGen keeps and what every emitter reads. JniGenBuilder { decls, sources } -- the describing surface, and the only type with mutators. build_with takes decls out and never puts them back. The split fell almost exactly on file boundaries, which is why it is mostly a rename: the 17 mutators were already concentrated in builder.rs and config.rs, and the ~117 read methods were already in files that only read. sources stays on the builder -- it is input to building, not a declaration, and keeping it out is what makes Declarations mean one thing. JniGen's fields are now module-private rather than pub(crate). Flat comes along for free: Registry::flat is private and Registry::flat() returns &Flat, so a Registry that cannot be reached mutably seals the model too. Two guards, both proven to fail before being trusted: a_built_jnigen_exposes_no_mutation -- reads the source with ALL whitespace stripped, so a multi-line signature is indistinguishable from a one-line one, and complains about any fn(&mut self) inside an impl Declarations, impl JniGen, or the Prebindgen impl. Sabotaged with a &mut self on Declarations: it names the file. builder.rs and config.rs are skipped, exactly as the registry's own test skips declare.rs. declarations_interior_mutability_is_only_the_two_memos -- iface_specs and fn_plans stay RefCell deliberately: they cache a pure function of (declarations, registry), both frozen by the time anything reads them, and caching that is not re-declaring anything. Naming them means a third has to be argued for. Its first version scanned line by line and could not see iface_specs, whose declaration wraps -- the very hazard the sibling test exists for -- so it strips whitespace too. Reported: regen-check byte-identical on every committed artifact; the two untracked example_flat_aarch64_unstable.{rs,h} are the pre-existing #219 disagreement. Ledger unchanged at 154 -- this moves no classification sites. Explained: a pure representation change; nothing about what is generated moves. Asserted: the type a built binding keeps has no mutators. Cbindgen is untouched by decision, and follows by analogy -- which is why the guard is written to be copied rather than generalised over both now. * The guard skipped files; it should have exempted impl blocks Both P1s hit the same weakness from opposite sides, and both are right: the scanner covered 14 of 22 sealed impl blocks. builder.rs and config.rs were skipped WHOLESALE because that is where the mutators live -- but they also hold six impl Declarations blocks between them, which is most of the read API the guard exists to protect. Adding a &mut self to any of those left the test green. The scanner already told JniGenBuilder from Declarations; skipping by filename threw that away. And the self type was matched by unqualified prefix, so impl super::JniGen -- which occurs in kotlin_emit.rs and report.rs -- was invisible. Now no file is skipped and the exemption is per block: the self type is read off the impl header (after the trait, when there is one) and matched by SUFFIX, so a qualified path is not a way around it. JniGenBuilder ends with neither name, which is what exempts the describing half without naming a file. Proven by sabotage in each of the three places the old version was blind to -- impl Declarations in builder.rs, the same in config.rs, and impl super::JniGen in kotlin_emit.rs -- each caught, plus a &mut self on impl JniGenBuilder confirming the describing half is still allowed. Added a floor on the number of sealed blocks scanned. A source scanner that silently stops matching passes forever, which is the failure mode this whole family of tests exists to avoid; the count may rise freely, and a drop means the header matching broke. Also fixed the stale field doc the review flagged: java_class_prefix moved to Declarations, and jni_class_path had not existed for some time. --- prebindgen/src/api/lang/jnigen/jni/builder.rs | 91 ++++++----- .../src/api/lang/jnigen/jni/classify.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/config.rs | 26 +-- prebindgen/src/api/lang/jnigen/jni/decl.rs | 12 +- .../src/api/lang/jnigen/jni/emit/callback.rs | 2 +- .../src/api/lang/jnigen/jni/emit/convert.rs | 4 +- .../src/api/lang/jnigen/jni/emit/delivery.rs | 4 +- .../api/lang/jnigen/jni/emit/flat_input.rs | 14 +- .../src/api/lang/jnigen/jni/emit/names.rs | 6 +- .../api/lang/jnigen/jni/emit/struct_out.rs | 10 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 4 +- .../src/api/lang/jnigen/jni/emit/vec_build.rs | 12 +- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 20 +-- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 24 +-- prebindgen/src/api/lang/jnigen/jni/fold.rs | 4 +- prebindgen/src/api/lang/jnigen/jni/iface.rs | 42 ++--- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 20 +-- .../src/api/lang/jnigen/jni/metadata.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/mod.rs | 53 +++++- .../src/api/lang/jnigen/jni/overloads.rs | 10 +- prebindgen/src/api/lang/jnigen/jni/render.rs | 34 ++-- prebindgen/src/api/lang/jnigen/jni/report.rs | 2 +- .../src/api/lang/jnigen/jni/selector.rs | 4 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 6 +- prebindgen/src/api/lang/jnigen/jni/symbol.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/symbols.rs | 9 +- .../src/api/lang/jnigen/jni/tests/flatten.rs | 3 +- .../src/api/lang/jnigen/jni/tests/mod.rs | 1 + .../src/api/lang/jnigen/jni/tests/phases.rs | 154 ++++++++++++++++++ .../src/api/lang/jnigen/jni/tests/sealed.rs | 11 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 38 +++-- prebindgen/src/api/lang/jnigen/mod.rs | 6 +- prebindgen/src/lib.rs | 2 +- 33 files changed, 424 insertions(+), 210 deletions(-) create mode 100644 prebindgen/src/api/lang/jnigen/jni/tests/phases.rs diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 0b0438fe..e531738a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -67,7 +67,7 @@ impl DeclaredKind { } } -impl JniGenBuilder { +impl Declarations { /// The module path a generated call to `#[prebindgen]` fn `ident` must be /// qualified with: the fn's **origin crate** as recorded from its /// stream's `SourceLocation` stamp (multi-source bindings — helper @@ -103,18 +103,8 @@ impl JniGenBuilder { } } -impl JniGenBuilder { - /// Start a binding generator with default settings: empty base - /// package, no `JNINative` init block, identity - /// name-mangling, handle locks enabled. Adjust settings with the `set_*` - /// methods, add declarations with [`package`](Self::package), - /// [`expand`](Self::expand), [`convert`](Self::convert), etc., then run the - /// result through `JniGenBuilder::build` → `JniGen::write_rust` / - /// `write_kotlin`. Settings and - /// declarations may be interleaved in any order — the builder stores - /// only raw inputs, and every setting-derived name is computed at the - /// point of use. - pub fn new() -> Self { +impl Default for Declarations { + fn default() -> Self { Self { package: String::new(), fun_name_mangle: None, @@ -142,10 +132,27 @@ impl JniGenBuilder { local_fns: Vec::new(), iface_specs: Default::default(), fn_plans: Default::default(), - sources: Default::default(), } } +} +impl JniGenBuilder { + /// Start a binding generator with default settings: empty base + /// package, no `JNINative` init block, identity + /// name-mangling, handle locks enabled. Adjust settings with the `set_*` + /// methods, add declarations with [`package`](Self::package), + /// [`expand`](Self::expand), [`convert`](Self::convert), etc., then run the + /// result through [`build`](Self::build) → `JniGen::write_rust` / + /// `write_kotlin`. Settings and + /// declarations may be interleaved in any order — the builder stores + /// only raw inputs, and every setting-derived name is computed at the + /// point of use. + pub fn new() -> Self { + Self::default() + } +} + +impl Declarations { /// Apply the package-level function-name mangle closure to `name`. pub(crate) fn mangle_fun(&self, package: &str, name: &str) -> String { match &self.fun_name_mangle { @@ -237,14 +244,12 @@ impl JniGenBuilder { } } -impl Default for JniGenBuilder { - fn default() -> Self { - Self::new() - } -} - // ── Accepting a `PackageDecl` ──────────────────────────────────────────── +/// The describing surface: everything that *adds* to [`Declarations`]. +/// +/// Every method here takes `self` or `&mut self`; not one of them exists on +/// `Declarations`, which is the whole point of the two types. impl JniGenBuilder { /// Register a package's worth of classes, functions and consts (a /// [`PackageDecl`], built with [`package!`](crate::package)). Call it once @@ -295,7 +300,7 @@ impl JniGenBuilder { functions, constants, } = decl; - self.packages.entry(name.clone()).or_default(); + self.decls.packages.entry(name.clone()).or_default(); for class in classes { self.accept_class(&name, class); } @@ -306,7 +311,7 @@ impl JniGenBuilder { // source was already lowered to an expression (`path()`) at decl // time, so only three storage kinds exist internally. for c in constants { - let pkg = self.packages.entry(name.clone()).or_default(); + let pkg = self.decls.packages.entry(name.clone()).or_default(); match c.source { super::decl::ConstSource::Item => { let mut entry = FunctionEntry::new(c.rust_ident); @@ -340,16 +345,16 @@ impl JniGenBuilder { pub fn ignore(mut self, decl: impl Into) -> Self { match decl.into().0 { super::decl::IgnoreKind::Fun(ident) => { - self.ignored_fns.insert(ident); + self.decls.ignored_fns.insert(ident); } super::decl::IgnoreKind::Type(key) => { - self.ignored_class_types.insert(key); + self.decls.ignored_class_types.insert(key); } super::decl::IgnoreKind::Const(ident) => { - self.ignored_const_idents.insert(ident); + self.decls.ignored_const_idents.insert(ident); } super::decl::IgnoreKind::Matching(pred) => { - self.ignored_name_predicates.push(pred); + self.decls.ignored_name_predicates.push(pred); } } self @@ -397,7 +402,7 @@ impl JniGenBuilder { ); } let short = rust_short_name(key); - match self.types.entry(key.clone()) { + match self.decls.types.entry(key.clone()) { std::collections::hash_map::Entry::Occupied(e) => { let cfg = e.into_mut(); cfg.kind.merge(kind, &short); @@ -414,6 +419,7 @@ impl JniGenBuilder { /// idempotent). fn store_iface_opts(&mut self, key: &TypeKey, iface: IfaceOpts) { let cfg = self + .decls .types .get_mut(key) .expect("register_class created the entry"); @@ -527,7 +533,8 @@ impl JniGenBuilder { // A constructor member's return is a factory, never // output-flattened — derived from `class_members` in // `build_deconstructors` (`skip_output`), not stored separately. - self.class_members + self.decls + .class_members .entry(key.clone()) .or_default() .push(ClassMember { @@ -541,7 +548,8 @@ impl JniGenBuilder { fn accept_function(&mut self, subpackage: &str, decl: FunctionDecl) { let mut entry = FunctionEntry::new(decl.rust_ident.clone()); entry.kotlin_name_override = decl.kotlin_name_override.clone(); - self.packages + self.decls + .packages .entry(subpackage.to_string()) .or_default() .functions @@ -578,17 +586,20 @@ impl JniGenBuilder { p = quote::quote!(#path) ); }; - self.local_fns.push((rust_ident.clone(), path, sig)); + self.decls.local_fns.push((rust_ident.clone(), path, sig)); } for (param, pdecl) in param_expands { - self.fn_param_expands + self.decls + .fn_param_expands .push((rust_ident.clone(), param, pdecl)); } if let Some(rdecl) = return_expand { - self.fn_return_expands.push((rust_ident.clone(), rdecl)); + self.decls + .fn_return_expands + .push((rust_ident.clone(), rdecl)); } for param in split_on_params { - self.fn_split_params.push((rust_ident.clone(), param)); + self.decls.fn_split_params.push((rust_ident.clone(), param)); } } } @@ -619,7 +630,7 @@ impl JniGenBuilder { .variant_self()", decl.key.as_str() ); - self.param_expand_decls.push(decl); + self.decls.param_expand_decls.push(decl); } ExpandDecl::Return(decl) => { assert!( @@ -628,12 +639,14 @@ impl JniGenBuilder { .field_self()", decl.key.as_str() ); - self.return_expand_decls.push(decl); + self.decls.return_expand_decls.push(decl); } } self } +} +impl Declarations { /// The Kotlin name of `func` as a declared member (`.method`/`.constructor`) /// of the class keyed by `key`, if it is one — the name-inheritance /// source for [`ExpandReturnDecl::field`]. @@ -1389,11 +1402,13 @@ impl JniGenBuilder { // Binding-local fn sources (`fun!(crate::f).sig(…)`) join the same // synthesis list as fun/method/constructor sites — after the // pre-pass they lower exactly like `#[prebindgen]` fn sources. - self.local_fns.append(&mut decl.locals); - self.convert_decls.push(decl); + self.decls.local_fns.append(&mut decl.locals); + self.decls.convert_decls.push(decl); self } +} +impl Declarations { /// Derive the rank-0 **input** converter body for a `convert!`-declared /// type: `(continue_ty, exc, body)` where `continue_ty` is the conversion /// fn's parameter type (by value) — the composed-converter machinery @@ -1699,7 +1714,7 @@ fn fn_return_type(item_fn: &syn::ItemFn) -> syn::Type { } } -impl JniGenBuilder { +impl Declarations { /// Build a `KotlinMeta` carrying just the value-context Kotlin name. /// Used by every built-in converter (primitives, structs, `Option<_>`, /// `Vec<_>`, `impl Fn(...)` lambdas). Errors are routed uniformly to the diff --git a/prebindgen/src/api/lang/jnigen/jni/classify.rs b/prebindgen/src/api/lang/jnigen/jni/classify.rs index 66613d4f..3a7e69d3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/classify.rs +++ b/prebindgen/src/api/lang/jnigen/jni/classify.rs @@ -42,7 +42,7 @@ impl TypeConfig { } } -impl JniGenBuilder { +impl Declarations { /// Classify `bare` against the declared-type table and the registry's /// captured structs. Callers strip `Option<_>` / `&_` layers first — /// wrapper folding is the resolver's business, not this table's. diff --git a/prebindgen/src/api/lang/jnigen/jni/config.rs b/prebindgen/src/api/lang/jnigen/jni/config.rs index e1cd7621..ba7ac3c0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/config.rs +++ b/prebindgen/src/api/lang/jnigen/jni/config.rs @@ -77,12 +77,12 @@ impl JniGenBuilder { // directly by many emitters, so mangling at this single storage // point keeps every reader consistent; warn here (the raw input is // only available now) when a segment was changed. - self.package = mangle_package(&trimmed); - if self.package != trimmed { + self.decls.package = mangle_package(&trimmed); + if self.decls.package != trimmed { println!( "cargo:warning=prebindgen: package prefix `{trimmed}` sanitized to `{}` \ (invalid Kotlin package identifier)", - self.package + self.decls.package ); } self @@ -99,7 +99,7 @@ impl JniGenBuilder { /// keeps the generator free of any concrete loading logic. Unset = no /// init block. pub fn set_jni_native_init(mut self, code: impl Into) -> Self { - self.jni_native_init = Some(code.into()); + self.decls.jni_native_init = Some(code.into()); self } @@ -112,7 +112,7 @@ impl JniGenBuilder { where F: Fn(&str) -> String + Send + Sync + 'static, { - self.harness_name_mangle = Some(Arc::new(f)); + self.decls.harness_name_mangle = Some(Arc::new(f)); self } @@ -124,7 +124,7 @@ impl JniGenBuilder { where F: Fn(&str, &str) -> String + Send + Sync + 'static, { - self.fun_name_mangle = Some(Arc::new(f)); + self.decls.fun_name_mangle = Some(Arc::new(f)); self } @@ -140,7 +140,7 @@ impl JniGenBuilder { where F: Fn(&str, &str) -> String + Send + Sync + 'static, { - self.interface_name_mangle = Some(Arc::new(f)); + self.decls.interface_name_mangle = Some(Arc::new(f)); self } @@ -151,7 +151,7 @@ impl JniGenBuilder { where F: Fn(&str, &str) -> String + Send + Sync + 'static, { - self.ptr_class_name_mangle = Some(Arc::new(f)); + self.decls.ptr_class_name_mangle = Some(Arc::new(f)); self } @@ -163,7 +163,7 @@ impl JniGenBuilder { where F: Fn(&str, &str) -> String + Send + Sync + 'static, { - self.data_class_name_mangle = Some(Arc::new(f)); + self.decls.data_class_name_mangle = Some(Arc::new(f)); self } @@ -174,7 +174,7 @@ impl JniGenBuilder { where F: Fn(&str, &str) -> String + Send + Sync + 'static, { - self.enum_name_mangle = Some(Arc::new(f)); + self.decls.enum_name_mangle = Some(Arc::new(f)); self } @@ -187,7 +187,7 @@ impl JniGenBuilder { where F: Fn(&str, &str, &str) -> String + Send + Sync + 'static, { - self.method_name_mangle = Some(Arc::new(f)); + self.decls.method_name_mangle = Some(Arc::new(f)); self } @@ -204,12 +204,12 @@ impl JniGenBuilder { /// re-verified on your own workload — generate once with `false`, /// benchmark both, and keep the default — not as an optimization knob. pub fn set_emit_handle_locks(mut self, emit: bool) -> Self { - self.emit_handle_locks = emit; + self.decls.emit_handle_locks = emit; self } } -impl JniGenBuilder { +impl Declarations { /// Materialize a [`NameSpec`] into a concrete Kotlin FQN under the /// current settings. Precedence for a declared class: per-decl /// `name_override` (package-resolved, mangle-bypassed), then the mangle diff --git a/prebindgen/src/api/lang/jnigen/jni/decl.rs b/prebindgen/src/api/lang/jnigen/jni/decl.rs index 5920c684..78439658 100644 --- a/prebindgen/src/api/lang/jnigen/jni/decl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/decl.rs @@ -1,20 +1,20 @@ //! Declaration objects: one standalone, independently-constructible value -//! type per kind of thing `JniGenBuilder` can be told about (a `ptr_class`, an +//! type per kind of thing `Declarations` can be told about (a `ptr_class`, an //! `enum_class`, a function, a scalar wire mapping, …), plus the `PackageDecl` //! that aggregates the package-scoped ones. Each type is both its own -//! "builder" and the final value `JniGenBuilder`/`PackageDecl` accepts — no separate +//! "builder" and the final value `Declarations`/`PackageDecl` accepts — no separate //! `Builder`/`Decl` split, no terminal `.build()` call. //! -//! `JniGenBuilder` itself only ever *accepts* fully-built values of these types +//! `Declarations` itself only ever *accepts* fully-built values of these types //! (`JniGenBuilder::package`, `JniGenBuilder::expand`, `JniGenBuilder::convert`, in //! `builder.rs`); none of them reach back -//! into any `JniGenBuilder` state while being built. +//! into any `Declarations` state while being built. use super::*; // ────────────────────────────────────────────────────────────────────── // Shared local accumulators (replayed into `Expansions`/`Deconstructors` -// by the accept logic in `builder.rs` once a decl is handed to `JniGenBuilder`) +// by the accept logic in `builder.rs` once a decl is handed to `Declarations`) // ────────────────────────────────────────────────────────────────────── /// One arm of an `expand_param!` `.variant*` list (type-level or per-fn). @@ -1884,7 +1884,7 @@ pub struct ConvertDecl { pub(crate) output: Option, pub(crate) domain: Option, /// Binding-local fn sources declared on this convert (`fun!(crate::f) - /// .sig(…)`): drained into [`JniGenBuilder::local_fns`] at acceptance so the + /// .sig(…)`): drained into [`Declarations::local_fns`] at acceptance so the /// synthesis pre-pass covers them. pub(crate) locals: Vec<(syn::Ident, syn::Path, syn::Signature)>, } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs index 353921bf..293458dd 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs @@ -20,7 +20,7 @@ use crate::api::core::registry::Conversions; /// Errors cannot reach a caller-side error sink (the declaring call already /// returned), so they are converted to `__JniErr` and logged via `tracing`. pub(crate) fn callback_input( - ext: &JniGenBuilder, + ext: &Declarations, args: &[syn::Type], registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs b/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs index d840a347..9aa9197a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs @@ -403,7 +403,7 @@ pub(crate) fn default_niches_for_wire(wire: &syn::Type) -> Niches { /// upstream type a bare `` resolves to in their include-site /// `use` statements. Pairs with output body below. pub(crate) fn enum_input_body( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, e: &syn::ItemEnum, ) -> (syn::Type, syn::Expr) { @@ -443,7 +443,7 @@ pub(crate) fn enum_input_body( /// upstream of the cast. The body works without naming the enum type /// at all — `v` is already typed via the wrapper signature, so the /// `as` cast picks up the right type by inference. -pub(crate) fn enum_output_body(_ext: &JniGenBuilder, e: &syn::ItemEnum) -> (syn::Type, syn::Expr) { +pub(crate) fn enum_output_body(_ext: &Declarations, e: &syn::ItemEnum) -> (syn::Type, syn::Expr) { assert_only_unit_variants(e); let body: syn::Expr = syn::parse_quote!({ v as jni::sys::jint }); (syn::parse_quote!(jni::sys::jint), body) diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index 65bb6bc7..4f8f1924 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -30,7 +30,7 @@ use crate::api::core::{ /// [`UnfoldShape::Base`]: crate::api::core::unfold::UnfoldShape::Base /// [`UnfoldShape::Optional`]: crate::api::core::unfold::UnfoldShape::Optional pub(crate) fn emit_unfold_delivery( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, plan: &crate::api::core::unfold::UnfoldPlan, iface: Option<&IfaceSpec>, @@ -788,7 +788,7 @@ fn reach_leaf( /// arm of fallible externs (whose `fail` falls back to a binding-error /// `signal_error` with default ze values). pub(crate) fn encode_plan_leaves( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, plan: &crate::api::core::unfold::UnfoldPlan, obj_idents: &[syn::Ident], diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 1c4a60a9..cb09c501 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -5,7 +5,7 @@ use super::*; use crate::api::core::registry::Conversions; pub(crate) fn struct_input_body( - ext: &JniGenBuilder, + ext: &Declarations, s: &syn::ItemStruct, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { @@ -280,7 +280,7 @@ pub(crate) fn struct_input_body( /// one field out of a `JObject` the caller already handed us costs nothing /// extra, so the asymmetry is real rather than an oversight. pub(crate) fn sum_input_body( - ext: &JniGenBuilder, + ext: &Declarations, e: &syn::ItemEnum, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { @@ -377,7 +377,7 @@ pub(crate) fn sum_input_body( /// [`struct_input_body`] performs, for the positions that are properties of a /// generated class rather than fields of a data class. fn read_kotlin_property( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, receiver: &TokenStream, prop: &str, @@ -869,7 +869,7 @@ fn wire_kotlin_type(entry: &crate::api::core::registry::TypeEntry) - /// text spliced into a wrapper whose import set this plan does not own. #[allow(clippy::too_many_arguments)] fn build_flat_sum_field( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, sum_ty: &syn::Type, field: syn::Ident, @@ -1116,7 +1116,7 @@ fn push_handle_leaf( /// `.jobject_input()` opt-in); an unmarked data class either returns a complete /// plan or a validation error — never a silent object fallback. pub(crate) fn build_flat_input_plan( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, param_name: &syn::Ident, arg_ty: &syn::Type, @@ -1202,7 +1202,7 @@ pub(crate) fn build_flat_input_plan( #[allow(clippy::too_many_arguments)] fn build_flat_struct_node( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, st: &syn::ItemStruct, optional: bool, @@ -1797,7 +1797,7 @@ pub(crate) struct OptionScalarInputPlan { /// only the cases that *would* box are intercepted — niche cases (already /// unboxed / ABI-clean) and opaque/value projections are left untouched. pub(crate) fn build_option_scalar_input_plan( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, param_name: &syn::Ident, arg_ty: &syn::Type, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs index a5d19bb7..156b8f5f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs @@ -5,8 +5,8 @@ use super::*; /// Last-segment ident of a `TypeKey` — e.g. `"Publisher<'static>"` → /// `"Publisher"`, `"AdvancedSubscriber<()>"` → `"AdvancedSubscriber"`. Used by -/// the structured builders ([`JniGenBuilder::ptr_class`], -/// [`JniGenBuilder::data_class`]) to derive a default Kotlin class name from +/// the structured builders ([`Declarations::ptr_class`], +/// [`Declarations::data_class`]) to derive a default Kotlin class name from /// the Rust type-key. Panics for non-path types (e.g. closures, references) — /// the per-kind `*_name_mangle` closures see only path-shaped /// shorts. For verbatim Kotlin expressions on non-path types, use a @@ -23,7 +23,7 @@ pub(crate) fn rust_short_name(key: &TypeKey) -> String { /// Fallible variant of [`rust_short_name`] — returns `None` for /// non-path types instead of panicking. Used by -/// [`JniGenBuilder::note_wrapper_registration`] which is called for rank-0 +/// [`Declarations::note_wrapper_registration`] which is called for rank-0 /// wrapper patterns including non-path shapes like `()` where there /// is no Kotlin short name to derive. pub(crate) fn rust_short_name_opt(key: &TypeKey) -> Option { diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index 1d5b6016..908dfa47 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -10,7 +10,7 @@ use crate::api::core::registry::Conversions; /// in `Nullable`) are encodable as a single `L;` ctor arg; a /// collection layer (`Iterable`, i.e. `Vec`) would need array /// codegen and is a loud build-time error until implemented. -pub(crate) fn handle_field_fqn(ext: &JniGenBuilder, h: &Projection) -> String { +pub(crate) fn handle_field_fqn(ext: &Declarations, h: &Projection) -> String { fn assert_scalar(s: &FoldStrategy) { match s { FoldStrategy::Base => {} @@ -81,7 +81,7 @@ pub(crate) fn primitive_default_for_descriptor(sig: &str) -> TokenStream { /// model (`registry.flat()`) — both populated before `resolve` — never the /// output converter table (not yet built at this stage). pub(crate) fn synth_value_struct_leaves( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, s: &syn::ItemStruct, path_prefix: &[crate::api::core::unfold::PathStep], @@ -171,7 +171,7 @@ pub(crate) fn synth_value_struct_leaves( /// plan `flatten_struct_factory` walks for the Kotlin side, so the slot /// order and JVM descriptors agree by construction. pub(crate) fn flatten_struct_encode( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, s: &syn::ItemStruct, access: &TokenStream, @@ -575,7 +575,7 @@ fn encode_field( } pub(crate) fn struct_output_body( - ext: &JniGenBuilder, + ext: &Declarations, s: &syn::ItemStruct, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { @@ -637,7 +637,7 @@ pub(crate) fn struct_output_body( } pub(crate) fn struct_module_path( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, s: &syn::ItemStruct, ) -> syn::Path { diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index 95707c61..e79bbb4e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -41,7 +41,7 @@ pub(crate) const SUM_TAG_LEAF: &str = "tag"; /// `variant!(V).name(...)` rename carries through to the builder's parameter /// names too. pub(crate) fn synth_sum_leaves( - ext: &JniGenBuilder, + ext: &Declarations, sum_cfg: &SumConfig, item_enum: &syn::ItemEnum, ) -> Vec { @@ -162,7 +162,7 @@ pub(crate) fn is_sum_leaves(leaves: &[crate::api::core::unfold::UnfoldLeaf]) -> /// is that a leaf here is not an independent expression — its slot exists in /// every arm and only one arm computes it. pub(crate) fn encode_sum_group( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, leaves: &[crate::api::core::unfold::UnfoldLeaf], obj_idents: &[syn::Ident], diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs index 8a77fd92..51e40c10 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs @@ -30,7 +30,7 @@ pub(crate) fn slice_or_vec_elem(arg_ty: &syn::Type) -> Option<(syn::Type, bool)> /// classifier, `render_extern_decl`, and the synthetic-extern emitter so all /// four sites agree on which params take the handle path. pub(crate) fn vec_build_elem( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, arg_ty: &syn::Type, ) -> Option<(syn::Type, bool)> { @@ -60,7 +60,7 @@ pub(crate) fn vec_build_elem( /// Deduped by [`TypeKey`] and sorted for deterministic output (mirrors /// [`build_handle_destructor_items`]). pub(crate) fn collect_vec_build_elem_types( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, ) -> Vec { let declared = ext.declared_functions(); @@ -99,7 +99,7 @@ pub(crate) struct VecBuildHelpers { /// from the element's **Kotlin** data-class short name (first char lowercased) so /// the generated methods read naturally (`Payload` → `payloadVec`). pub(crate) fn vec_build_helpers( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, elem: &syn::Type, ) -> Option { @@ -137,7 +137,7 @@ pub(crate) fn vec_build_helpers( /// through the method mangler like every other `JNINative` extern. The Rust JNI symbol /// (see [`vec_helper_symbol`]) and the Kotlin call site both use this, so they /// agree. -pub(crate) fn vec_helper_method_name(ext: &JniGenBuilder, base: &str, suffix: &str) -> String { +pub(crate) fn vec_helper_method_name(ext: &Declarations, base: &str, suffix: &str) -> String { ext.mangle_jni_method(&format!("{base}{suffix}")) } @@ -145,7 +145,7 @@ pub(crate) fn vec_helper_method_name(ext: &JniGenBuilder, base: &str, suffix: &s /// `Java___…` scheme function wrappers use via the plan's /// `native_symbol` (see `symbol`, #86); these helpers live on the /// `JNINative` object, so they share its class path. -fn vec_helper_symbol(ext: &JniGenBuilder, base: &str, suffix: &str) -> String { +fn vec_helper_symbol(ext: &Declarations, base: &str, suffix: &str) -> String { ext.native_method_symbol(&vec_helper_method_name(ext, base, suffix)) } @@ -164,7 +164,7 @@ fn vec_helper_symbol(ext: &JniGenBuilder, base: &str, suffix: &str) -> String { /// shared across all callers of a given element type). This keeps the Kotlin /// push loop free of a per-element failure check. pub(crate) fn build_vec_build_helper_items( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, ) -> Vec { let mut named: Vec<(String, syn::Item)> = Vec::new(); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 4546f6cb..6aa94ead 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -5,7 +5,7 @@ use super::*; use crate::api::core::{registry::Conversions, types_util::result_ok_type}; pub(crate) fn emit_jni_function_wrapper( - ext: &JniGenBuilder, + ext: &Declarations, f: &syn::ItemFn, registry: &Registry, ) -> TokenStream { @@ -14,7 +14,7 @@ pub(crate) fn emit_jni_function_wrapper( /// The synthetic nullary getter signature a declared const is emitted /// through: `pub fn const_get_() -> `. Both sides — -/// the Rust extern ([`JniGenBuilder::on_const`] via +/// the Rust extern ([`Declarations::on_const`] via /// [`emit_jni_function_wrapper_with_callee`]) and the Kotlin `val` /// initializer (`render_const_val`) — derive the extern symbol from this one /// ident, so they stay in sync by construction. The body is never used. @@ -32,7 +32,7 @@ pub(crate) fn const_getter_fn(c: &syn::ItemConst) -> syn::ItemFn { /// shared closeable `val` is semantically wrong (whose `close()` is it?). /// Expose a factory function instead — the established idiom (e.g. zenoh's /// `encoding_const_*` companion factories). -pub(crate) fn reject_handle_const(ext: &JniGenBuilder, c: &syn::ItemConst) { +pub(crate) fn reject_handle_const(ext: &Declarations, c: &syn::ItemConst) { reject_handle_constant_type(ext, &c.ty, "const", &c.ident.to_string()); } @@ -41,7 +41,7 @@ pub(crate) fn reject_handle_const(ext: &JniGenBuilder, c: &syn::ItemConst) { /// declared opaque handle. `what`/`ident` shape the error message /// (`const MAX_LEN` / `constant fn encoding_const_x_str`). pub(crate) fn reject_handle_constant_type( - ext: &JniGenBuilder, + ext: &Declarations, ty: &syn::Type, what: &str, name: &str, @@ -79,7 +79,7 @@ pub(crate) fn reject_handle_constant_type( /// the `val` initializer's throwing `JniErrorHandler` only fits the /// infallible wrapper shape), and its return type must not peel to a /// declared opaque handle (same rationale as [`reject_handle_const`]). -pub(crate) fn validate_constant_fn(ext: &JniGenBuilder, f: &syn::ItemFn) { +pub(crate) fn validate_constant_fn(ext: &Declarations, f: &syn::ItemFn) { assert!( f.sig.inputs.is_empty(), "constant fn `{}`: takes {} parameter(s) — a function-backed constant must be nullary \ @@ -115,7 +115,7 @@ pub(crate) fn const_expr_getter_fn(kotlin_name: &str, ty: &syn::Type) -> syn::It /// Validates an expression constant's declared value type (checked on both /// write paths): not a `Result` (a domain-fallible value is not a constant), /// not (peeled to) a declared opaque handle. -pub(crate) fn validate_constant_expr(ext: &JniGenBuilder, kotlin_name: &str, ty: &syn::Type) { +pub(crate) fn validate_constant_expr(ext: &Declarations, kotlin_name: &str, ty: &syn::Type) { assert!( result_ok_type(ty).is_none(), "constant expr `{kotlin_name}`: type is a `Result` — an expression constant must be \ @@ -127,11 +127,11 @@ pub(crate) fn validate_constant_expr(ext: &JniGenBuilder, kotlin_name: &str, ty: /// [`emit_jni_function_wrapper`] with the raw callee expression overridable: /// `None` = the ordinary `::(args)` call; `Some(e)` /// splices `e` verbatim as the value the output phase converts. Used by the -/// const getter emission (`JniGenBuilder::on_const`), whose synthetic nullary `f` +/// const getter emission (`Declarations::on_const`), whose synthetic nullary `f` /// carries the signature while the value comes from /// `::` — a path, not a call. pub(crate) fn emit_jni_function_wrapper_with_callee( - ext: &JniGenBuilder, + ext: &Declarations, f: &syn::ItemFn, registry: &Registry, callee: Option, @@ -433,7 +433,7 @@ fn unfold_builder_param(iterable_fold: bool) -> TokenStream { /// site only renders each [`InputKind`]'s decode. #[allow(clippy::type_complexity)] fn emit_input_param( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, original_ident: &syn::Ident, param: &PlanParam, @@ -702,7 +702,7 @@ fn emit_plain_decode( /// through the same error sink as any fallible input. The returned call /// argument is the built value (`&value` when the original parameter was `&T`). pub(crate) fn emit_expanded_param( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, plan: &crate::api::core::expand::FoldPlan, leaves: &[PlanLeaf], diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index cdb770a1..834b8e48 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -31,7 +31,7 @@ pub(crate) struct JniFunctionPlan { /// The onError handler interfaces — the always-present binding /// `JniErrorHandler` plus, for a fallible function, its typed domain /// `Handler` (see [`ErrorIfaces`]). Shared from the - /// [`JniGenBuilder::iface_spec`] memo: one derivation per channel feeds the Rust + /// [`Declarations::iface_spec`] memo: one derivation per channel feeds the Rust /// `__SINK_*` statics, the Kotlin sink wiring, and the interface /// declarations, so the FQN/descriptor pairs of the cached `run` lookups /// cannot drift. `None` = the domain channel is underivable (the Rust @@ -150,7 +150,7 @@ pub(crate) struct UnfoldOutputPlan { /// The builder/folder `fun interface` spec the delivery calls into — /// [`folder_iface_for_plan`] for an iterable fold (incl. the fixed /// whole-element form), the memoized [`SpecKey::Builder`] spec - /// otherwise. Shared from the [`JniGenBuilder::iface_spec`] memo: one + /// otherwise. Shared from the [`Declarations::iface_spec`] memo: one /// derivation feeds the Rust upcall statics, every Kotlin surface read, /// and the interface declaration, so the cached `run` FQN/descriptor /// pair cannot drift. `None` = underivable (the Rust emitter keeps its @@ -242,7 +242,7 @@ impl PlanError { ), PlanError::UnresolvedOutput { ty } => format!( "JniGen::on_function: return type `{}` of `{}` has no registered output \ - converter — register one via `JniGenBuilder::output_wrapper(pat, |…| Some((ty, exc, body)))` \ + converter — register one via `Declarations::output_wrapper(pat, |…| Some((ty, exc, body)))` \ (exc = `None` for non-throwing, `Some(parse_quote!())` \ to bind a domain exception)", ty, fn_ident, @@ -267,7 +267,7 @@ impl PlanError { /// /// [`Prebindgen::validate_resolved`]: crate::api::core::prebindgen::Prebindgen::validate_resolved pub(crate) fn validate_bindings( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, ) -> Result<(), String> { let mut errors: Vec = Vec::new(); @@ -315,7 +315,7 @@ pub(crate) fn validate_bindings( } // Declared consts: their synthetic nullary getters run through the same - // plan machinery (`JniGenBuilder::on_const`). + // plan machinery (`Declarations::on_const`). if let Some(declared_consts) = ext.declared_consts() { let mut consts: Vec<&crate::api::core::flat::Constant> = registry.flat().constants().collect(); @@ -372,7 +372,7 @@ impl FnOutputPlan { } } -impl JniGenBuilder { +impl Declarations { /// The memoized lowered plan for one bound function — the "build the plan /// once and store it" stage [`JniFunctionPlan::build`] anticipated (issue /// #90). Keyed by the function's ident (bound functions live in one flat @@ -382,7 +382,7 @@ impl JniGenBuilder { /// (an unresolved converter) is passed through — it only occurs at the /// validation phase, which reports it and fails `resolve` before any /// emitter runs. Same interior-mutable contract as - /// [`JniGenBuilder::iface_spec`]; drift is guarded externally by the byte-identity + /// [`Declarations::iface_spec`]; drift is guarded externally by the byte-identity /// regen check (a plan change alters generated code). pub(crate) fn fn_plan( &self, @@ -402,10 +402,10 @@ impl JniGenBuilder { impl JniFunctionPlan { /// Lower `f`'s inputs. Deterministic over `(ext, registry, f)`. Emission - /// and validation go through the memo [`JniGenBuilder::fn_plan`], so the plan is + /// and validation go through the memo [`Declarations::fn_plan`], so the plan is /// built ONCE per function and shared; this is the underlying derivation. pub fn build( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, f: &syn::ItemFn, ) -> Result { @@ -516,7 +516,7 @@ fn kotlin_jvm_slots(ty: &str) -> usize { /// collection helper; recursive data-class leaves are valid in constructor /// expansions and reuse the same Rust/Kotlin lowering as ordinary parameters. fn classify_leaf( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, ident: &syn::Ident, ty: &syn::Type, @@ -610,7 +610,7 @@ fn classify_leaf( /// declared-surface facts from `classify_return`'s inputs /// (render_extern_decl's `ret_decl` reconstruction). fn build_output( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, f: &syn::ItemFn, ) -> Result { @@ -712,7 +712,7 @@ impl ReturnSurface { /// the single peel that subsumed both `classify_return`'s inline peel /// and the former `canonical_return_ty`. pub fn classify( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, output: &syn::ReturnType, ) -> (Self, syn::Type) { diff --git a/prebindgen/src/api/lang/jnigen/jni/fold.rs b/prebindgen/src/api/lang/jnigen/jni/fold.rs index 4e3caf49..02ea52f7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fold.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fold.rs @@ -55,7 +55,7 @@ pub(crate) fn handle_kt_type(strategy: &FoldStrategy, leaf: &kt::KtType) -> kt:: /// Typed Kotlin leaf of a projection. Declared handle projections /// take their configured class FQN; the built-in `u64` projection is Kotlin's /// stable unsigned scalar type. -pub(crate) fn projection_leaf_kt(ext: &JniGenBuilder, proj: &Projection) -> Option { +pub(crate) fn projection_leaf_kt(ext: &Declarations, proj: &Projection) -> Option { match proj.kind { ProjectionKind::Handle => ext.kotlin_fqn(&proj.leaf_key).map(kt::KtType::cls), ProjectionKind::Unsigned64 => Some(kt::KtType::cls("ULong")), @@ -169,7 +169,7 @@ pub(crate) fn is_kotlin_primitive_ty(t: &kt::KtType) -> bool { /// optional) and a leaf reconstructs with its wrap. #[allow(clippy::too_many_arguments)] pub(crate) fn flatten_struct_factory( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, s: &syn::ItemStruct, prefix: &str, diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index 6413ffef..ef23539e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -14,7 +14,7 @@ //! `run` with raw typed `jvalue`s: no per-leaf boxing upcalls, no erased //! `FunctionN`. //! -//! Each identity's spec is derived ONCE, through the [`JniGenBuilder::iface_spec`] +//! Each identity's spec is derived ONCE, through the [`Declarations::iface_spec`] //! memo keyed by [`SpecKey`], and shared by all three sites — the //! FQN/descriptor pair cannot drift between the artifact tiers (issue #107). //! The constructors stay deterministic over `(ext, registry)`; in debug @@ -668,7 +668,7 @@ fn subject_short(ty: &syn::Type) -> String { /// Package a subject type's interface lives in: the package of the type's /// registered Kotlin FQN, the root `ext.package` otherwise. -fn subject_package(ext: &JniGenBuilder, subject: &syn::Type) -> String { +fn subject_package(ext: &Declarations, subject: &syn::Type) -> String { let key = TypeKey::from_type(&crate::api::core::types_util::peel_ref_option_vec(subject)); ext.kotlin_fqn(&key) .and_then(|fqn| fqn.rsplit_once('.').map(|(p, _)| p.to_string())) @@ -678,7 +678,7 @@ fn subject_package(ext: &JniGenBuilder, subject: &syn::Type) -> String { /// The interface param list for a decomposition's leaves: names from /// [`plan_leaf_names`], typed + raw views per leaf. fn plan_leaf_params( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, leaves: &[crate::api::core::unfold::UnfoldLeaf], ) -> Option> { @@ -698,7 +698,7 @@ fn plan_leaf_params( /// inert-group nullability rule below have to hold at every one of those sites, /// not just where the plan happens to be walked as a whole. fn plan_leaf_param( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, name: String, leaf: &crate::api::core::unfold::UnfoldLeaf, @@ -743,7 +743,7 @@ fn plan_leaf_param( /// the close-unless-taken contract needs the native side to `close()` the /// wrapped object after the invoke. fn leaf_iface_param( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, name: String, out_ty: &syn::Type, @@ -855,7 +855,7 @@ fn leaf_iface_param( /// `run` (close-unless-taken). Replaces the former Rust-side `new_object` + /// post-invoke `close()`. `None` if the arg's projection FQN can't be resolved. pub(crate) fn owned_handle_iface_param( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, name: String, out_ty: &syn::Type, @@ -971,12 +971,12 @@ pub(crate) fn fixed_leaf_element_keys( } /// Derive the spec for one identity — the SINGLE construction point behind -/// [`JniGenBuilder::iface_spec`]. Any `syn` context comes from the key's stored +/// [`Declarations::iface_spec`]. Any `syn` context comes from the key's stored /// normalized type ([`TypeKey::to_type`] — a clone, not a reparse). A /// `Folder` derivation folds the fixed-builder typed-group view in per /// `DeconId` (see [`fixed_decon_ids`]). fn derive_iface_spec( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, key: &SpecKey, ) -> Option { @@ -999,7 +999,7 @@ fn derive_iface_spec( } } -impl JniGenBuilder { +impl Declarations { /// The memoized spec for one interface identity: derived once per /// generator run and shared by every consumer — the resolve-time /// trampoline, the per-function plan, and the declaration emitter — so @@ -1045,7 +1045,7 @@ impl JniGenBuilder { /// property types, not the wire — so it reassembles through the same inlined /// `when` over the tag that a sum-typed struct field gets. fn fixed_reassembly( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, source: &syn::Type, leaves: &[crate::api::core::unfold::UnfoldLeaf], @@ -1070,7 +1070,7 @@ fn fixed_reassembly( /// returning `Unit`. Named `Callback` (`Fn()` → `VoidCallback`), /// placed in the first arg type's package (root for `Fn()`). pub(crate) fn callback_iface_spec( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, cb_args: &[syn::Type], ) -> Option { @@ -1302,7 +1302,7 @@ pub(crate) fn callback_iface_spec( /// function's own plan. Named `Builder`, placed in the source /// type's package. pub(crate) fn builder_iface_spec( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, decon: &DeconId, ) -> Option { @@ -1328,7 +1328,7 @@ pub(crate) fn builder_iface_spec( /// the element's deconstructor declaration. Named `Folder`, /// placed in the element type's package. pub(crate) fn folder_iface_spec( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, decon: &DeconId, ) -> Option { @@ -1354,7 +1354,7 @@ pub(crate) fn folder_iface_spec( /// without a deconstructor — no declaration involved): /// `run(acc: A, element): A`. One shape per element type by construction. pub(crate) fn whole_folder_iface_spec( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, element: &syn::Type, ) -> Option { @@ -1386,11 +1386,11 @@ pub(crate) fn whole_folder_iface_spec( /// The folder spec for an `Iterable` plan: declaration-keyed when the /// element decomposes, whole-element otherwise. Thin KEY dispatch into the -/// [`JniGenBuilder::iface_spec`] memo — the fixed-builder typed-group view is +/// [`Declarations::iface_spec`] memo — the fixed-builder typed-group view is /// applied there per `DeconId` (the declaration identity the JVM resolves /// against), not per this plan's own `fixed_builder` flag. pub(crate) fn folder_iface_for_plan( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, plan: &UnfoldPlan, ) -> Option> { @@ -1415,7 +1415,7 @@ pub(crate) fn folder_iface_for_plan( /// emission (`write_iface_files`), keyed by the element's deconstructor so the /// two stay in lockstep. pub(crate) fn fixed_folder_typed_groups( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, decon: &DeconId, ) -> Option> { @@ -1449,7 +1449,7 @@ pub(crate) fn fixed_folder_typed_groups( /// Keyed by the error type's deconstructor declaration. Named /// `Handler`, placed in the error type's package. pub(crate) fn error_handler_iface_spec( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, decon: &DeconId, ) -> Option { @@ -1481,7 +1481,7 @@ pub(crate) fn error_handler_iface_spec( /// The shared infallible handler `JniErrorHandler { run(je: String?): R }` /// — every function without an error plan takes one; placed in the root /// package. -pub(crate) fn jni_error_handler_iface_spec(ext: &JniGenBuilder) -> IfaceSpec { +pub(crate) fn jni_error_handler_iface_spec(ext: &Declarations) -> IfaceSpec { let params = vec![IfaceParam::same( "je".to_string(), kt::KtType::string().nullable(), @@ -1521,11 +1521,11 @@ pub(crate) struct ErrorIfaces { /// The onError handler interfaces for a declared function — the always-present /// binding `JniErrorHandler` plus, for a fallible function, its /// declaration-keyed typed domain `Handler`. Thin KEY dispatch into the -/// [`JniGenBuilder::iface_spec`] memo. `None` = the domain handler is underivable +/// [`Declarations::iface_spec`] memo. `None` = the domain handler is underivable /// (the Rust emitter panics, the Kotlin renderer skips) — the binding channel /// alone always derives. pub(crate) fn onerror_iface_spec( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, fn_ident: &syn::Ident, ) -> Option { diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 8b85c8d9..76858832 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -1,6 +1,6 @@ -//! `KotlinExt` impl for [`JniGenBuilder`]. +//! `KotlinExt` impl for [`Declarations`]. //! -//! [`JniGenBuilder::write_kotlin`] is the single entry point for every Kotlin +//! [`Declarations::write_kotlin`] is the single entry point for every Kotlin //! file the JNI back-end emits. Each per-kind emitter builds in-memory //! [`kt::KtFile`] *model fragments* (declarations, not strings — the //! generator module `api::gen::kotlin` owns formatting and imports): @@ -36,8 +36,8 @@ use crate::api::{ /// Declaration of one auto-generated typed `NativeHandle` subclass. /// -/// Consumed by [`JniGenBuilder::write_typed_handles`] (and forwarded to -/// [`JniGenBuilder::write_jni_wrappers`] so the same promotion list can carve +/// Consumed by [`Declarations::write_typed_handles`] (and forwarded to +/// [`Declarations::write_jni_wrappers`] so the same promotion list can carve /// the matching skip-list). Each entry says "this Kotlin class is the /// home for the named `#[prebindgen]` functions"; everything else stays /// in the catch-all `JNIWrappers` object. @@ -72,7 +72,7 @@ impl super::JniGen { } } -impl JniGenBuilder { +impl Declarations { /// Kotlin emission body — the public entry point is /// `JniGen::write_kotlin`, which guarantees the registry /// was resolved first. @@ -463,7 +463,7 @@ pub(crate) struct OwnedTypedHandle { pub key: TypeKey, } -impl JniGenBuilder { +impl Declarations { /// Emit one Kotlin `enum class` file per `enum_class`-declared type. /// Variants render in declaration order using SCREAMING_SNAKE_CASE names; the /// constructor stores the Rust discriminant value (or the ordinal as @@ -1135,7 +1135,7 @@ impl JniGenBuilder { uses.into_iter() .filter_map(|u| { // Every spec comes from the SAME memo the wrappers and the - // resolve-time trampoline read ([`JniGenBuilder::iface_spec`]) — + // resolve-time trampoline read ([`Declarations::iface_spec`]) — // this site only classifies the extras: `is_error` ⇒ also // emit the zero-alloc capture holder used by the generated // wrappers' error channel; `fixed` carries a @@ -1695,7 +1695,7 @@ impl JniGenBuilder { } /// Emit the centralized Native-object Kotlin file under `output_dir` - /// (class name from [`JniGenBuilder::jni_native_class_name`]). Holds one + /// (class name from [`Declarations::jni_native_class_name`]). Holds one /// `external fun` per `#[prebindgen]` function — names mangled as methods /// via [`JniGenBuilder::set_method_name_mangle`], parameter and return types rendered at /// the JNI **wire** level so the declarations match the Rust extern @@ -1703,7 +1703,7 @@ impl JniGenBuilder { /// `Java___` (see `symbol`, #86). Every generated native /// call routes through this object, so its static initializer is the /// single point at which native-library loading can be triggered: when - /// [`JniGenBuilder::jni_native_init`] is set, its Kotlin statement(s) are emitted + /// [`Declarations::jni_native_init`] is set, its Kotlin statement(s) are emitted /// inside an `init { … }` block here (e.g. a reference to the consumer's /// own loader object). Unset, the holder stays free of any loading logic /// and the wrapper layer is responsible for loading. @@ -1844,7 +1844,7 @@ impl JniGenBuilder { /// same `handles` slice to both methods. /// /// Each handle's `kotlin_fqn` must be registered via - /// [`JniGenBuilder::kotlin_fqn`] so the generator can map it back to its + /// [`Declarations::kotlin_fqn`] so the generator can map it back to its /// Rust type-key (which identifies the first param to drop in each /// promoted method's signature). pub(crate) fn write_typed_handles( diff --git a/prebindgen/src/api/lang/jnigen/jni/metadata.rs b/prebindgen/src/api/lang/jnigen/jni/metadata.rs index 30cef3ae..16f12d2c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/metadata.rs +++ b/prebindgen/src/api/lang/jnigen/jni/metadata.rs @@ -64,7 +64,7 @@ pub enum ProjectionKind { #[derive(Clone, Debug)] pub struct Projection { /// Canonical key of the leaf type (e.g. `ZKeyExpr`, `ZenohId`); derive - /// the typed Kotlin FQN via `JniGenBuilder::kotlin_fqn` — a typed key, so the + /// the typed Kotlin FQN via `Declarations::kotlin_fqn` — a typed key, so the /// lookup cannot drift from the declaration table's constructor. pub leaf_key: crate::api::core::registry::TypeKey, /// `false` for `&T` borrows of a handle — still a projection (param diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index f4ac4b60..f898510c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -371,9 +371,13 @@ pub(crate) type MethodNameMangle = Arc String + Send pub struct JniGen { /// What the binding declared. The emitters read it for names, classes and /// decompositions. - pub(crate) gen: JniGenBuilder, + /// + /// A [`Declarations`], not the [`JniGenBuilder`] it came from: the builder's + /// mutators would otherwise ride into the finished object, and "finished" + /// would be a comment rather than a type. + decls: Declarations, /// Every crossing this binding needs, each with its conversion. - pub(crate) registry: crate::core::Registry, + registry: crate::core::Registry, } // Opaque — exists so `Result::expect_err` works in tests. @@ -402,7 +406,7 @@ impl JniGen { ) -> Result { Ok(crate::api::core::write::write_rust( &self.registry, - &self.gen, + &self.decls, out_path, )?) } @@ -413,19 +417,34 @@ impl JniGen { } /// What the binding declared. - pub fn declarations(&self) -> &JniGenBuilder { - &self.gen + pub fn declarations(&self) -> &Declarations { + &self.decls } } +/// Everything a binding **declared**, once it is done declaring. +/// +/// The read-only half of what used to be one `JniGenBuilder`. Splitting it is what +/// keeps the phase separation now that a generator owns its own registry: before +/// [#253](https://github.com/milyin/prebindgen/pull/253) a build script held a +/// `RegistryBuilder` and then a `Registry`, and that type split *was* the +/// enforcement. Once `JniGen::builder().source(..).build()` moved both inside the +/// generator, the built object was left holding the builder — mutators and all. +/// +/// So the mutators live on [`JniGenBuilder`] and nothing else, and this type — the +/// one a [`JniGen`] keeps and every emitter reads — **has no `&mut self` method at +/// all**. Not "the obvious ones were removed": none, which +/// `a_built_jnigen_exposes_no_mutation` checks by reading the source, because a +/// one-line grep once missed a multi-line signature and let `Registry::supply` +/// survive two commits that claimed it was gone. #[derive(Clone)] -pub struct JniGenBuilder { +pub struct Declarations { /// Single source of truth for the JVM/Kotlin namespace this binding /// targets, dot-separated (e.g. `io.zenoh.jni`). Empty = no prefix. /// Every derived form — slash-separated for `FindClass` - /// (`JniGenBuilder::java_class_prefix()`), `_`-mangled for JNI extern idents - /// (`JniGenBuilder::jni_class_path()`), dot-separated for Kotlin `package` - /// declarations — is computed from this at the point of use. + /// ([`Declarations::java_class_prefix`]), `_`-mangled for JNI extern idents, + /// dot-separated for Kotlin `package` declarations — is computed from this at + /// the point of use. /// `pub(crate)`: consumers go through [`JniGenBuilder::set_package_prefix`], /// whose trimming a direct field write would bypass. pub(crate) package: String, @@ -577,6 +596,18 @@ pub struct JniGenBuilder { /// "derived state, keyed by `(self, registry)`" contract as /// [`Self::iface_specs`]. pub(crate) fn_plans: std::cell::RefCell>>, +} + +/// Describe a JNI binding: state the Kotlin surface, then [`build`](Self::build). +/// +/// Holds the [`Declarations`] being filled and the sources to parse, and it is the +/// only type with mutators. `build` consumes it, hands the declarations to the +/// registry, and stores them in a [`JniGen`] — from where nothing can declare +/// anything again, because this type is gone by then. +#[derive(Clone, Default)] +pub struct JniGenBuilder { + /// What has been declared so far. Moved out whole by [`Self::build`]. + pub(crate) decls: Declarations, /// Where the `#[prebindgen]` items come from. /// @@ -584,6 +615,10 @@ pub struct JniGenBuilder { /// three feeders it has — so a build script says where the source is in the /// vocabulary the model already uses, and never names a `Flat` or a /// `Registry` itself. + /// + /// Not a declaration, which is why it stays here rather than moving across: + /// it is *input to* building, and keeping it out is what makes + /// [`Declarations`] mean one thing. pub(crate) sources: crate::api::core::flat::FlatBuilder, } diff --git a/prebindgen/src/api/lang/jnigen/jni/overloads.rs b/prebindgen/src/api/lang/jnigen/jni/overloads.rs index 475a2229..72d9c020 100644 --- a/prebindgen/src/api/lang/jnigen/jni/overloads.rs +++ b/prebindgen/src/api/lang/jnigen/jni/overloads.rs @@ -4,7 +4,7 @@ //! (`expectedSel: Int, expected00: Long?, …`); the raw call site passes magic //! ints and null-padding. Two mechanisms turn that into idiomatic Kotlin: //! -//! * **Proactive splittability check** ([`JniGenBuilder::validate_split_declarations`]): +//! * **Proactive splittability check** ([`Declarations::validate_split_declarations`]): //! every multi-variant `expand_param!` declaration (type-level or per-fn) is //! verified up front to be *splittable* — its arms surface as pairwise-distinct //! JVM signatures — so a function can safely request overloads. A collision is @@ -35,7 +35,7 @@ use crate::api::core::{ registry::Conversions, }; -impl JniGenBuilder { +impl Declarations { /// Proactively verify every multi-variant `expand_param!` declaration is /// splittable (its arms have pairwise-distinct JVM-erased signatures), so /// [`FunctionDecl::split_on_param`](crate::fun) can emit unambiguous @@ -108,7 +108,7 @@ impl JniGenBuilder { /// Uses the shared [`erase_kt_type`] model (issue #89 stage 2) so the split /// ambiguity check and the whole-artifact overload table agree on erasure. fn arm_erased_sig( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, target: &syn::Type, ctor: Option<&syn::Ident>, @@ -141,7 +141,7 @@ fn arm_erased_sig( /// type with no resolved surface. References are peeled first (`&T` erases /// like `T`). fn rust_type_erased( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, ty: &syn::Type, ) -> ErasedJvmType { @@ -405,7 +405,7 @@ fn resolve_split<'a>( /// Emits the cartesian product of the named params' arms; panics (a build /// error) if the product has two combinations with the same JVM signature. pub(crate) fn render_param_overloads( - ext: &JniGenBuilder, + ext: &Declarations, f: &syn::ItemFn, registry: &Registry, sel_fun: &kt::KtFun, diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index ae2e50ff..970d2bd5 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -67,7 +67,7 @@ pub(crate) fn build_enum_class(class_name: &str, item_enum: &syn::ItemEnum) -> k /// them), and the `fromParts` factory's raw-text class references carry their /// imports on the factory body `Code`. pub(crate) fn build_data_class( - ext: &JniGenBuilder, + ext: &Declarations, class_name: &str, item_struct: &syn::ItemStruct, registry: &Registry, @@ -254,7 +254,7 @@ pub(crate) fn build_data_class( /// to the matching `Java___` /// extern on the Rust side (the auto-generated destructor). pub(crate) fn build_typed_handle( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, class_name: &str, rust_doc_name: &str, @@ -464,7 +464,7 @@ pub(crate) fn is_iterable_fold(shape: &crate::api::core::unfold::UnfoldShape) -> /// `kt_return` (Unit is no return type). `None` if a param's converter isn't /// resolved. Full-FQN types throughout — no derivation-time shortening. pub(crate) fn render_extern_decl( - ext: &JniGenBuilder, + ext: &Declarations, f: &syn::ItemFn, registry: &Registry, ) -> Option { @@ -752,7 +752,7 @@ pub(crate) struct WrapperSurface { /// import set. Validation calls this directly and skips the body work /// (`build_native_call` / `render_body` / KDoc / opaque-lock collection). pub(crate) fn build_wrapper_surface( - ext: &JniGenBuilder, + ext: &Declarations, f: &syn::ItemFn, registry: &Registry, kotlin_name_override: Option<&str>, @@ -831,7 +831,7 @@ pub(crate) fn build_wrapper_surface( } pub(crate) fn render_wrapper_fn( - ext: &JniGenBuilder, + ext: &Declarations, f: &syn::ItemFn, registry: &Registry, kotlin_name_override: Option<&str>, @@ -888,7 +888,7 @@ pub(crate) fn render_wrapper_fn( /// the ordinary output machinery — plus the public lazily-initialized `val` /// that calls it once, on first use (see [`render_val_over_helper`]). pub(crate) fn render_const_val( - ext: &JniGenBuilder, + ext: &Declarations, package: &str, c: &syn::ItemConst, registry: &Registry, @@ -919,7 +919,7 @@ pub(crate) fn render_const_val( /// computed once, on first use, through the ordinary generated wrapper /// (one JNI call, exactly like a const getter). pub(crate) fn render_constant_fn_val( - ext: &JniGenBuilder, + ext: &Declarations, package: &str, f: &syn::ItemFn, registry: &Registry, @@ -949,7 +949,7 @@ pub(crate) fn render_constant_fn_val( /// is the binding-defined expression, evaluated once, on first use, through /// the generated getter. pub(crate) fn render_const_expr_val( - ext: &JniGenBuilder, + ext: &Declarations, package: &str, decl: &crate::api::lang::jnigen::jni::decl::ConstExprDecl, registry: &Registry, @@ -982,7 +982,7 @@ pub(crate) fn render_const_expr_val( /// `error(...)` at first use). Lazy, not eager: a consts-heavy package must /// not fire one JNI call per `val` at class-load (issue #58). fn render_val_over_helper( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, mut helper: kt::KtFun, val_name: String, @@ -1072,7 +1072,7 @@ struct DomainSink { /// instance-method receiver (the first param whose peeled type matches /// `receiver_key`), which is bound to `this` and dropped from the signature. fn classify_params( - ext: &JniGenBuilder, + ext: &Declarations, fplan: &JniFunctionPlan, registry: &Registry, imports: &mut BTreeSet, @@ -1262,7 +1262,7 @@ fn classify_params( /// with no such projection ⇒ the callback is passed directly (M1–M4 /// unchanged). fn classify_output( - ext: &JniGenBuilder, + ext: &Declarations, f: &syn::ItemFn, fplan: &JniFunctionPlan, registry: &Registry, @@ -1434,7 +1434,7 @@ fn classify_output( /// deliberately deferred to [`build_success_return`], after the native error /// captures have been checked. fn build_native_call( - ext: &JniGenBuilder, + ext: &Declarations, jni_call: &str, params: &[Param], out: &OutputPlan, @@ -1530,7 +1530,7 @@ fn build_native_call( /// This expression is emitted only after binding/domain captures have been /// checked, so a native failure placeholder can never reach an enum lookup, /// value projection, or erased-result cast. -fn build_success_return(ext: &JniGenBuilder, out: &OutputPlan, raw: &str) -> String { +fn build_success_return(ext: &Declarations, out: &OutputPlan, raw: &str) -> String { if let Some(p) = &out.projection { // Fold the wrap through the projection strategy. The wrap class is // the projection leaf's typed short name (a Handle's typed-handle @@ -1807,7 +1807,7 @@ fn render_value_stmt(bind: &str, body_expr: &str, opaques: &[Opaque]) -> kt::Cod /// statements are needed) so the caller can bind it to `__ret`, rethrow a /// captured sink error, then return. fn render_core_stmt( - ext: &JniGenBuilder, + ext: &Declarations, opaques: &[Opaque], body_expr: &str, imports: &mut BTreeSet, @@ -1912,7 +1912,7 @@ enum BodyReturn { } fn render_body( - ext: &JniGenBuilder, + ext: &Declarations, params: &[Param], opaques: &[Opaque], sink: &ErrorSink, @@ -2007,7 +2007,7 @@ fn render_body( /// value projection that can't be built Rust-side). /// Shared by the unfold builder/fold lambda and the callback lambda params. pub(crate) fn unfold_leaf_kt( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, out_ty: &syn::Type, nullable: bool, @@ -2145,7 +2145,7 @@ pub(crate) fn kotlin_for_wire(wire: &syn::Type) -> Option { /// and pick the JNI extern's wire return (`Long` for `Handle`). `None` for /// plain non-projection returns. pub(crate) fn classify_return( - ext: &JniGenBuilder, + ext: &Declarations, output: &syn::ReturnType, registry: &impl Conversions, ) -> Option<( diff --git a/prebindgen/src/api/lang/jnigen/jni/report.rs b/prebindgen/src/api/lang/jnigen/jni/report.rs index aa044ef6..aff72a82 100644 --- a/prebindgen/src/api/lang/jnigen/jni/report.rs +++ b/prebindgen/src/api/lang/jnigen/jni/report.rs @@ -248,7 +248,7 @@ impl super::JniGen { } } -impl JniGenBuilder { +impl Declarations { /// Human-readable class-kind name of a declared type (report use). pub(crate) fn class_kind_name(&self, key: &TypeKey) -> &'static str { let Some(cfg) = self.types.get(key) else { diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index 069f0e60..d4d0884a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -1,4 +1,4 @@ -//! Structural converter-selection policy for [`JniGenBuilder`]. +//! Structural converter-selection policy for [`Declarations`]. use super::*; use crate::api::core::registry::Conversions; @@ -33,7 +33,7 @@ fn ref_wildcard(r: &syn::TypeReference) -> syn::Type { syn::Type::Reference(pr) } -impl JniGenBuilder { +impl Declarations { /// Select the input converter for `ty`: terminals, user wrappers, then /// built-in structural wrappers. pub(crate) fn select_input_type( diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 5c24a21e..1ccde249 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -183,7 +183,7 @@ pub(crate) struct SumPlanField { /// name) — consistently for BOTH sides, where the former parallel walks /// could silently diverge on such edge cases. pub(crate) fn build_struct_plan( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, s: &syn::ItemStruct, depth: usize, @@ -215,7 +215,7 @@ pub(crate) fn build_struct_plan( /// `owner` is the dotted path used in diagnostics (`Config.mode`, /// `Reading::Exact.v0`). pub(crate) fn classify_field( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, ty: &syn::Type, owner: &str, @@ -464,7 +464,7 @@ impl PlanFieldKind { /// whenever a payload's converter happened to resolve later than this plan /// was first attempted. fn sum_plan_kind( - ext: &JniGenBuilder, + ext: &Declarations, registry: &impl Conversions, ty: &syn::Type, owner: &str, diff --git a/prebindgen/src/api/lang/jnigen/jni/symbol.rs b/prebindgen/src/api/lang/jnigen/jni/symbol.rs index 213c1fc3..449b5066 100644 --- a/prebindgen/src/api/lang/jnigen/jni/symbol.rs +++ b/prebindgen/src/api/lang/jnigen/jni/symbol.rs @@ -69,7 +69,7 @@ pub(crate) fn native_symbol(package: &str, class: &str, method: &str) -> String /// The long native symbol for **overloaded** natives: the short name plus /// `__` and the escaped argument signature (the descriptor between `(` and /// `)`, e.g. `ILjava/lang/String;` — `/`→`_`, `;`→`_2`, `[`→`_3`). Nothing -/// JniGenBuilder emits today is overloaded at the extern level (every `JNINative` +/// Declarations emits today is overloaded at the extern level (every `JNINative` /// method is uniquely named), so this is provided-but-unwired per #86's /// direction: if overloaded natives are ever emitted, they must come from /// this same abstraction. diff --git a/prebindgen/src/api/lang/jnigen/jni/symbols.rs b/prebindgen/src/api/lang/jnigen/jni/symbols.rs index bc09ba3f..fdce8523 100644 --- a/prebindgen/src/api/lang/jnigen/jni/symbols.rs +++ b/prebindgen/src/api/lang/jnigen/jni/symbols.rs @@ -34,10 +34,7 @@ use super::*; /// names colliding in one package, including a collision the mangler /// created. /// * **Warnings** — where the default mangler sanitized a Rust-derived name. -pub(crate) fn validate_symbols( - ext: &JniGenBuilder, - registry: &Registry, -) -> Vec { +pub(crate) fn validate_symbols(ext: &Declarations, registry: &Registry) -> Vec { let mut errors: Vec = Vec::new(); // (package, name) → origin, for top-level-unique Kotlin declarations. let mut top_level: BTreeMap<(String, String), String> = BTreeMap::new(); @@ -113,7 +110,7 @@ pub(crate) fn validate_symbols( // name `Companion` is ours — an artifact of emitting a companion at // all, not a name Kotlin reserves — so when a variant wants it the // generator renames the companion instead of making the source crate - // rename a legitimate variant (`JniGenBuilder::sum_companion_name`). + // rename a legitimate variant (`Declarations::sum_companion_name`). // // The interface's own name is different: BOTH colliding names come // from the source crate (the enum's name and its variant's), so the @@ -262,7 +259,7 @@ fn check_ident(name: &str, origin: &str, errors: &mut Vec) { /// Emit a `cargo:warning` for each Rust struct field (data-class property) or /// enum variant whose Kotlin name the default mangler had to change. -fn warn_derived_name_changes(ext: &JniGenBuilder, registry: &Registry) { +fn warn_derived_name_changes(ext: &Declarations, registry: &Registry) { let warn = |raw: &str, mangled: &str, what: &str, owner: &str| { if raw != mangled { println!( diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs b/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs index f888281d..b8c2b0ee 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/flatten.rs @@ -655,10 +655,11 @@ fn ignore_matching_acknowledges_naming_family() { .ignore(crate::ty!(ZUnusedThing)); // The predicate flows through the Prebindgen hook… { - let preds = jni.ignored_name_predicates(); + let preds = jni.decls.ignored_name_predicates(); assert_eq!(preds.len(), 1); assert!(preds[0]("detail_const_a") && !preds[0]("z_len")); assert!(jni + .decls .ignored_types() .contains(&TypeKey::parse("ZUnusedThing").expect("test type"))); } diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs index 07ece2b7..4d820448 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs @@ -31,6 +31,7 @@ mod consts; mod cross_artifact; mod flatten; mod niches; +mod phases; mod sealed; mod snapshots; mod symbols; diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/phases.rs b/prebindgen/src/api/lang/jnigen/jni/tests/phases.rs new file mode 100644 index 00000000..adbc1d89 --- /dev/null +++ b/prebindgen/src/api/lang/jnigen/jni/tests/phases.rs @@ -0,0 +1,154 @@ +//! The describe/built split is only worth having if something checks it. + +/// A built `JniGen` has no route back into being described. +/// +/// The sibling of `a_built_registry_exposes_no_mutation`, and it exists for the +/// same reason at one remove. That test guards `Registry`, which a build script +/// used to build itself — the `RegistryBuilder` → `Registry` split *was* the +/// enforcement, and it lived in the caller's hands. Once +/// `JniGen::builder().source(..).build()` moved both phases inside the generator, +/// the guarantee became this crate's to keep, and there was nothing keeping it: +/// `JniGen` held its `JniGenBuilder` whole, mutators and all. +/// +/// So [`Declarations`](crate::lang::Declarations) exists, and what this checks is +/// that it stays what it is: the type a built binding keeps, with **no `&mut self` +/// method at all** — not "the obvious ones were removed". +/// +/// Reads the source with **all** whitespace stripped, which makes a multi-line +/// signature indistinguishable from a one-line one. That is not defensive +/// styling: `Registry::supply` survived two commits claiming it was gone because +/// the check for it was a single-line grep and its signature spanned lines. +/// +/// **No file is skipped**, and the exemption is per `impl` block rather than per +/// file. `builder.rs` and `config.rs` are where the mutators live, but they also +/// hold six `impl Declarations` blocks between them — most of the read API — so +/// skipping those files wholesale, as the first version of this test did, left +/// the larger half of what it claims to guard unguarded. +/// +/// The self type is read off the `impl` header and matched by **suffix**, so a +/// qualified path is not a way around it: `impl super::JniGen` occurs in +/// `kotlin_emit.rs` and `report.rs`, and an unqualified-prefix match missed both. +#[test] +fn a_built_jnigen_exposes_no_mutation() { + let mut offenders: Vec = Vec::new(); + + let root = concat!(env!("CARGO_MANIFEST_DIR"), "/src/api/lang/jnigen/jni"); + let mut dirs = vec![std::path::PathBuf::from(root)]; + let mut sealed_blocks = 0usize; + while let Some(dir) = dirs.pop() { + for entry in std::fs::read_dir(&dir).expect("jnigen module dir") { + let path = entry.expect("dir entry").path(); + if path.is_dir() { + // `tests/` is this file's own home, and a fixture may hold a + // builder mutably on purpose. + if path.file_name().is_some_and(|n| n != "tests") { + dirs.push(path); + } + continue; + } + if path.extension().is_none_or(|e| e != "rs") { + continue; + } + let name = path + .file_name() + .expect("file name") + .to_string_lossy() + .to_string(); + let src = std::fs::read_to_string(&path).expect("read source"); + let bare: String = src.chars().filter(|c| !c.is_whitespace()).collect(); + + let mut rest = bare.as_str(); + while let Some(at) = rest.find("impl") { + rest = &rest[at + "impl".len()..]; + let Some(brace) = rest.find('{') else { break }; + let header = &rest[..brace]; + // A trait impl's self type is what follows `for`; an inherent + // impl's is the whole header. + let self_ty = header.rsplit("for").next().unwrap_or(header); + // Suffix, so `super::JniGen` and `crate::…::Declarations` count. + // `JniGenBuilder` does not end with either name, which is what + // exempts the describing half without naming a file. + let sealed = self_ty.ends_with("Declarations") || self_ty.ends_with("JniGen"); + + // The block's own text, up to the next `impl`. + let end = rest.find("impl").unwrap_or(rest.len()); + if sealed { + sealed_blocks += 1; + let mut scan = &rest[..end]; + while let Some(f) = scan.find("fn") { + scan = &scan[f + "fn".len()..]; + let Some(open) = scan.find('(') else { break }; + if scan[open..].starts_with("(&mutself") { + offenders.push(format!("{name}: fn {}(&mut self …)", &scan[..open])); + } + } + } + rest = &rest[end..]; + } + } + } + + assert!( + offenders.is_empty(), + "a built `JniGen` and its `Declarations` must be read-only; found mutation: {offenders:#?}" + ); + // A scanner that silently matches nothing passes forever. This is the count + // at the time of writing; it may drift upward freely, and a *drop* means the + // header matching stopped recognising blocks it used to. + assert!( + sealed_blocks >= 22, + "expected to scan at least 22 sealed impl blocks, saw {sealed_blocks} — \ + the header matching is no longer finding them" + ); +} + +/// The two `RefCell`s on `Declarations` are memos, and are named here so a third +/// one has to be argued for rather than merely added. +/// +/// Interior mutability is not a hole in the split: `iface_specs` and `fn_plans` +/// cache work derived from `(declarations, registry)`, both of which are frozen by +/// the time anything reads them. Caching a pure function of frozen inputs is not +/// re-declaring anything. A `RefCell` holding *declaration state* would be, and +/// that is what this catches. +#[test] +fn declarations_interior_mutability_is_only_the_two_memos() { + let src = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/api/lang/jnigen/jni/mod.rs" + )) + .expect("read mod.rs"); + + let decls = { + let start = src.find("pub struct Declarations {").expect("Declarations"); + let rest = &src[start..]; + let end = rest.find("\n}\n").expect("struct end"); + &rest[..end] + }; + + // Whitespace-stripped, for the reason the sibling test gives: `iface_specs` + // wraps onto a second line, so a line-based scan sees a bare `RefCell<..>` + // with no field name attached — which is how this check first "passed" on a + // field it could not identify. + let bare: String = decls.chars().filter(|c| !c.is_whitespace()).collect(); + + let mut named: Vec = Vec::new(); + let mut rest = bare.as_str(); + while let Some(at) = rest.find("RefCell") { + // The field name is the ident before the `:` that introduces this type. + let decl_start = rest[..at].rfind(',').map(|i| i + 1).unwrap_or(0); + let field = &rest[decl_start..at]; + named.push(field.trim_end_matches(|c: char| c != ':').to_string()); + rest = &rest[at + "RefCell".len()..]; + } + + assert_eq!( + named.len(), + 2, + "expected exactly the two memo fields, found: {named:#?}" + ); + assert!( + named.iter().any(|f| f.contains("iface_specs:")) + && named.iter().any(|f| f.contains("fn_plans:")), + "the two memos must be `iface_specs` and `fn_plans`, found: {named:#?}" + ); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs index 9259cd89..7fc4e25b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs @@ -298,7 +298,8 @@ fn reopened_ptr_class_keeps_gc_managed() { .package(crate::package!().class(first).class(second)); drop(registry); let key = TypeKey::from_type(&syn::parse_quote!(Session)); - jni.types + jni.decls + .types .get(&key) .expect("declared") .opaque() @@ -647,10 +648,14 @@ fn sum_is_its_own_type_kind() { .package(crate::package!().class(crate::sealed_class!(Reading))); let ty: syn::Type = syn::parse_quote!(Reading); assert!(matches!( - jni.type_kind(®istry, &ty), + jni.decls.type_kind(®istry, &ty), crate::api::lang::jnigen::jni::classify::TypeKind::Sum )); - let cfg = jni.types.get(&TypeKey::from_type(&ty)).expect("declared"); + let cfg = jni + .decls + .types + .get(&TypeKey::from_type(&ty)) + .expect("declared"); assert!(cfg.special_decl()); } diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index cc1f4e67..acab91e8 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -36,7 +36,7 @@ fn generated_converter_attr() -> syn::Attribute { // and consuming-crate wrapper exts like ZenohJniExt). // ────────────────────────────────────────────────────────────────────── -impl JniGenBuilder { +impl Declarations { /// Build the standard JNI input-converter `fn`. Body assumes in-scope /// `env: &mut JNIEnv` and `v: &` (or `v: ` for raw-pointer /// wires); produces a value of `rust`. Returned function has its name @@ -548,7 +548,7 @@ pub(crate) fn build_signal_domain_error_item() -> syn::Item { /// whose declare/undeclare fns are `#[cfg]`'d out of the scan) from /// producing destructors that reference types not in scope. pub(crate) fn build_handle_destructor_items( - ext: &JniGenBuilder, + ext: &Declarations, registry: &Registry, ) -> Vec { let mut named: Vec<(String, syn::Item)> = Vec::new(); @@ -619,7 +619,7 @@ pub(crate) fn build_handle_destructor_items( /// the two `Option<_>` sub-cases (direct-handle-by-value vs general), which /// share a pattern and so live together in [`JniGenBuilder::input_option`] to keep /// their original fall-through. -impl JniGenBuilder { +impl Declarations { /// `& _` / `& mut _` borrow: share T's resolved converter — `&T`'s entry /// points at the same `ItemFn` (the fn returns owned `T`; the call site in /// `emit_jni_function_wrapper` adds `&decoded`). Exists so the @@ -961,25 +961,31 @@ impl JniGenBuilder { /// /// The seam tests use to feed synthetic items without a source directory; /// `build` is this with the model read from [`Self::source`]. + /// + /// **This is the phase change.** The declarations are taken out of the + /// builder here and never put back: everything below runs against + /// `&decls`, and what it produces is stored in a [`JniGen`], which has no + /// route to a `JniGenBuilder` at all. pub(crate) fn build_with( self, registry: crate::api::core::registry::RegistryBuilder, ) -> Result { - let registry = self + let decls = self.decls; + let registry = decls .declare_into(registry)? - .validate_with(&self)? - .convert_with(|crossing, built| self.convert_crossing(crossing, built))? + .validate_with(&decls)? + .convert_with(|crossing, built| decls.convert_crossing(crossing, built))? .build()?; // Post-resolve invariants, run once here so the writers are pure reads // and a `JniGen` is valid by construction. - self.validate_resolved(®istry) + decls + .validate_resolved(®istry) .map_err(|message| crate::core::ScanError::AdapterInvariant { message })?; - Ok(JniGen { - gen: self, - registry, - }) + Ok(JniGen { decls, registry }) } +} +impl Declarations { /// Build the conversion for one crossing, against what is already built. /// /// `None` is *cannot*, never *not yet*: `crossings` hands them out @@ -1081,7 +1087,7 @@ impl JniGenBuilder { } } -impl JniGenBuilder { +impl Declarations { pub(crate) fn build_value_struct_decons( &self, registry: &impl Conversions, @@ -1191,7 +1197,7 @@ impl JniGenBuilder { } } -impl JniGenBuilder { +impl Declarations { fn dispatch_fn_input( &self, args: &[syn::Type], @@ -1215,7 +1221,7 @@ impl JniGenBuilder { } } -impl Prebindgen for JniGenBuilder { +impl Prebindgen for Declarations { /// Cross-language extras every JNI converter carries — currently /// the Kotlin value-context type name. Filled by the rank-N /// handlers at the same point they build the wire/body; the @@ -1565,7 +1571,7 @@ impl Prebindgen for JniGenBuilder { /// Structural converter builders — the rank-0 terminal chains and the rank-1 /// wrapper-shape handlers, now inherent helpers called by the structural /// [`Prebindgen::on_input_type`] / [`Prebindgen::on_output_type`]. -impl JniGenBuilder { +impl Declarations { // ── Input converters ───────────────────────────────────────────── /// Whole-type **input** terminal categories (opaque handle, enum, @@ -2182,7 +2188,7 @@ impl JniGenBuilder { /// These were trait methods the registry called back into the adapter from /// inside `resolve`. They are the adapter's own business now, gathered into the /// one value the registry is constructed from. -impl JniGenBuilder { +impl Declarations { /// Union of every `.fun(...)` list across all /// [`Self::package`] subpackage contexts. Each entry is a /// `#[prebindgen]` fn ident the user explicitly hooked into the diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index 85e46ec9..66266379 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -42,9 +42,9 @@ pub use jni::{ box_jboolean, box_jbyte, box_jchar, box_jdouble, box_jfloat, box_jint, box_jlong, box_jshort, decode_byte_array, decode_string, encode_byte_array, encode_string, matching, null_byte_array, null_string, CachedIfaceMethod, ClassDecl, ConstDecl, ConvertDecl, ConvertSourceDecl, - DataClassDecl, EnumClassDecl, ExpandDecl, ExpandParamDecl, ExpandReturnDecl, FieldsDecl, - FunctionDecl, IgnoreDecl, JniBindingError, JniGen, JniGenBuilder, PackageDecl, PtrClassDecl, - SealedClassDecl, VariantDecl, + DataClassDecl, Declarations, EnumClassDecl, ExpandDecl, ExpandParamDecl, ExpandReturnDecl, + FieldsDecl, FunctionDecl, IgnoreDecl, JniBindingError, JniGen, JniGenBuilder, PackageDecl, + PtrClassDecl, SealedClassDecl, VariantDecl, }; // Kotlin emission types now live in the standalone generator module diff --git a/prebindgen/src/lib.rs b/prebindgen/src/lib.rs index 2406307d..85de3744 100644 --- a/prebindgen/src/lib.rs +++ b/prebindgen/src/lib.rs @@ -348,7 +348,7 @@ pub mod lang { box_jboolean, box_jbyte, box_jchar, box_jdouble, box_jfloat, box_jint, box_jlong, box_jshort, decode_byte_array, decode_string, encode_byte_array, encode_string, matching, null_byte_array, null_string, CachedIfaceMethod, ClassDecl, ConstDecl, ConvertDecl, - ConvertSourceDecl, DataClassDecl, EnumClassDecl, ExpandDecl, ExpandParamDecl, + ConvertSourceDecl, DataClassDecl, Declarations, EnumClassDecl, ExpandDecl, ExpandParamDecl, ExpandReturnDecl, FieldsDecl, FunctionDecl, IgnoreDecl, JniBindingError, JniGen, JniGenBuilder, KotlinFile, PackageDecl, PtrClassDecl, SealedClassDecl, VariantDecl, WriteKotlinError, From a7ce2d53eafae6d772acf50074f29d9a59ab1f35 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 10:58:24 +0200 Subject: [PATCH 17/52] keep unstable examle autogenerated --- .../example_flat_aarch64_unstable.rs | 1226 +++++++++++++++++ .../include/example_flat_aarch64_unstable.h | 192 +++ 2 files changed, 1418 insertions(+) create mode 100644 examples/example-cbindgen/generated/example_flat_aarch64_unstable.rs create mode 100644 examples/example-cbindgen/include/example_flat_aarch64_unstable.h diff --git a/examples/example-cbindgen/generated/example_flat_aarch64_unstable.rs b/examples/example-cbindgen/generated/example_flat_aarch64_unstable.rs new file mode 100644 index 00000000..7e4a90fa --- /dev/null +++ b/examples/example-cbindgen/generated/example_flat_aarch64_unstable.rs @@ -0,0 +1,1226 @@ +extern "C" { + fn malloc(size: usize) -> *mut ::core::ffi::c_void; + fn free(ptr: *mut ::core::ffi::c_void); +} +#[allow(non_snake_case, dead_code)] +pub(crate) fn __cbg_alloc_cstr(s: ::std::string::String) -> *mut ::core::ffi::c_char { + let c = ::std::ffi::CString::new(s).unwrap_or_default(); + let bytes = c.as_bytes_with_nul(); + unsafe { + let p = malloc(bytes.len()) as *mut u8; + if p.is_null() { + return ::core::ptr::null_mut(); + } + ::core::ptr::copy_nonoverlapping(bytes.as_ptr(), p, bytes.len()); + p as *mut ::core::ffi::c_char + } +} +#[no_mangle] +#[allow(non_snake_case, unused_variables)] +pub unsafe extern "C" fn example_free(p: *mut ::core::ffi::c_void) { + free(p); +} +#[allow(non_snake_case, dead_code)] +pub(crate) unsafe fn __cbg_alloc_array(v: ::std::vec::Vec) -> (*mut W, usize) { + let n = v.len(); + if n == 0 { + return (::core::ptr::null_mut(), 0); + } + let p = malloc(n.wrapping_mul(::core::mem::size_of::())) as *mut W; + if p.is_null() { + return (::core::ptr::null_mut(), 0); + } + for (i, e) in v.into_iter().enumerate() { + ::core::ptr::write(p.add(i), e); + } + (p, n) +} +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct calculator_t { + _private: [u8; 0], +} +#[no_mangle] +#[allow(non_snake_case, unused_variables)] +pub unsafe extern "C" fn calculator_drop(this_: *mut calculator_t) { + if !this_.is_null() { + drop(::std::boxed::Box::from_raw(this_ as *mut example_flat::Calculator)); + } +} +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct caption_t { + pub id: u64, + pub text: *mut ::core::ffi::c_char, + pub emphatic: ::core::mem::MaybeUninit, +} +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct drawing_t { + pub id: u64, + pub shape: ::core::mem::MaybeUninit, +} +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct foo_t { + pub id: u64, + pub aarch64_field: u64, + pub unstable_field: u64, +} +#[repr(C)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum inside_foo_t { + DouddleDee = 14, + DouddleDum = 88, +} +#[repr(C)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum operation_t { + Add = 0, + Sub = 1, + Mul = 2, + Div = 3, +} +#[repr(C)] +#[allow(non_camel_case_types)] +pub enum note_t { + Silent, + Titled(caption_t), + After(u64), + Flagged(::core::mem::MaybeUninit), + Sketched(drawing_t), +} +#[no_mangle] +#[allow(non_snake_case, unused_variables)] +pub unsafe extern "C" fn note_drop(this_: *mut ::core::mem::MaybeUninit) { + if this_.is_null() { + return; + } + const _: () = { + assert!( + ::core::mem::size_of:: < note_t > () >= ::core::mem::size_of:: < + ::core::ffi::c_int > (), + "`note_t`: a #[repr(C)] enum with payload variants must be at least as large as its C `int` discriminant" + ); + }; + let __tag: ::core::ffi::c_int = ::core::ptr::read( + (*this_).as_ptr() as *const ::core::ffi::c_int, + ); + if !((__tag as i64) >= 0 && (__tag as i64) < 5i64) { + return; + } + match (*this_).assume_init_mut() { + note_t::Titled(__f0) => { + free((*__f0).text as *mut ::core::ffi::c_void); + (*__f0).text = ::core::ptr::null_mut(); + } + note_t::Sketched(__f0) => { + shape_drop(&mut (*__f0).shape); + } + _ => {} + } +} +#[repr(C)] +#[allow(non_camel_case_types)] +pub enum shape_t { + Empty, + Circle(f64), + Rect { width: f64, height: f64 }, + Labeled(*mut ::core::ffi::c_char, ::core::mem::MaybeUninit), +} +#[no_mangle] +#[allow(non_snake_case, unused_variables)] +pub unsafe extern "C" fn shape_drop(this_: *mut ::core::mem::MaybeUninit) { + if this_.is_null() { + return; + } + const _: () = { + assert!( + ::core::mem::size_of:: < shape_t > () >= ::core::mem::size_of:: < + ::core::ffi::c_int > (), + "`shape_t`: a #[repr(C)] enum with payload variants must be at least as large as its C `int` discriminant" + ); + }; + let __tag: ::core::ffi::c_int = ::core::ptr::read( + (*this_).as_ptr() as *const ::core::ffi::c_int, + ); + if !((__tag as i64) >= 0 && (__tag as i64) < 4i64) { + return; + } + match (*this_).assume_init_mut() { + shape_t::Labeled(__f0, __f1) => { + free(*__f0 as *mut ::core::ffi::c_void); + *__f0 = ::core::ptr::null_mut(); + } + _ => {} + } +} +#[repr(C)] +#[allow(non_camel_case_types)] +pub struct closure_value_t { + pub context: *mut ::core::ffi::c_void, + pub call: ::core::option::Option< + unsafe extern "C" fn(f64, *mut ::core::ffi::c_void), + >, + pub drop: ::core::option::Option, +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_Calculator( + v: *mut calculator_t, +) -> ::core::result::Result { + if v.is_null() { + return ::core::result::Result::Err( + ::std::string::String::from("null Calculator handle passed by value"), + ); + } + ::core::result::Result::Ok( + *::std::boxed::Box::from_raw(v as *mut example_flat::Calculator), + ) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_Caption(v: caption_t) -> example_flat::Caption { + example_flat::Caption { + id: v.id, + text: if v.text.is_null() { + ::std::string::String::new() + } else { + ::std::ffi::CStr::from_ptr(v.text).to_string_lossy().into_owned() + }, + emphatic: ::core::ptr::read(v.emphatic.as_ptr() as *const u8) != 0, + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_Drawing( + v: drawing_t, +) -> ::core::result::Result { + ::core::result::Result::Ok(example_flat::Drawing { + id: v.id, + shape: __cbg_in_Shape(v.shape)?, + }) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_Foo(v: foo_t) -> example_flat::Foo { + example_flat::Foo { + id: v.id, + aarch64_field: v.aarch64_field, + unstable_field: v.unstable_field, + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_InsideFoo( + v: ::core::mem::MaybeUninit, +) -> ::core::result::Result { + const _: () = { + assert!( + ::core::mem::size_of:: < inside_foo_t > () == ::core::mem::size_of:: < + ::core::ffi::c_int > (), + "`inside_foo_t`: a #[repr(C)] enum must have the size of a C `int`" + ); + assert!( + ::core::mem::align_of:: < inside_foo_t > () == ::core::mem::align_of:: < + ::core::ffi::c_int > (), + "`inside_foo_t`: a #[repr(C)] enum must have the alignment of a C `int`" + ); + }; + let __raw: ::core::ffi::c_int = ::core::ptr::read( + v.as_ptr() as *const ::core::ffi::c_int, + ); + if __raw == inside_foo_t::DouddleDee as ::core::ffi::c_int { + return ::core::result::Result::Ok(example_flat::InsideFoo::DouddleDee); + } + if __raw == inside_foo_t::DouddleDum as ::core::ffi::c_int { + return ::core::result::Result::Ok(example_flat::InsideFoo::DouddleDum); + } + ::core::result::Result::Err( + ::std::format!("invalid discriminant {} for `inside_foo_t`", __raw), + ) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_in_Millis(v: u64) -> example_flat::Millis { + example_flat::millis_from_raw(v) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_Note( + v: ::core::mem::MaybeUninit, +) -> ::core::result::Result { + const _: () = { + assert!( + ::core::mem::size_of:: < note_t > () >= ::core::mem::size_of:: < + ::core::ffi::c_int > (), + "`note_t`: a #[repr(C)] enum with payload variants must be at least as large as its C `int` discriminant" + ); + }; + let __tag: ::core::ffi::c_int = ::core::ptr::read( + v.as_ptr() as *const ::core::ffi::c_int, + ); + if !((__tag as i64) >= 0 && (__tag as i64) < 5i64) { + return ::core::result::Result::Err( + ::std::format!("invalid tag {} for `note_t` (expected 0..5)", __tag), + ); + } + let v = v.assume_init(); + ::core::result::Result::Ok( + match v { + note_t::Silent => example_flat::Note::Silent, + note_t::Titled(__f0) => example_flat::Note::Titled(__cbg_in_Caption(__f0)), + note_t::After(__f0) => example_flat::Note::After(__cbg_in_Millis(__f0)), + note_t::Flagged(__f0) => { + example_flat::Note::Flagged( + ::core::ptr::read(__f0.as_ptr() as *const u8) != 0, + ) + } + note_t::Sketched(__f0) => { + example_flat::Note::Sketched(__cbg_in_Drawing(__f0)?) + } + }, + ) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_Operation( + v: ::core::mem::MaybeUninit, +) -> ::core::result::Result { + const _: () = { + assert!( + ::core::mem::size_of:: < operation_t > () == ::core::mem::size_of:: < + ::core::ffi::c_int > (), + "`operation_t`: a #[repr(C)] enum must have the size of a C `int`" + ); + assert!( + ::core::mem::align_of:: < operation_t > () == ::core::mem::align_of:: < + ::core::ffi::c_int > (), + "`operation_t`: a #[repr(C)] enum must have the alignment of a C `int`" + ); + }; + let __raw: ::core::ffi::c_int = ::core::ptr::read( + v.as_ptr() as *const ::core::ffi::c_int, + ); + if __raw == operation_t::Add as ::core::ffi::c_int { + return ::core::result::Result::Ok(example_flat::Operation::Add); + } + if __raw == operation_t::Sub as ::core::ffi::c_int { + return ::core::result::Result::Ok(example_flat::Operation::Sub); + } + if __raw == operation_t::Mul as ::core::ffi::c_int { + return ::core::result::Result::Ok(example_flat::Operation::Mul); + } + if __raw == operation_t::Div as ::core::ffi::c_int { + return ::core::result::Result::Ok(example_flat::Operation::Div); + } + ::core::result::Result::Err( + ::std::format!("invalid discriminant {} for `operation_t`", __raw), + ) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_Shape( + v: ::core::mem::MaybeUninit, +) -> ::core::result::Result { + const _: () = { + assert!( + ::core::mem::size_of:: < shape_t > () >= ::core::mem::size_of:: < + ::core::ffi::c_int > (), + "`shape_t`: a #[repr(C)] enum with payload variants must be at least as large as its C `int` discriminant" + ); + }; + let __tag: ::core::ffi::c_int = ::core::ptr::read( + v.as_ptr() as *const ::core::ffi::c_int, + ); + if !((__tag as i64) >= 0 && (__tag as i64) < 4i64) { + return ::core::result::Result::Err( + ::std::format!("invalid tag {} for `shape_t` (expected 0..4)", __tag), + ); + } + let v = v.assume_init(); + ::core::result::Result::Ok( + match v { + shape_t::Empty => example_flat::Shape::Empty, + shape_t::Circle(__f0) => example_flat::Shape::Circle(__f0), + shape_t::Rect { width: __f0, height: __f1 } => { + example_flat::Shape::Rect { + width: __f0, + height: __f1, + } + } + shape_t::Labeled(__f0, __f1) => { + example_flat::Shape::Labeled( + if __f0.is_null() { + ::std::string::String::new() + } else { + ::std::ffi::CStr::from_ptr(__f0).to_string_lossy().into_owned() + }, + __cbg_in_Operation(__f1)?, + ) + } + }, + ) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_String( + v: *const ::core::ffi::c_char, +) -> ::core::result::Result<::std::string::String, ::std::string::String> { + if v.is_null() { + return ::core::result::Result::Err( + ::std::string::String::from("null pointer passed for String argument"), + ); + } + match ::std::ffi::CStr::from_ptr(v).to_str() { + ::core::result::Result::Ok(s) => ::core::result::Result::Ok(s.to_owned()), + ::core::result::Result::Err(_) => { + ::core::result::Result::Err( + ::std::string::String::from("invalid UTF-8 in String argument"), + ) + } + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in___Calculator<'a>( + v: *const calculator_t, +) -> ::core::result::Result<&'a example_flat::Calculator, ::std::string::String> { + if v.is_null() { + return ::core::result::Result::Err( + ::std::string::String::from("null Calculator pointer"), + ); + } + ::core::result::Result::Ok(&*(v as *const example_flat::Calculator)) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in___mut_Calculator<'a>( + v: *mut calculator_t, +) -> ::core::result::Result<&'a mut example_flat::Calculator, ::std::string::String> { + if v.is_null() { + return ::core::result::Result::Err( + ::std::string::String::from("null Calculator pointer"), + ); + } + ::core::result::Result::Ok(&mut *(v as *mut example_flat::Calculator)) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in___str<'a>( + v: *const ::core::ffi::c_char, +) -> ::core::result::Result<&'a str, ::std::string::String> { + if v.is_null() { + return ::core::result::Result::Err( + ::std::string::String::from("null pointer passed for str argument"), + ); + } + match ::std::ffi::CStr::from_ptr(v).to_str() { + ::core::result::Result::Ok(s) => ::core::result::Result::Ok(s), + ::core::result::Result::Err(_) => { + ::core::result::Result::Err( + ::std::string::String::from("invalid UTF-8 in str argument"), + ) + } + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_bool(v: ::core::mem::MaybeUninit) -> bool { + ::core::ptr::read(v.as_ptr() as *const u8) != 0 +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) unsafe fn __cbg_in_closure_value_t( + c: closure_value_t, +) -> impl Fn(f64) + Send + Sync + 'static { + struct __Ctx { + context: *mut ::core::ffi::c_void, + drop: ::core::option::Option, + } + unsafe impl ::core::marker::Send for __Ctx {} + unsafe impl ::core::marker::Sync for __Ctx {} + impl ::core::ops::Drop for __Ctx { + fn drop(&mut self) { + if let ::core::option::Option::Some(__d) = self.drop { + unsafe { __d(self.context) } + } + } + } + let __call = c.call; + let __ctx = ::std::sync::Arc::new(__Ctx { + context: c.context, + drop: c.drop, + }); + move |__a0: f64| { + let __w0 = __cbg_out_f64(__a0); + if let ::core::option::Option::Some(__f) = __call { + unsafe { __f(__w0, __ctx.context) } + } + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_in_f64(v: f64) -> f64 { + v +} +#[allow(non_snake_case, dead_code, unused_variables)] +pub(crate) fn __cbg_in_str() {} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_in_u64(v: u64) -> u64 { + v +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Calculator(v: example_flat::Calculator) -> *mut calculator_t { + ::std::boxed::Box::into_raw(::std::boxed::Box::new(v)) as *mut calculator_t +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Caption(v: example_flat::Caption) -> caption_t { + caption_t { + id: v.id, + text: __cbg_alloc_cstr(v.text), + emphatic: ::core::mem::MaybeUninit::new(v.emphatic), + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Drawing(v: example_flat::Drawing) -> drawing_t { + drawing_t { + id: v.id, + shape: __cbg_out_Shape(v.shape), + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Error(v: example_flat::Error) -> *mut ::core::ffi::c_char { + __cbg_alloc_cstr(example_flat::error_get_message(&v)) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Foo(v: example_flat::Foo) -> foo_t { + foo_t { + id: v.id, + aarch64_field: v.aarch64_field, + unstable_field: v.unstable_field, + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_InsideFoo(v: example_flat::InsideFoo) -> inside_foo_t { + match v { + example_flat::InsideFoo::DouddleDee => inside_foo_t::DouddleDee, + example_flat::InsideFoo::DouddleDum => inside_foo_t::DouddleDum, + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Millis(v: example_flat::Millis) -> u64 { + example_flat::millis_to_raw(&v) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Note(v: example_flat::Note) -> ::core::mem::MaybeUninit { + ::core::mem::MaybeUninit::new( + match v { + example_flat::Note::Silent => note_t::Silent, + example_flat::Note::Titled(__f0) => note_t::Titled(__cbg_out_Caption(__f0)), + example_flat::Note::After(__f0) => note_t::After(__cbg_out_Millis(__f0)), + example_flat::Note::Flagged(__f0) => { + note_t::Flagged(::core::mem::MaybeUninit::new(__f0)) + } + example_flat::Note::Sketched(__f0) => { + note_t::Sketched(__cbg_out_Drawing(__f0)) + } + }, + ) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Operation(v: example_flat::Operation) -> operation_t { + match v { + example_flat::Operation::Add => operation_t::Add, + example_flat::Operation::Sub => operation_t::Sub, + example_flat::Operation::Mul => operation_t::Mul, + example_flat::Operation::Div => operation_t::Div, + } +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_Shape( + v: example_flat::Shape, +) -> ::core::mem::MaybeUninit { + ::core::mem::MaybeUninit::new( + match v { + example_flat::Shape::Empty => shape_t::Empty, + example_flat::Shape::Circle(__f0) => shape_t::Circle(__f0), + example_flat::Shape::Rect { width: __f0, height: __f1 } => { + shape_t::Rect { + width: __f0, + height: __f1, + } + } + example_flat::Shape::Labeled(__f0, __f1) => { + shape_t::Labeled( + __cbg_alloc_cstr(__f0), + ::core::mem::MaybeUninit::new(__cbg_out_Operation(__f1)), + ) + } + }, + ) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_String(v: ::std::string::String) -> *mut ::core::ffi::c_char { + __cbg_alloc_cstr(v) +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_bool(v: bool) -> bool { + v +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_f64(v: f64) -> f64 { + v +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_i32(v: i32) -> i32 { + v +} +#[allow(non_snake_case, unused_variables, dead_code)] +pub(crate) fn __cbg_out_u64(v: u64) -> u64 { + v +} +#[allow(non_snake_case, dead_code, unused_variables)] +pub(crate) fn __cbg_out_unit(v: ()) {} +#[allow(non_snake_case, dead_code, unused)] +pub(crate) fn __cbg_outmark_vec_f64() {} +#[allow(non_snake_case, dead_code, unused)] +pub(crate) fn __cbg_result_Result___Calculator___Error__() {} +#[allow(non_snake_case, dead_code, unused)] +pub(crate) fn __cbg_result_Result___f64___Error__() {} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_absorb( + a: *mut calculator_t, + b: *const calculator_t, + out: *mut f64, + e: *mut *mut ::core::ffi::c_char, +) -> bool { + if !(a as *const ()).is_null() && (a as *const ()) == (b as *const ()) { + let __msg = ::std::string::String::from( + "aliasing arguments: `a` (consumed) and `b` (borrowed) are the same `Calculator` — a consumed or exclusively-borrowed resource may not be named twice in one call", + ); + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return false; + } + let a = match __cbg_in_Calculator(a) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return false; + } + }; + let b = match __cbg_in___Calculator(b) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return false; + } + }; + match example_flat::calculator_absorb(a, b) { + ::core::result::Result::Ok(__v) => { + *out = __cbg_out_f64(__v); + true + } + ::core::result::Result::Err(__err) => { + if !e.is_null() { + *e = __cbg_out_Error(__err); + } + false + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_apply( + c: *mut calculator_t, + op: ::core::mem::MaybeUninit, + operand: f64, + out: *mut f64, + e: *mut *mut ::core::ffi::c_char, +) -> bool { + let c = match __cbg_in___mut_Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return false; + } + }; + let op = match __cbg_in_Operation(op) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return false; + } + }; + let operand = __cbg_in_f64(operand); + match example_flat::calculator_apply(c, op, operand) { + ::core::result::Result::Ok(__v) => { + *out = __cbg_out_f64(__v); + true + } + ::core::result::Result::Err(__err) => { + if !e.is_null() { + *e = __cbg_out_Error(__err); + } + false + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_for_each( + c: *const calculator_t, + f: closure_value_t, +) { + let c = match __cbg_in___Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let f = __cbg_in_closure_value_t(f); + example_flat::calculator_for_each(c, f); +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_get_count(c: *const calculator_t) -> u64 { + let c = match __cbg_in___Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::calculator_get_count(c); + let __ret: u64; + __ret = __cbg_out_u64(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_get_history( + c: *const calculator_t, + len: *mut usize, +) -> *mut f64 { + let c = match __cbg_in___Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::calculator_get_history(c); + let __ret: *mut f64; + let __arr: ::std::vec::Vec = __v.into_iter().map(__cbg_out_f64).collect(); + let (__p, __n) = __cbg_alloc_array(__arr); + __ret = __p; + *len = __n; + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_get_value(c: *const calculator_t) -> f64 { + let c = match __cbg_in___Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::calculator_get_value(c); + let __ret: f64; + __ret = __cbg_out_f64(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_is(c: *const calculator_t, value: f64) -> bool { + let c = match __cbg_in___Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let value = __cbg_in_f64(value); + let __v = example_flat::calculator_is(c, value); + let __ret: bool; + __ret = __cbg_out_bool(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_merge( + a: *mut calculator_t, + b: *mut calculator_t, + e: *mut *mut ::core::ffi::c_char, +) -> *mut calculator_t { + if !(a as *const ()).is_null() && (a as *const ()) == (b as *const ()) { + let __msg = ::std::string::String::from( + "aliasing arguments: `a` (consumed) and `b` (consumed) are the same `Calculator` — a consumed or exclusively-borrowed resource may not be named twice in one call", + ); + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return ::core::ptr::null_mut(); + } + let a = match __cbg_in_Calculator(a) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return ::core::ptr::null_mut(); + } + }; + let b = match __cbg_in_Calculator(b) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return ::core::ptr::null_mut(); + } + }; + match example_flat::calculator_merge(a, b) { + ::core::result::Result::Ok(__v) => { + let __ret: *mut calculator_t; + __ret = __cbg_out_Calculator(__v); + __ret + } + ::core::result::Result::Err(__err) => { + if !e.is_null() { + *e = __cbg_out_Error(__err); + } + ::core::ptr::null_mut() + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_new() -> *mut calculator_t { + let __v = example_flat::calculator_new(); + let __ret: *mut calculator_t; + __ret = __cbg_out_Calculator(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_new_clone( + c: *const calculator_t, +) -> *mut calculator_t { + let c = match __cbg_in___Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::calculator_new_clone(c); + let __ret: *mut calculator_t; + __ret = __cbg_out_Calculator(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_new_from_str( + s: *const ::core::ffi::c_char, + e: *mut *mut ::core::ffi::c_char, +) -> *mut calculator_t { + let s = match __cbg_in___str(s) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return ::core::ptr::null_mut(); + } + }; + match example_flat::calculator_new_from_str(s) { + ::core::result::Result::Ok(__v) => { + let __ret: *mut calculator_t; + __ret = __cbg_out_Calculator(__v); + __ret + } + ::core::result::Result::Err(__err) => { + if !e.is_null() { + *e = __cbg_out_Error(__err); + } + ::core::ptr::null_mut() + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_reset(c: *mut calculator_t) { + let c = match __cbg_in___mut_Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + example_flat::calculator_reset(c); +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn calculator_to_string( + c: *const calculator_t, +) -> *mut ::core::ffi::c_char { + let c = match __cbg_in___Calculator(c) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::calculator_to_string(c); + let __ret: *mut ::core::ffi::c_char; + __ret = __cbg_out_String(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn caption_new( + id: u64, + text: *const ::core::ffi::c_char, + emphatic: ::core::mem::MaybeUninit, +) -> caption_t { + let id = __cbg_in_u64(id); + let text = match __cbg_in___str(text) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let emphatic = __cbg_in_bool(emphatic); + let __v = example_flat::caption_new(id, text, emphatic); + let __ret: caption_t; + __ret = __cbg_out_Caption(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn drawing_get_shape( + d: drawing_t, +) -> ::core::mem::MaybeUninit { + let d = match __cbg_in_Drawing(d) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::drawing_get_shape(d); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Shape(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn drawing_new( + id: u64, + shape: ::core::mem::MaybeUninit, +) -> drawing_t { + let id = __cbg_in_u64(id); + let shape = match __cbg_in_Shape(shape) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::drawing_new(id, shape); + let __ret: drawing_t; + __ret = __cbg_out_Drawing(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn foo_get_id(f: foo_t) -> u64 { + let f = __cbg_in_Foo(f); + let __v = example_flat::foo_get_id(f); + let __ret: u64; + __ret = __cbg_out_u64(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn foo_new(id: u64) -> foo_t { + let id = __cbg_in_u64(id); + let __v = example_flat::foo_new(id); + let __ret: foo_t; + __ret = __cbg_out_Foo(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn inside_foo_default() -> inside_foo_t { + let __v = example_flat::inside_foo_default(); + let __ret: inside_foo_t; + __ret = __cbg_out_InsideFoo(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn inside_foo_value( + x: ::core::mem::MaybeUninit, +) -> i32 { + let x = match __cbg_in_InsideFoo(x) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::inside_foo_value(x); + let __ret: i32; + __ret = __cbg_out_i32(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn note_emphatic(n: ::core::mem::MaybeUninit) -> bool { + let n = match __cbg_in_Note(n) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::note_emphatic(n); + let __ret: bool; + __ret = __cbg_out_bool(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn note_new_after( + millis: u64, +) -> ::core::mem::MaybeUninit { + let millis = __cbg_in_u64(millis); + let __v = example_flat::note_new_after(millis); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Note(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn note_new_flagged( + flag: ::core::mem::MaybeUninit, +) -> ::core::mem::MaybeUninit { + let flag = __cbg_in_bool(flag); + let __v = example_flat::note_new_flagged(flag); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Note(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn note_new_silent() -> ::core::mem::MaybeUninit { + let __v = example_flat::note_new_silent(); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Note(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn note_new_sketched( + id: u64, + label: *const ::core::ffi::c_char, +) -> ::core::mem::MaybeUninit { + let id = __cbg_in_u64(id); + let label = match __cbg_in___str(label) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::note_new_sketched(id, label); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Note(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn note_new_titled( + id: u64, + text: *const ::core::ffi::c_char, + emphatic: ::core::mem::MaybeUninit, +) -> ::core::mem::MaybeUninit { + let id = __cbg_in_u64(id); + let text = match __cbg_in___str(text) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let emphatic = __cbg_in_bool(emphatic); + let __v = example_flat::note_new_titled(id, text, emphatic); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Note(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn note_value(n: ::core::mem::MaybeUninit) -> u64 { + let n = match __cbg_in_Note(n) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::note_value(n); + let __ret: u64; + __ret = __cbg_out_u64(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn shape_area(s: ::core::mem::MaybeUninit) -> f64 { + let s = match __cbg_in_Shape(s) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::shape_area(s); + let __ret: f64; + __ret = __cbg_out_f64(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn shape_get_label( + s: ::core::mem::MaybeUninit, +) -> *mut ::core::ffi::c_char { + let s = match __cbg_in_Shape(s) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::shape_get_label(s); + let __ret: *mut ::core::ffi::c_char; + __ret = __cbg_out_String(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn shape_new_circle( + radius: f64, +) -> ::core::mem::MaybeUninit { + let radius = __cbg_in_f64(radius); + let __v = example_flat::shape_new_circle(radius); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Shape(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn shape_new_empty() -> ::core::mem::MaybeUninit { + let __v = example_flat::shape_new_empty(); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Shape(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn shape_new_labeled( + label: *const ::core::ffi::c_char, + op: ::core::mem::MaybeUninit, +) -> ::core::mem::MaybeUninit { + let label = match __cbg_in___str(label) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let op = match __cbg_in_Operation(op) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + panic!("{}", __msg); + } + }; + let __v = example_flat::shape_new_labeled(label, op); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Shape(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn shape_new_rect( + width: f64, + height: f64, +) -> ::core::mem::MaybeUninit { + let width = __cbg_in_f64(width); + let height = __cbg_in_f64(height); + let __v = example_flat::shape_new_rect(width, height); + let __ret: ::core::mem::MaybeUninit; + __ret = __cbg_out_Shape(__v); + __ret +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, unused_unsafe, dead_code)] +pub unsafe extern "C" fn shape_try_area( + s: ::core::mem::MaybeUninit, + out: *mut f64, + e: *mut *mut ::core::ffi::c_char, +) -> bool { + let s = match __cbg_in_Shape(s) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__msg) => { + if !e.is_null() { + *e = __cbg_out_Error( + >::from(__msg), + ); + } + return false; + } + }; + match example_flat::shape_try_area(s) { + ::core::result::Result::Ok(__v) => { + *out = __cbg_out_f64(__v); + true + } + ::core::result::Result::Err(__err) => { + if !e.is_null() { + *e = __cbg_out_Error(__err); + } + false + } + } +} +const _: () = { + konst::assertc_eq!( + example_flat::FEATURES, "example-flat/unstable", + "prebindgen: features mismatch between source crate and prebindgen generated file.\n\ + This usually happens if source crate is compiled with different feature set\n\ + for build dependencies and for library usage. You may need to explicitly set\n\ + the necessary features." + ); +}; diff --git a/examples/example-cbindgen/include/example_flat_aarch64_unstable.h b/examples/example-cbindgen/include/example_flat_aarch64_unstable.h new file mode 100644 index 00000000..7417e755 --- /dev/null +++ b/examples/example-cbindgen/include/example_flat_aarch64_unstable.h @@ -0,0 +1,192 @@ +/* Generated by cbindgen for example_flat bindings */ + +#ifndef EXAMPLE_FLAT_H +#define EXAMPLE_FLAT_H + +/* Generated with cbindgen:0.29.4 */ + +/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ + +#include +#include +#include +#include + +typedef enum operation_t { + Add = 0, + Sub = 1, + Mul = 2, + Div = 3, +} operation_t; + +typedef enum inside_foo_t { + DouddleDee = 14, + DouddleDum = 88, +} inside_foo_t; + +typedef struct calculator_t { + uint8_t _private[0]; +} calculator_t; + +typedef struct caption_t { + uint64_t id; + char *text; + bool emphatic; +} caption_t; + +typedef enum shape_t_Tag { + Empty, + Circle, + Rect, + Labeled, +} shape_t_Tag; + +typedef struct Rect_Body { + double width; + double height; +} Rect_Body; + +typedef struct Labeled_Body { + char *_0; + enum operation_t _1; +} Labeled_Body; + +typedef struct shape_t { + shape_t_Tag tag; + union { + struct { + double circle; + }; + Rect_Body rect; + Labeled_Body labeled; + }; +} shape_t; + +typedef struct drawing_t { + uint64_t id; + struct shape_t shape; +} drawing_t; + +typedef enum note_t_Tag { + Silent, + Titled, + After, + Flagged, + Sketched, +} note_t_Tag; + +typedef struct note_t { + note_t_Tag tag; + union { + struct { + struct caption_t titled; + }; + struct { + uint64_t after; + }; + struct { + bool flagged; + }; + struct { + struct drawing_t sketched; + }; + }; +} note_t; + +typedef struct closure_value_t { + void *context; + void (*call)(double, void*); + void (*drop)(void*); +} closure_value_t; + +typedef struct foo_t { + uint64_t id; + uint64_t aarch64_field; + uint64_t unstable_field; +} foo_t; + +extern void *malloc(uintptr_t size); + +extern void free(void *ptr); + +void example_free(void *p); + +void calculator_drop(struct calculator_t *this_); + +void note_drop(struct note_t *this_); + +void shape_drop(struct shape_t *this_); + +bool calculator_absorb(struct calculator_t *a, const struct calculator_t *b, double *out, char **e); + +bool calculator_apply(struct calculator_t *c, + enum operation_t op, + double operand, + double *out, + char **e); + +void calculator_for_each(const struct calculator_t *c, struct closure_value_t f); + +uint64_t calculator_get_count(const struct calculator_t *c); + +double *calculator_get_history(const struct calculator_t *c, uintptr_t *len); + +double calculator_get_value(const struct calculator_t *c); + +bool calculator_is(const struct calculator_t *c, double value); + +struct calculator_t *calculator_merge(struct calculator_t *a, struct calculator_t *b, char **e); + +struct calculator_t *calculator_new(void); + +struct calculator_t *calculator_new_clone(const struct calculator_t *c); + +struct calculator_t *calculator_new_from_str(const char *s, char **e); + +void calculator_reset(struct calculator_t *c); + +char *calculator_to_string(const struct calculator_t *c); + +struct caption_t caption_new(uint64_t id, const char *text, bool emphatic); + +struct shape_t drawing_get_shape(struct drawing_t d); + +struct drawing_t drawing_new(uint64_t id, struct shape_t shape); + +uint64_t foo_get_id(struct foo_t f); + +struct foo_t foo_new(uint64_t id); + +enum inside_foo_t inside_foo_default(void); + +int32_t inside_foo_value(enum inside_foo_t x); + +bool note_emphatic(struct note_t n); + +struct note_t note_new_after(uint64_t millis); + +struct note_t note_new_flagged(bool flag); + +struct note_t note_new_silent(void); + +struct note_t note_new_sketched(uint64_t id, const char *label); + +struct note_t note_new_titled(uint64_t id, const char *text, bool emphatic); + +uint64_t note_value(struct note_t n); + +double shape_area(struct shape_t s); + +char *shape_get_label(struct shape_t s); + +struct shape_t shape_new_circle(double radius); + +struct shape_t shape_new_empty(void); + +struct shape_t shape_new_labeled(const char *label, enum operation_t op); + +struct shape_t shape_new_rect(double width, double height); + +bool shape_try_area(struct shape_t s, double *out, char **e); + +#endif /* EXAMPLE_FLAT_H */ From 47c48692fb75a6f876137c524a0c51c4656cea50 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 10:59:18 +0200 Subject: [PATCH 18/52] Live counts belong in the ledger and the PR, not in prose here (#259) The Size-of-the-problem table copied the ledger into prose, so it was wrong the moment L2a merged: it still listed types_util at 14 and registry/walk at 9, a file that no longer exists. This is the third time a number in this file went stale between merges, which is the signal that it is in the wrong place. The seed stays -- 202, split 71/25/106 -- because a seed is a fixed fact worth keeping. The current count is boundary.ledger, which is generated and therefore cannot drift, and the stage-by-stage history is #229, which is where stage state is edited. Same for L4's parenthetical count. Kept the lesson the table was carrying, since that does not go stale: a site leaving is not a site migrating, and a stage that does not say which it achieved is not reporting. --- docs/language-integration.md | 38 +++++++++++++++++------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 5efac843..8ee8d31f 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -85,25 +85,23 @@ scoreboard for this whole program. ## Size of the problem -Seeded by L0 at **202 classification sites** outside the frontend. The second -population it was seeded alongside — **113** reads of the registry's `syn`-keyed -item maps — is **gone**: L1.5 deleted those maps, so every one of those reads now -goes through the model. What remains is the ledger, and it now stands at **167**. - -| Area | Ledger sites | Seeded | Stage | -|---|---:|---:|---| -| `api/core` (`unfold` 16, `types_util` 14, `registry/walk` 9, `registry/scan` 2, `expand` 4) | 45 | 71 | L2 | -| `api/lang/cbindgen` | 25 | 25 | L3 | -| `api/lang/jnigen` | 97 | 106 | L4 | -| **total** | **167** | **202** | | - -**The ledger has started falling.** The whole −35 is -[#248](https://github.com/milyin/prebindgen/pull/248), which deleted the pattern -engine: `types_util` 40 → 14, `jnigen/builder` 13 → 4, and `registry` 11 split -into `walk` 9 + `scan` 2. Not one of those sites was migrated to read an element — -they went away because the code that held them went away, which is the cheaper -half of L2 and the reason it was done first. L1.5 before it moved reads off the -deleted maps but took only two classifiers off the ledger. +Seeded by L0 at **202 classification sites** outside the frontend, split +`api/core` 71, `cbindgen` 25, `jnigen` 106. The second population it was seeded +alongside — **113** reads of the registry's `syn`-keyed item maps — is **gone**: +L1.5 deleted those maps, so every one of those reads now goes through the model. + +Those are the numbers this document keeps, because a seed is a fixed fact. **The +current count is in `boundary.ledger`**, which is generated, and its stage-by-stage +history is in [#229](https://github.com/milyin/prebindgen/pull/229). A table of +live counts copied into prose here would be wrong after the next merge, and was. + +Two things the falling count has taught, which the count itself does not show: + +**A site leaving is not the same as a site migrating.** The largest single drop was +[#248](https://github.com/milyin/prebindgen/pull/248) deleting a pattern engine +whose tables held one entry in the whole crate. Nothing was migrated to read an +element; the code holding the sites went away. Both are real progress, and a stage +that does not say which one it achieved is not reporting. Not every site must go: some inspect types the adapter itself *synthesized* — wire types, converter signatures — which is legitimately the adapter's business. @@ -122,7 +120,7 @@ moves it. | L1.75 | The registry becomes describable | **done** — #249–#253, squashed into #248's commit | | L2 | `api/core` stops classifying source syntax | **in progress** — [#248](https://github.com/milyin/prebindgen/pull/248) took 35 of 71 | | L3 | `Cbindgen` consumes elements | not started | -| L4 | `JniGen` consumes elements *(the long pole — 97 sites)* | not started | +| L4 | `JniGen` consumes elements *(the long pole)* | not started | | L5 | Close the seam: the public contract stops being `syn` | not started | ### L0 — the parser — **done** (#227) From 8c905f2a0b12abd0b1beb79fc77c3315296b2f1b Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 13:28:06 +0200 Subject: [PATCH 19/52] L2b: one layer read, and api/core stops classifying types (#261) * L2b: one layer read, and api/core stops classifying types Ledger 154 -> 135. expand.rs 4 -> 0, unfold.rs 16 -> 1. Every one of those sites asked the same question by taking a spelling apart: option_inner_type, then vec_inner_type, then a syn::Type::Reference match, each rebuilding the peeled type as it went. That is Optional(Sequence(Ref)), which the model already names. TypeRef::layers() answers it once. The order is fixed and each layer peels at most once -- Optional, then Sequence, then Ref -- because that is the shape of a boundary, an optional list of borrowed things. Reading layers in whatever order they appear would call Vec> an optional list, which is a different type. A layer out of position stays on the core, where a caller that cares matches kind directly: &[T] reports by_ref with a Sequence core, because the borrow is outside the run. `wrappers` carries the three spellings a fold REGISTERS -- the whole type, the collection, the element -- which is a different question from what finally crosses, and the one site that un-requires a leaf-delivered layer needs all three. TypeRef::fallible_parts() covers the Result sites. Not every site wanted the full peel, and one test proved it. peel_borrow is the model-driven peel_ref, kept separate deliberately: a site that peels only the borrow means it, because the layer underneath is what it is about to classify. Vec answers (false, Vec) there and (iterable, T) in layers(), and using layers() at the callback-arg site turned "this arg is a collection" into "this arg is a T" -- caught by callback_arg_nonbare_skipped, which is exactly the fixture for it. Also migrated the ident-name classifiers the ledger cannot see -- the seg.ident == "Option" family. Nine call sites of option_inner_type / vec_inner_type / result_err_type in unfold, and result_ok_type in expand, which had the model's reading in hand already (f.ret is a TypeRef). Those are a listed blind spot of the check, so leaving them would have let api/core keep classifying while the count said otherwise. peel_ref stays, and so does its one ledger site: jnigen calls it three times, so it cannot be deleted until L4. The plan said it had no adapter callers; that was wrong, and the honest count for this stage is 1 rather than 0. api/core is now 13: types_util 10, registry/scan 2, unfold 1. The ten in types_util have between 13 and 40 adapter callers each and take no model, so only L3/L4 can free them. The two in scan inspect a key a build-script author wrote, to diagnose that spelling, and stay for good. Reported: regen-check byte-identical on every committed artifact. Explained: nothing about what is generated moves -- the classification is the same, taken from the model instead of re-derived. Asserted: no site in unfold or expand decides what a source type means. * A Vec param is not a T param, even when T has a constructor Review catch, and a real one: layers() peels Sequence, but the expand sites historically peeled only Option and the borrow, and FoldPlan still has no iterable arm -- its shape is Base or Optional(Base). So with a default constructor for T and a declared fn taking `xs: Vec`, the default-application loop matched `xs` as `T`, built FoldPlan { target: T, shape: Base }, and the generated wrapper would reconstruct a single T and hand it to a parameter expecting Vec. Not a rejected plan -- a wrong one, in emitted code. All four expand sites had it, both crossing_core callers and both layer reads. Fixed by peeling exactly what expansion can represent. crossing_core becomes constructed_value (plus constructed_value_layers for the two sites that need the flags), composing Optional then Ref and stopping there, so Vec answers Vec and the match correctly fails. TypeRef gains optional_inner, sequence_elem and borrow_target -- one layer each, named -- because "peel exactly what I can represent" is a different question from "peel the boundary shape", and only layers() answers the second. The regression test is the part that was actually missing. The whole suite passed either way and regen-check was byte-identical, because no example declares a Vec parameter whose element is constructible -- so the evidence I reported for this PR could not have caught it, and saying "byte-identical" proved less than it sounded like. Sabotage-checked: restoring layers().core fails it. Ledger unchanged at 135; this moves no classification sites. * Delete Layers: the model already had a layer algebra core::shape::Shape's own module doc says it replaced "three former per-module copies of the same leaf + wrapper layers idea". Layers was a fourth copy, encoded as flags instead of a stack, and exported. What that cost is visible at the main fold site, which did a round trip: let layers = peel(...); // TypeKind -> flags let inner = if iterable { Iterable(Base) } else { Base }; let shape = if optional { Optional((), inner) } else { inner }; UnfoldShape IS Shape, so the plan's shape and the type's shape were the same object computed twice, in two vocabularies. TypeRef::layer_stack returns the stack directly and those three lines become one clone. Two things the move corrected: A borrow is not a layer. Optional and Iterable change arity -- none or one, none or many -- while &T is the same single value held differently. That is ownership, and it belongs on the core where borrow_target reads it. Putting by_ref in a tuple with the other two conflated structure with ownership. wrappers was a wart: three fixed slots that repeat when a layer is absent, so a caller reads duplicates and cannot tell which were real. layer_types walks the actual chain. Its one consumer -- the unrequire of a leaf-delivered value -- reads better for it, and layered_core turned out to be dead and went. Returning a stack rather than flags is also what lets a consumer DECLINE. A caller that can only build Base and Optional(Base) matches those and falls through; it cannot silently consume an Iterable it has no arm for, which is exactly the Vec defect from the previous commit expressed as a type rather than as a comment. Shape becomes public. It was crate-private, so a pub fn returning it leaked -- and since the model now produces it and every plan engine consumes it, it is part of what a generator has to speak. Ledger unchanged at 135; regen byte-identical; 528 tests. * The layer stack stops at a layer that is out of order Review catch, and the code contradicted the contract written directly above it. layer_stack recursed, so Vec> returned Iterable(Optional(Base)) over T while the doc said Iterable(Base) over Option. Not just wrong on paper. returns_type compares the core, so a Vec> return matched a decomposition target T and wire_fixed_returns installed a nested optional fold -- for a type the explicit decomposition path a few hundred lines away refuses outright as "Vec> returns" unsupported. Two paths disagreeing about the same return type, with the silent one winning. The accepted crossing is Option>: at most one optional, then at most one run, in that order. So the peel is bounded rather than recursive, and an out-of-position or repeated layer ends the stack and stays on the core, where it belongs to the element rather than to the boundary. layer_types stops at the same point, or the registration view would un-require a type the shape says is part of the element. Two tests, each sabotage-checked against restoring the recursion: the_layer_stack_stops_at_an_out_of_order_layer pins the model directly -- in-order, out-of-order, repeated, and the borrow that is not a layer at all. a_vec_of_optionals_installs_no_fixed_fold pins the consequence the review named: no fold is installed for the return the sibling path rejects. Ledger unchanged at 135; regen byte-identical; 530 tests. --- prebindgen/src/api/core/expand.rs | 94 ++++-- prebindgen/src/api/core/expand/tests.rs | 45 +++ prebindgen/src/api/core/flat/boundary.ledger | 5 +- .../src/api/core/flat/tests/acceptance.rs | 73 +++++ prebindgen/src/api/core/flat/ty.rs | 106 +++++++ prebindgen/src/api/core/unfold.rs | 292 +++++++++++------- prebindgen/src/api/core/unfold/tests.rs | 43 +++ prebindgen/src/lib.rs | 5 + 8 files changed, 512 insertions(+), 151 deletions(-) diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index 95a661e0..c671e735 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -31,7 +31,7 @@ use quote::quote; use crate::api::core::{ registry::{Registry, TypeKey}, - types_util::{ident, option_inner_type, result_ok_type}, + types_util::ident, }; mod error; @@ -197,11 +197,7 @@ pub fn apply( .ok_or_else(|| ExpandError::UnknownFunction(ed.func.clone()))?; let param_ty = find_param_type(&item_fn, &ed.param) .ok_or_else(|| ExpandError::UnknownParam(ed.func.clone(), ed.param.clone()))?; - let inner = option_inner_type(¶m_ty).unwrap_or(param_ty); - let bare = match &inner { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other.clone(), - }; + let bare = constructed_value(registry.flat(), ¶m_ty); if TypeKey::from_type(&bare) != TypeKey::from_type(declared) { return Err(ExpandError::ParamTypeMismatch { func: ed.func.clone(), @@ -249,11 +245,7 @@ pub fn apply( let receiver_key = method_receivers.get(func); let mut receiver_skipped = false; for (pname, pty) in fn_params(&item_fn) { - let core = option_inner_type(&pty).unwrap_or(pty); - let bare = match &core { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other.clone(), - }; + let bare = constructed_value(registry.flat(), &pty); let bare_key = TypeKey::from_type(&bare); if !receiver_skipped && receiver_key == Some(&bare_key) { receiver_skipped = true; @@ -312,16 +304,9 @@ fn process_expand( let param_ty = find_param_type(&item_fn, &ed.param) .ok_or_else(|| ExpandError::UnknownParam(ed.func.clone(), ed.param.clone()))?; - // Peel `Option<…>` (whole param optional) then a leading `&` (borrow): - // `Option<&T>` → optional + by_ref, `Option` → optional, `&T` → by_ref. - let (optional, inner) = match option_inner_type(¶m_ty) { - Some(i) => (true, i), - None => (false, param_ty.clone()), - }; - let (by_ref, target) = match &inner { - syn::Type::Reference(r) => (true, (*r.elem).clone()), - other => (false, other.clone()), - }; + // The boundary layers: `Option<&T>` → optional + by_ref, `Option` → + // optional, `&T` → by_ref, and `target` is what is left under them. + let (optional, by_ref, target) = constructed_value_layers(registry.flat(), ¶m_ty); let target_key = TypeKey::from_type(&target); let variants = resolve_constructor(exp, registry, &target_key, ed)?; @@ -387,10 +372,11 @@ fn ctor_signature(registry: &Registry, func: &syn::Ident) -> Result (ok, true), - None => (ret, false), + // The model already read this return; `fallible_parts` is that reading, not a + // second look at the spelling. + let (target, fallible) = match f.ret.fallible_parts() { + Some((ok, _)) => (ok.origin.syntax.clone(), true), + None => (f.ret.origin.syntax.clone(), false), }; Ok(CtorSig { params, @@ -669,15 +655,8 @@ fn build_arg( leaves: &mut Vec, visited: &mut HashSet, ) -> Result { - // Peel `Option<…>` then a leading `&` to reach the parameter's core type. - let (popt, core) = match option_inner_type(pty) { - Some(i) => (true, i), - None => (false, pty.clone()), - }; - let (pby_ref, bare) = match &core { - syn::Type::Reference(r) => (true, (*r.elem).clone()), - other => (false, other.clone()), - }; + // The boundary layers down to the parameter's core type. + let (popt, pby_ref, bare) = constructed_value_layers(registry.flat(), pty); let key = TypeKey::from_type(&bare); // A default constructor for the parameter's type ⇒ recursive nested build. let canon = exp @@ -1088,6 +1067,53 @@ fn ctor_call_result(path: &syn::Path, args: &[I], fallible: // Small helpers // ────────────────────────────────────────────────────────────────────── +/// The value a constructor builds: `Option` off, then the borrow, and **nothing +/// else** — read off the model's classification rather than by taking the +/// spelling apart. +/// +/// `Option<&T>`, `&T` and `T` all answer `T`, which is what every caller here +/// wants: they are matching a declared target, and a declaration names the type, +/// not the way a particular parameter happens to wrap it. +/// +/// **`Vec` answers `Vec`, deliberately.** Expansion builds one value — +/// `FoldPlan`'s shape is `Base` or `Optional(Base)`, with no iterable arm — so +/// peeling a `Sequence` here would let a `Vec` parameter match a `T` +/// constructor and emit a wrapper that reconstructs a single `T` and hands it to +/// a parameter expecting the collection. Leaving the `Sequence` on the core is +/// what makes that a non-match instead of a miscompile, and it is the reason this +/// is not [`TypeRef::layers`], which peels all three. +/// +/// A type the grammar cannot express answers itself — the identity, not a +/// fallback classifier. Nothing reaching here can be one: every signature in play +/// was accepted by the frontend before the scan registered it. +fn constructed_value(flat: &crate::api::core::flat::Flat, ty: &syn::Type) -> syn::Type { + let Ok(reading) = flat.classify(ty) else { + return ty.clone(); + }; + let after_opt = reading.optional_inner().unwrap_or(&reading); + after_opt + .borrow_target() + .unwrap_or(after_opt) + .origin + .syntax + .clone() +} + +/// [`constructed_value`], plus which of the two layers were there. +fn constructed_value_layers( + flat: &crate::api::core::flat::Flat, + ty: &syn::Type, +) -> (bool, bool, syn::Type) { + let Ok(reading) = flat.classify(ty) else { + return (false, false, ty.clone()); + }; + let optional = reading.optional_inner().is_some(); + let after_opt = reading.optional_inner().unwrap_or(&reading); + let by_ref = after_opt.borrow_target().is_some(); + let core = after_opt.borrow_target().unwrap_or(after_opt); + (optional, by_ref, core.origin.syntax.clone()) +} + fn find_param_type(item_fn: &syn::ItemFn, param: &syn::Ident) -> Option { for input in &item_fn.sig.inputs { if let syn::FnArg::Typed(pt) = input { diff --git a/prebindgen/src/api/core/expand/tests.rs b/prebindgen/src/api/core/expand/tests.rs index 09a4fec9..2fae7ac9 100644 --- a/prebindgen/src/api/core/expand/tests.rs +++ b/prebindgen/src/api/core/expand/tests.rs @@ -756,3 +756,48 @@ fn invalid_declarations_collected() { "{text}" ); } + +/// A `Vec` parameter is **not** a `T` parameter, even when `T` has a +/// constructor. +/// +/// Expansion builds one value: `FoldPlan`'s shape is `Base` or `Optional(Base)`, +/// with no iterable arm. So the layer peel here stops at the borrow — if it also +/// peeled the `Sequence`, a `Vec` parameter would match a `T` constructor and +/// the wrapper would reconstruct a single `T` and hand it to a parameter +/// expecting the collection. Not a rejected plan: a wrong one, in generated code. +/// +/// Missed once, and by everything: the whole suite passed either way, and +/// regen-check was byte-identical, because no example declares a `Vec` +/// parameter whose element is constructible. That is what this fixture is. +#[test] +fn a_vec_param_does_not_match_its_elements_constructor() { + let mut reg: Registry<()> = reg_with(&[ + "fn z_keyexpr_try_from(s: String) -> Result { todo!() }", + "fn z_keyexpr_join_all(parts: Vec) -> bool { todo!() }", + ]); + let mut exp = Expansions::default(); + // The type-level default: every `ZKeyExpr` parameter may be built from a + // `String`. `parts` is a `Vec`, so it is not one of them. + exp.constructors.push(ConstructorDecl { + target: syn::parse_quote!(ZKeyExpr), + variants: vec![Variant::Ctor(ident("z_keyexpr_try_from"))], + default: true, + }); + + apply( + &mut reg, + &exp, + &[ident("z_keyexpr_join_all")].into_iter().collect(), + &Default::default(), + &Default::default(), + ) + .expect("apply"); + + assert!( + !reg.expansion_plans + .contains_key(&(ident("z_keyexpr_join_all"), ident("parts"))), + "a Vec parameter must not be expanded as one ZKeyExpr; \ + plans: {:?}", + reg.expansion_plans.keys().collect::>() + ); +} diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 6b8bca5e..df15392c 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -44,10 +44,9 @@ # A check that silently under-reports is worse than no check, which is why the # gaps are listed here rather than implied away. -4 api/core/expand.rs 2 api/core/registry/scan.rs 10 api/core/types_util.rs -16 api/core/unfold.rs +1 api/core/unfold.rs 8 api/lang/cbindgen/builder.rs 1 api/lang/cbindgen/convert.rs 5 api/lang/cbindgen/emit.rs @@ -73,4 +72,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 154 +# total: 135 diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index bbe41cff..b10a1aba 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -1697,3 +1697,76 @@ fn an_unsupported_item_still_holds_its_name() { ]) .is_err()); } + +/// The layer stack accepts one shape — `Option>` — and stops at the first +/// layer that is out of that order or repeats. +/// +/// A recursion would happily return `Iterable(Optional(Base))` for +/// `Vec>`, and that is wrong in a way the shape alone does not show: +/// the optional there belongs to the **element**, not to the boundary. The +/// difference is behavioural — `returns_type` compares the core, so an unbounded +/// peel makes a `Vec>` return match a decomposition target `T` and +/// installs a nested optional fold, while the explicit path next to it still +/// refuses `Vec>` as unsupported. One of the two has to be wrong, and +/// it is not the refusal. +/// +/// `layer_types` has to stop at the same place, or the registration view +/// un-requires types the shape says are part of the element. +#[test] +fn the_layer_stack_stops_at_an_out_of_order_layer() { + use crate::api::core::shape::Shape; + + let shape_of = |ty: proc_macro2::TokenStream| { + let reading = lower(ty).expect("lowers"); + let (shape, core) = reading.layer_stack(); + let rendered = match &shape { + Shape::Base => "Base".to_string(), + Shape::Optional(_, i) => match &**i { + Shape::Base => "Optional(Base)".to_string(), + Shape::Iterable(_) => "Optional(Iterable(Base))".to_string(), + Shape::Optional(..) => "Optional(Optional(..))".to_string(), + }, + Shape::Iterable(i) => match &**i { + Shape::Base => "Iterable(Base)".to_string(), + other => format!("Iterable({other:?})"), + }, + }; + ( + rendered, + quote::ToTokens::to_token_stream(&core.origin.syntax).to_string(), + reading.layer_types().len(), + ) + }; + + // In order: both layers are the boundary's. + assert_eq!( + shape_of(quote::quote!(Option>)), + ("Optional(Iterable(Base))".into(), "Sample".into(), 3) + ); + assert_eq!( + shape_of(quote::quote!(Option)), + ("Optional(Base)".into(), "Sample".into(), 2) + ); + assert_eq!( + shape_of(quote::quote!(Vec)), + ("Iterable(Base)".into(), "Sample".into(), 2) + ); + + // Out of order: the optional is the element's, so the stack stops. + assert_eq!( + shape_of(quote::quote!(Vec>)), + ("Iterable(Base)".into(), "Option < Sample >".into(), 2) + ); + + // Repeated: the boundary has one way to say absent. + assert_eq!( + shape_of(quote::quote!(Option>)), + ("Optional(Base)".into(), "Option < Sample >".into(), 2) + ); + + // A borrow is not a layer at all — it is ownership, and stays on the core. + assert_eq!( + shape_of(quote::quote!(Option>)), + ("Optional(Iterable(Base))".into(), "& Sample".into(), 3) + ); +} diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 102022bf..38ac1898 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -42,6 +42,112 @@ pub struct TypeRef { } impl TypeRef { + /// The **arity layers** over this type, and what they wrap. + /// + /// `Option>` is `Optional(Iterable(Base))` over `T`. The stack is the + /// same [`Shape`](crate::core::shape::Shape) the expansion and decomposition plans are built from — so a + /// consumer that needs a plan shape has it, rather than rebuilding one from + /// flags that were derived from this type moments earlier. + /// + /// **A borrow is not a layer.** `Optional` and `Iterable` change arity — none + /// or one, none or many — while `&T` is the same single value held + /// differently. That is ownership, and it stays on the returned core, where + /// [`borrow_target`](Self::borrow_target) reads it. + /// + /// A layer out of position is not a layer: `Vec>` is + /// `Iterable(Base)` over `Option`, because the optional is inside the run. + /// The stack is what wraps the payload, in order, and nothing is reordered to + /// make it fit a shape a caller hoped for. + /// + /// Returning the stack rather than a set of flags is what lets a caller + /// **decline**: a consumer that can only build `Base` and `Optional(Base)` + /// matches those and falls through on anything else, instead of silently + /// consuming a layer it cannot honour. + pub fn layer_stack(&self) -> (crate::api::core::shape::Shape, &TypeRef) { + use crate::api::core::shape::Shape; + // Bounded on purpose, and not a recursion: the accepted crossing is + // `Option>` — at most one optional, then at most one run, in that + // order. Recursing would accept `Vec>` as `Iterable(Optional)`, + // which reads the inner optional as a boundary layer when it is part of + // the element, and `Option>` as two nullable layers when the + // boundary has one way to say absent. + let mut core = self; + let optional = matches!(core.kind, TypeKind::Optional(_)); + if let TypeKind::Optional(inner) = &core.kind { + core = inner; + } + let iterable = matches!(core.kind, TypeKind::Sequence(_)); + if let TypeKind::Sequence(inner) = &core.kind { + core = inner; + } + + let mut shape = Shape::Base; + if iterable { + shape = Shape::iterable(shape); + } + if optional { + shape = Shape::optional((), shape); + } + (shape, core) + } + + /// Every type on the way down through the arity layers, outermost first and + /// ending at the core [`layer_stack`](Self::layer_stack) returns. + /// + /// What a **registration** walks, which is a different question from what + /// crosses: a value delivered layer-by-layer needs each of these un-required, + /// and none of them has a converter of its own. + pub fn layer_types(&self) -> Vec<&TypeRef> { + // Stops exactly where `layer_stack` stops, or the registration view would + // un-require types the shape says are part of the element. + let mut out = vec![self]; + let mut cur = self; + if let TypeKind::Optional(inner) = &cur.kind { + out.push(inner); + cur = inner; + } + if let TypeKind::Sequence(inner) = &cur.kind { + out.push(inner); + } + out + } + + /// What an `Option` wraps, else `None`. + /// + /// One layer, named. [`layer_stack`](Self::layer_stack) reads the whole + /// arity stack; these three read exactly the layer a caller asks for, which + /// is what a consumer wants when it can only *represent* some of them. + pub fn optional_inner(&self) -> Option<&TypeRef> { + match &self.kind { + TypeKind::Optional(inner) => Some(inner), + _ => None, + } + } + + /// The element of a run of values (`Vec`, `[T]`), else `None`. + pub fn sequence_elem(&self) -> Option<&TypeRef> { + match &self.kind { + TypeKind::Sequence(elem) => Some(elem), + _ => None, + } + } + + /// What a borrow points at, else `None`. + pub fn borrow_target(&self) -> Option<&TypeRef> { + match &self.kind { + TypeKind::Ref { inner, .. } => Some(inner), + _ => None, + } + } + + /// The `Ok` and `Err` sides when this is a `Result`, else `None`. + pub fn fallible_parts(&self) -> Option<(&TypeRef, &TypeRef)> { + match &self.kind { + TypeKind::Fallible { ok, err } => Some((ok, err)), + _ => None, + } + } + /// The extent of this type when it is an array, else `None`. pub fn array_extent(&self) -> Option<&ArrayExtent> { match &self.kind { diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index e83d0a44..d9ae01b4 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -27,10 +27,7 @@ use std::collections::HashSet; -use crate::api::core::{ - registry::{Registry, TypeKey}, - types_util::{option_inner_type, result_err_type, vec_inner_type}, -}; +use crate::api::core::registry::{Registry, TypeKey}; mod error; mod plan; @@ -302,7 +299,7 @@ pub fn apply( .map(|f| f.origin.syntax.clone()) .ok_or_else(|| UnfoldError::UnknownFunction(ed.func.clone()))?; let ret = fn_return(&item_fn); - if !returns_type(&ret, &TypeKey::from_type(declared)) { + if !returns_type(registry, &ret, &TypeKey::from_type(declared)) { return Err(UnfoldError::ReturnTypeMismatch { func: ed.func.clone(), declared: TypeKey::from_type(declared).as_str().to_string(), @@ -349,7 +346,7 @@ pub fn apply( }; let ret = fn_return(&item_fn); // Error position: fn returns `Result<_, E>` and `E == d.target`. - if let Some(err_ty) = result_err_type(&ret) { + if let Some(err_ty) = fallible_err(registry, &ret) { if TypeKey::from_type(&err_ty) == dkey && done.insert((func.clone(), DeconTarget::Error)) { @@ -368,7 +365,7 @@ pub fn apply( } // Output position: fn returns `T` / `&T` / `Option` / `Vec` // with `T == d.target` (Result returns keep a handle — factories). - if returns_type(&ret, &dkey) + if returns_type(registry, &ret, &dkey) && !acc.skip_output.contains(func) && done.insert((func.clone(), DeconTarget::Output)) { @@ -418,13 +415,15 @@ pub fn apply( // (cloned) through the reference instead of by move. The plan is // keyed under the ACTUAL arg type (`&T`) — that is what // `callback_input`/`callback_iface_spec` look up. - let (by_ref, core_ty) = match &arg_ty { - syn::Type::Reference(r) => (true, (*r.elem).clone()), - other => (false, other.clone()), - }; - // Only a bare path core type can match a deconstructor target - // (`Option` / `Vec` / tuple args are delivered whole). - if !matches!(&core_ty, syn::Type::Path(_)) { + let (by_ref, core_ty) = peel_borrow(registry, &arg_ty); + // Only a NAMED core can match a deconstructor target: an + // `Option` / `Vec` / tuple arg is delivered whole. The model + // says which, so a wrapper the language sees through — `Box` — + // no longer reads as un-nameable. + if !matches!( + registry.flat().classify(&core_ty).map(|r| r.kind.clone()), + Ok(crate::api::core::flat::TypeKind::Named { .. }) + ) { continue; } let key = TypeKey::from_type(&arg_ty); @@ -617,7 +616,7 @@ fn wire_fixed_returns( continue; }; let ret = fn_return(&item_fn); - if !returns_type(&ret, &vd.key) || registry.unfold_plans.contains_key(func) { + if !returns_type(registry, &ret, &vd.key) || registry.unfold_plans.contains_key(func) { continue; } // Shape over the leaf decomposition: peel an outer `Option`, then a @@ -628,39 +627,25 @@ fn wire_fixed_returns( // null result). `element: None` keeps the decomposed-leaf path. The // element/inner borrow-ness sets `by_ref` (the reach clones either // way). - let (optional, after_opt) = match option_inner_type(&ret) { - Some(inner) => (true, inner), - None => (false, ret.clone()), - }; - let (iterable, core) = match vec_inner_type(&after_opt) { - Some(inner) => (true, inner), - None => (false, after_opt.clone()), - }; - let by_ref = matches!(&core, syn::Type::Reference(_)); - let inner_shape = if iterable { - UnfoldShape::Iterable(Box::new(UnfoldShape::Base)) - } else { - UnfoldShape::Base - }; - let shape = if optional { - UnfoldShape::Optional((), Box::new(inner_shape)) - } else { - inner_shape - }; + let layers = peel(registry, &ret); + let by_ref = layers.by_ref; + // The model's layer stack is the plan's shape — `UnfoldShape` is `Shape` + // — so there is nothing to rebuild here. + let shape = layers.shape.clone(); if no_converter { // The plan delivers the return leaf-by-leaf, so no converter is // needed for the declared return — and for a sum none can exist. // Drop the scan-time registrations of every layer (the boundary-only // pass only reaches the bare type), so the missing converters are not // flagged as unresolved-required. - // All THREE peeled layers, the `Vec` element included. The shape - // fold peels here, so the matching unrequire belongs here; leaving - // the element out made the invariant depend on the adapter's - // `boundary_only_types` covering it — true for JniGenBuilder today, and - // the only reason a `Vec`-only declaration resolves. - registry.unrequire_output(&ret); - registry.unrequire_output(&after_opt); - registry.unrequire_output(&core); + // EVERY layer, the `Vec` element included. The shape fold peels here, + // so the matching unrequire belongs here; leaving the element out made + // the invariant depend on the adapter's `boundary_only_types` covering + // it — true for JniGenBuilder today, and the only reason a + // `Vec`-only declaration resolves. + for layer in &layers.layer_types { + registry.unrequire_output(layer); + } } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { registry.require_output(&leaf.out_ty); @@ -715,16 +700,16 @@ fn wire_fixed_callbacks( // value via `fromParts`); an `impl Fn(&[T])` / `impl Fn([T])` arg // becomes an `Iterable` fixed FOLDER (the trampoline folds each // element's leaves into a foreign list — see the callback emitter). - let (by_ref, after_ref) = match &arg_ty { - syn::Type::Reference(r) => (true, (*r.elem).clone()), - other => (false, other.clone()), - }; - let (shape, matches_key) = match &after_ref { - syn::Type::Slice(s) => ( + let (by_ref, after_ref) = peel_borrow(registry, &arg_ty); + // A run of `T` is an Iterable fold over the element; anything else + // is a Base fold over the value itself. `Sequence` is the one + // question, and it covers `[T]` and `Vec` alike. + let (shape, matches_key) = match sequence_elem(registry, &after_ref) { + Some(elem) => ( UnfoldShape::Iterable(Box::new(UnfoldShape::Base)), - TypeKey::from_type(&s.elem) == vd.key, + TypeKey::from_type(&elem) == vd.key, ), - other => (UnfoldShape::Base, TypeKey::from_type(other) == vd.key), + None => (UnfoldShape::Base, TypeKey::from_type(&after_ref) == vd.key), }; if !matches_key { continue; @@ -792,11 +777,11 @@ pub fn apply_leaf_vec_folds( // already exists (declared deconstructor / value-struct fold). if !registry.unfold_plans.contains_key(func) { let ret = fn_return(&item_fn); - let (optional, after_opt) = match option_inner_type(&ret) { + let (optional, after_opt) = match optional_inner(registry, &ret) { Some(inner) => (true, inner), None => (false, ret.clone()), }; - if let Some(vec_elem) = vec_inner_type(&after_opt) { + if let Some(vec_elem) = sequence_elem(registry, &after_opt) { let bare = peel_ref(&vec_elem); if is_nominated(&bare) { let inner_shape = UnfoldShape::Iterable(Box::new(UnfoldShape::Base)); @@ -814,9 +799,10 @@ pub fn apply_leaf_vec_folds( // JObject-shaped), and de-requiring keeps that `None` from // being flagged as an unresolved-required error. registry.unrequire_output(&ret); - registry - .unfold_plans - .insert(func.clone(), whole_leaf_fold_plan(&vec_elem, shape)); + registry.unfold_plans.insert( + func.clone(), + whole_leaf_fold_plan(registry, &vec_elem, shape), + ); } } } @@ -829,12 +815,11 @@ pub fn apply_leaf_vec_folds( continue; }; for arg_ty in args { - let after_ref = peel_ref(&arg_ty); - let syn::Type::Slice(s) = &after_ref else { + let (_, after_ref) = peel_borrow(registry, &arg_ty); + let Some(elem) = sequence_elem(registry, &after_ref) else { continue; }; - let elem = (*s.elem).clone(); - if !is_nominated(&peel_ref(&elem)) { + if !is_nominated(&peel_borrow(registry, &elem).1) { continue; } let key = TypeKey::from_type(&arg_ty); @@ -842,8 +827,11 @@ pub fn apply_leaf_vec_folds( continue; } registry.require_output(&elem); - let plan = - whole_leaf_fold_plan(&elem, UnfoldShape::Iterable(Box::new(UnfoldShape::Base))); + let plan = whole_leaf_fold_plan( + registry, + &elem, + UnfoldShape::Iterable(Box::new(UnfoldShape::Base)), + ); registry.callback_arg_plans.insert(key, plan); } } @@ -854,11 +842,15 @@ pub fn apply_leaf_vec_folds( /// Build a fixed-builder whole-element fold [`UnfoldPlan`] for a single-leaf /// element `vec_elem` (the `Vec`/slice element as written, keeping any leading /// `&` so `into_iter()`'s yield matches the element's own output converter). -fn whole_leaf_fold_plan(vec_elem: &syn::Type, shape: UnfoldShape) -> UnfoldPlan { +fn whole_leaf_fold_plan( + registry: &Registry, + vec_elem: &syn::Type, + shape: UnfoldShape, +) -> UnfoldPlan { UnfoldPlan { source: vec_elem.clone(), decon: None, - by_ref: matches!(vec_elem, syn::Type::Reference(_)), + by_ref: peel_borrow(registry, vec_elem).0, shape, leaves: vec![], element: Some(vec_elem.clone()), @@ -914,6 +906,104 @@ fn check_records( Ok(()) } +/// The arity layers over `ty`, the types they wrap, and the value underneath. +/// +/// [`TypeRef::layer_stack`](crate::api::core::flat::TypeRef::layer_stack) and +/// [`layer_types`](crate::api::core::flat::TypeRef::layer_types) with the borrows +/// resolved to clones, because these feed plan fields and registry calls that own +/// their types. The classification is the model's; only the copying is local. +/// +/// The stack **is** the plan's shape — `UnfoldShape` is `Shape` — so a caller +/// stores it rather than rebuilding one from flags. +/// +/// A type the grammar cannot express answers `Base` over itself: the identity, +/// not a fallback classifier. Nothing reaching here can be one, since every +/// signature in play was accepted by the frontend before the scan saw it. +struct Layered { + /// The arity layers, outermost first. + shape: UnfoldShape, + /// Every type on the way down, outermost first — what a registration walks. + layer_types: Vec, + /// Past the borrow too: what actually crosses. + core: syn::Type, + /// Whether the core is reached through a borrow. + by_ref: bool, +} + +fn peel(registry: &Registry, ty: &syn::Type) -> Layered { + let Ok(reading) = registry.flat().classify(ty) else { + return Layered { + shape: UnfoldShape::Base, + layer_types: vec![ty.clone()], + core: ty.clone(), + by_ref: false, + }; + }; + let (shape, layered) = reading.layer_stack(); + let borrowed = layered.borrow_target(); + Layered { + shape, + layer_types: reading + .layer_types() + .iter() + .map(|t| t.origin.syntax.clone()) + .collect(), + core: borrowed.unwrap_or(layered).origin.syntax.clone(), + by_ref: borrowed.is_some(), + } +} + +/// What `Option` wraps, if `ty` is one. +fn optional_inner(registry: &Registry, ty: &syn::Type) -> Option { + use crate::api::core::flat::TypeKind; + match registry.flat().classify(ty).ok()?.kind { + TypeKind::Optional(inner) => Some(inner.origin.syntax.clone()), + _ => None, + } +} + +/// The error side of a `Result`, if `ty` is one. +fn fallible_err(registry: &Registry, ty: &syn::Type) -> Option { + Some( + registry + .flat() + .classify(ty) + .ok()? + .fallible_parts()? + .1 + .origin + .syntax + .clone(), + ) +} + +/// The element of a run of values (`Vec`, `[T]`), if `ty` is one. +fn sequence_elem(registry: &Registry, ty: &syn::Type) -> Option { + use crate::api::core::flat::TypeKind; + match registry.flat().classify(ty).ok()?.kind { + TypeKind::Sequence(elem) => Some(elem.origin.syntax.clone()), + _ => None, + } +} + +/// Just the borrow: whether `ty` is one, and what it borrows. +/// +/// The model-driven `peel_ref`, and deliberately **not** [`peel`]: a site that +/// peels only the borrow means it, because the layer underneath is the thing it +/// is about to classify. `Vec` answers `(false, Vec)` here and +/// `(iterable, T)` there, and confusing the two turns "this arg is a collection" +/// into "this arg is a T". +fn peel_borrow(registry: &Registry, ty: &syn::Type) -> (bool, syn::Type) { + use crate::api::core::flat::TypeKind; + match registry.flat().classify(ty) { + Ok(reading) => match &reading.kind { + TypeKind::Ref { inner, .. } => (true, inner.origin.syntax.clone()), + _ => (false, ty.clone()), + }, + Err(_) => (false, ty.clone()), + } +} + /// Strip a single leading `&` (one level) from a type. pub(crate) fn peel_ref(ty: &syn::Type) -> syn::Type { match ty { @@ -934,21 +1024,8 @@ fn fn_return(item_fn: &syn::ItemFn) -> syn::Type { /// `T == key` — the default-output match. `Result<_, _>` is NOT peeled, so a /// fallible factory (`-> Result`) keeps its handle return; the error /// position is matched separately on `E`. -fn returns_type(ret: &syn::Type, key: &TypeKey) -> bool { - // Peel an outer `Option`, then a `Vec` (so `Option>` matches too), - // then a leading `&`. - let mut core = ret.clone(); - if let Some(inner) = option_inner_type(&core) { - core = inner; - } - if let Some(inner) = vec_inner_type(&core) { - core = inner; - } - let bare = match &core { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other.clone(), - }; - TypeKey::from_type(&bare) == *key +fn returns_type(registry: &Registry, ret: &syn::Type, key: &TypeKey) -> bool { + TypeKey::from_type(&peel(registry, ret).core) == *key } /// Build one output/error plan for `ed` and store it in the right registry map. @@ -969,9 +1046,11 @@ fn process_decl( let ret_ty: syn::Type = match ed.target { DeconTarget::Output => fn_return(&item_fn), DeconTarget::Error => { - result_err_type(&fn_return(&item_fn)).ok_or_else(|| UnfoldError::Unsupported { - func: ed.func.clone(), - reason: "convert_error/deconstruct_error on a non-Result return", + fallible_err(registry, &fn_return(&item_fn)).ok_or_else(|| { + UnfoldError::Unsupported { + func: ed.func.clone(), + reason: "convert_error/deconstruct_error on a non-Result return", + } })? } }; @@ -983,7 +1062,7 @@ fn process_decl( // the historical probe order (the `Vec` probe runs on `E` itself), so // an `Option>` error stays whole. let (optional, after_opt) = match ed.target { - DeconTarget::Output => match option_inner_type(&ret_ty) { + DeconTarget::Output => match optional_inner(registry, &ret_ty) { Some(inner) => (true, inner), None => (false, ret_ty.clone()), }, @@ -997,8 +1076,8 @@ fn process_decl( // via its own output converter + projection, fold `(acc, T) -> acc`. // The other shapes (`Option`/scalar) decompose via an accessor // (M1–M3). `Vec>` is not supported. - let plan = if let Some(inner) = vec_inner_type(&after_opt) { - if option_inner_type(&inner).is_some() { + let plan = if let Some(inner) = sequence_elem(registry, &after_opt) { + if optional_inner(registry, &inner).is_some() { return Err(UnfoldError::Unsupported { func: ed.func.clone(), reason: "Vec> returns", @@ -1024,10 +1103,7 @@ fn process_decl( } } // Element type peeled of a leading `&` (accessors take `&Element`). - let (by_ref, element) = match &inner { - syn::Type::Reference(r) => (true, (*r.elem).clone()), - other => (false, other.clone()), - }; + let (by_ref, element) = peel_borrow(registry, &inner); let ekey = TypeKey::from_type(&element); if let Some(d) = find_deconstructor_by_type(acc, &ekey) { // Decomposed: reuse the shared flatten (M3 nesting composes). @@ -1044,7 +1120,7 @@ fn process_decl( // element's own output converter matches `into_iter()`'s yield. // No declaration is involved (`decon: None`) — the element // crosses whole through its own converter. - let by_ref = matches!(&inner, syn::Type::Reference(_)); + let by_ref = peel_borrow(registry, &inner).0; registry.require_output(&inner); UnfoldPlan { source: inner.clone(), @@ -1066,15 +1142,12 @@ fn process_decl( // `Option`); for `Error` it happens here, unchanged. let (optional, core_ty) = match ed.target { DeconTarget::Output => (optional, after_opt.clone()), - DeconTarget::Error => match option_inner_type(&after_opt) { + DeconTarget::Error => match optional_inner(registry, &after_opt) { Some(inner) => (true, inner), None => (false, after_opt.clone()), }, }; - let (by_ref, source) = match &core_ty { - syn::Type::Reference(r) => (true, (*r.elem).clone()), - other => (false, other.clone()), - }; + let (by_ref, source) = peel_borrow(registry, &core_ty); let source_key = TypeKey::from_type(&source); let shape = if optional { UnfoldShape::Optional((), Box::new(UnfoldShape::Base)) @@ -1462,7 +1535,7 @@ fn flatten( for fr in fields { // A field's own `Option` makes everything under it nullable, // exactly as an `Option`-returning accessor step does. - let (opt, core) = match option_inner_type(&fr.ty) { + let (opt, core) = match optional_inner(registry, &fr.ty) { Some(inner) => (true, inner), None => (false, fr.ty.clone()), }; @@ -1573,14 +1646,13 @@ fn flatten( // Default unwrap: if the return type has its own deconstructor, // splice it (recurse); otherwise the return is one leaf. Peel an // `Option` (value may be absent) + leading `&` to reach the child. - let (opt, core) = match option_inner_type(&ret) { - Some(inner) => (true, inner), - None => (false, ret.clone()), - }; - let child_ty = match &core { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other.clone(), - }; + // This site peels an `Option` only — an accessor returning a run + // of values is not spliced — so it asks the model for that one + // layer rather than the whole stack. + let after_opt = optional_inner(registry, &ret); + let opt = after_opt.is_some(); + let core = after_opt.unwrap_or_else(|| ret.clone()); + let (core_by_ref, child_ty) = peel_borrow(registry, &core); let child_key = TypeKey::from_type(&child_ty); // A child already on the nesting chain: for a `#[prebindgen]` // accessor that is an authoring cycle (hard error); a @@ -1601,11 +1673,7 @@ fn flatten( visited.insert(child_key.clone()); let child_records = child_decl.records.clone(); let mut child_path = path_prefix.to_vec(); - child_path.push(PathStep::call( - func.clone(), - opt, - !matches!(core, syn::Type::Reference(_)), - )); + child_path.push(PathStep::call(func.clone(), opt, !core_by_ref)); flatten( acc, registry, @@ -1635,7 +1703,7 @@ fn flatten( // one-identity-per-deconstructor budget with // `.field_self()` — two handle deliveries of one value // make no sense. - let cond_handle = local && opt && matches!(core, syn::Type::Reference(_)); + let cond_handle = local && opt && core_by_ref; if cond_handle { if seen_identity { return Err(UnfoldError::MultipleIdentity { @@ -1650,11 +1718,7 @@ fn flatten( (ret, nullable, false) }; let mut path = path_prefix.to_vec(); - path.push(PathStep::call( - func.clone(), - opt, - !matches!(core, syn::Type::Reference(_)), - )); + path.push(PathStep::call(func.clone(), opt, !core_by_ref)); leaves.push(UnfoldLeaf { name: seg_name(name).join("__"), path, diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index 6f585f5e..758c7495 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -1834,3 +1834,46 @@ fn sum_callback_arg_is_a_fixed_builder_plan() { assert!(matches!(plan.shape, UnfoldShape::Base)); assert_eq!(plan.leaves[0].source, LeafSource::SumTag); } + +/// A `Vec>` return does not match a `T` decomposition. +/// +/// The boundary accepts `Option>` — an optional list — and the layer stack +/// stops at any layer out of that order. So the optional inside a `Vec` belongs +/// to the **element**, the return's core is `Option` rather than +/// `Payload`, and no fixed fold is installed. +/// +/// This is the pairing that matters: the explicit decomposition path a few +/// hundred lines up refuses `Vec>` outright as unsupported, so a +/// silently-installed nested fold here would mean the two paths disagree about +/// the same return type. An unbounded layer peel makes exactly that happen — +/// verified by sabotage, not assumed. +#[test] +fn a_vec_of_optionals_installs_no_fixed_fold() { + let mut reg: Registry<()> = + reg_with(&["fn storage_get_vec(s: &Storage) -> Vec> { todo!() }"]); + let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { + name: name.to_string(), + path: vec![PathStep::field(ident(name), false)], + out_ty: ty, + identity: false, + nullable: false, + source: LeafSource::Field, + group: None, + }; + let vd = ValueDecon { + key: TypeKey::from_type(&syn::parse_quote!(Payload)), + source: syn::parse_quote!(Payload), + leaves: vec![ + leaf("id", syn::parse_quote!(i64)), + leaf("seq", syn::parse_quote!(i32)), + ], + }; + let declared: std::collections::HashSet = + ["storage_get_vec"].iter().map(|s| ident(s)).collect(); + apply_value_structs(&mut reg, vec![vd], &declared).expect("apply_value_structs"); + + assert!( + !reg.unfold_plans.contains_key(&ident("storage_get_vec")), + "a Vec> return must not fold as a Payload decomposition" + ); +} diff --git a/prebindgen/src/lib.rs b/prebindgen/src/lib.rs index 85de3744..aa85c5dc 100644 --- a/prebindgen/src/lib.rs +++ b/prebindgen/src/lib.rs @@ -309,6 +309,11 @@ pub mod core { /// [`flat::Element`]s that make up one flat namespace, and the model itself. /// Not to be confused with [`crate::lang`], the *destination* adapters. pub use crate::api::core::flat; + /// The layer algebra a boundary value is shaped by — a leaf under an ordered + /// stack of `Optional` / `Iterable` layers. Public because the model now + /// *produces* it ([`flat::TypeRef::layer_stack`]) and the plan engines and + /// adapters consume it, so it is part of what a generator has to speak. + pub use crate::api::core::shape; /// [`Flat`] and [`Element`] sit here too, next to [`Registry`]: they are what /// a build script names, and the rest of the model stays in [`mod@flat`] /// where an adapter reaches for it. From 89b612b6941f26f052bf12be3baefb114998dd88 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 14:18:21 +0200 Subject: [PATCH 20/52] Carry the reading; stop re-deriving it from origin (#263) The review is right that L2's ledger drop overstated what changed, and the example given is exact. Six sites did this: let item_fn = flat.function(&f).map(|f| f.origin.syntax.clone())?; let ret = fn_return(&item_fn); // dig the return out of raw syntax returns_type(registry, &ret, &key) // -> classify() -> re-lower it when Function::ret is ALREADY a TypeRef with kind computed at parse time. The model handed the answer over and the code threw it away, reached into origin, and derived it again. That is origin used for reasoning, which is what it is not for, and #211 says so outright: "no component re-derives a source fact by matching captured syntax". The ledger could not see it. It counts variant mentions of watched syn enums per file OUTSIDE core::flat, so moving the match into classify() dropped 19 without changing the data flow at all. The count measures who matches syn variants; it does not measure who reasons from origin, and those two came apart the moment the matching moved into one shared classifier. So the fix is a signature, not a checker. The helpers take &TypeRef: fn peel(ty: &TypeRef) -> Layered // was (&Registry, &syn::Type) fn peel_borrow(ty: &TypeRef) -> (bool, &TypeRef) fn returns_type(ret: &TypeRef, key: &TypeKey) -> bool A caller must already hold a reading, so the round trip stops compiling. The registry parameter disappearing from all of them is the tell that they were never registry operations -- they are questions about a type. The small peelers are gone entirely: optional_inner, sequence_elem and fallible_parts are methods on the reading. What was measured and removed: 6 fn_return(&f.origin.syntax) -> f.ret 5 extract_fn_trait_args(&pt.ty) -> TypeKind::Callback { args }, which carries TypeRefs, so nothing is re-extracted 3 find_param_type / fn_params off a cloned ItemFn -> Param::ty, via one param_reading helper so three sites stop spelling the lookup out fn_return, find_param_type and fn_params are deleted The ItemFn clones were not even load-bearing: they existed to end the immutable borrow of flat() before the &mut registry calls below. Cloning a TypeRef does that too, and is far cheaper than cloning an item with its attributes, body and docs. TypeRef flows through the reasoning; origin.syntax is read only where a value is stored for emission -- a plan field, a registry require/unrequire. That drop happens once, at the boundary, not on every question asked along the way. The plans keep syn::Type: seven jnigen files read them, and moving those is L4's. Registry::reading is the one addition, and it is the layering this settled on: the registry is the authority on what a type means, because it is the thing that stores readings. It answers from the cell the scan filled, and lowers only for a type with no cell -- an adapter's value-form field record naming a combination the scan never registered whole. Flat::classify is its private tool, and both callers are in registry/scan.rs. Guard: classify_has_no_caller_outside_the_registry, sabotage-checked -- a classify call added to unfold fails it by name. It pins the one thing a signature cannot say: do not add a second way in. Also adds TypeRef::key(), so a caller needing a TypeKey does not reach into origin for it. Identity from the canonical spelling is legitimate (#113); it should be the model's answer rather than each caller's. Reported: regen-check byte-identical on every committed artifact. Ledger unchanged at 135 -- and that is the point: it cannot see this class, so the claim here is not a count. It is that the round trip no longer compiles. Explained: same classification, read instead of re-derived. Asserted: nothing outside the registry classifies a type, and nothing that holds an element re-derives what the element already carries. --- prebindgen/src/api/core/expand.rs | 110 +++--- prebindgen/src/api/core/flat/mod.rs | 8 + prebindgen/src/api/core/flat/ty.rs | 10 + prebindgen/src/api/core/registry/scan.rs | 27 ++ prebindgen/src/api/core/registry/tests.rs | 62 ++++ prebindgen/src/api/core/unfold.rs | 402 +++++++++------------- 6 files changed, 317 insertions(+), 302 deletions(-) diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index c671e735..a5c9a7a3 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -190,14 +190,8 @@ pub fn apply( // (`Option`/`&`) type must equal the decl's declared type — the // typo guard for both coordinates of `.expand_param(name, decl)`. if let Some(declared) = &ed.declared_target { - let item_fn = registry - .flat() - .function(&ed.func) - .map(|f| f.origin.syntax.clone()) - .ok_or_else(|| ExpandError::UnknownFunction(ed.func.clone()))?; - let param_ty = find_param_type(&item_fn, &ed.param) - .ok_or_else(|| ExpandError::UnknownParam(ed.func.clone(), ed.param.clone()))?; - let bare = constructed_value(registry.flat(), ¶m_ty); + let param_ty = param_reading(registry, &ed.func, &ed.param)?; + let bare = constructed_value(¶m_ty); if TypeKey::from_type(&bare) != TypeKey::from_type(declared) { return Err(ExpandError::ParamTypeMismatch { func: ed.func.clone(), @@ -233,19 +227,15 @@ pub fn apply( if accessor_fns.contains(func) { continue; } - let Some(item_fn) = registry - .flat() - .function(&func) - .map(|f| f.origin.syntax.clone()) - else { + let Some(params) = registry.flat().function(&func).map(|f| f.params.clone()) else { continue; }; // A method's receiver (first param of its class type) binds to `this` // and is never input-flattened; skip exactly that one param. let receiver_key = method_receivers.get(func); let mut receiver_skipped = false; - for (pname, pty) in fn_params(&item_fn) { - let bare = constructed_value(registry.flat(), &pty); + for (pname, pty) in params.iter().map(|p| (p.name.clone(), p.ty.clone())) { + let bare = constructed_value(&pty); let bare_key = TypeKey::from_type(&bare); if !receiver_skipped && receiver_key == Some(&bare_key) { receiver_skipped = true; @@ -274,19 +264,26 @@ pub fn apply( } /// `(name, type)` of each typed parameter. -fn fn_params(item_fn: &syn::ItemFn) -> Vec<(syn::Ident, syn::Type)> { - item_fn - .sig - .inputs +/// The **reading** of a declared function's parameter. +/// +/// `Param::ty` is a `TypeRef` computed at parse time. Reaching into the item's +/// `origin.syntax` and digging the parameter out of `sig.inputs` — what these +/// three sites used to do — re-derives a fact the model was already handing over, +/// which is `origin` used for reasoning rather than for emission. +fn param_reading( + registry: &Registry, + func: &syn::Ident, + param: &syn::Ident, +) -> Result { + registry + .flat() + .function(&func) + .ok_or_else(|| ExpandError::UnknownFunction(func.clone()))? + .params .iter() - .filter_map(|input| match input { - syn::FnArg::Typed(pt) => match &*pt.pat { - syn::Pat::Ident(pi) => Some((pi.ident.clone(), (*pt.ty).clone())), - _ => None, - }, - _ => None, - }) - .collect() + .find(|p| &p.name == param) + .map(|p| p.ty.clone()) + .ok_or_else(|| ExpandError::UnknownParam(func.clone(), param.clone())) } /// Build + store the fold plan for one `.construct` declaration. @@ -295,18 +292,11 @@ fn process_expand( exp: &Expansions, ed: &ExpandDecl, ) -> Result<(), ExpandError> { - let item_fn = registry - .flat() - .function(&ed.func) - .map(|f| f.origin.syntax.clone()) - .ok_or_else(|| ExpandError::UnknownFunction(ed.func.clone()))?; - - let param_ty = find_param_type(&item_fn, &ed.param) - .ok_or_else(|| ExpandError::UnknownParam(ed.func.clone(), ed.param.clone()))?; + let param_ty = param_reading(registry, &ed.func, &ed.param)?; // The boundary layers: `Option<&T>` → optional + by_ref, `Option` → // optional, `&T` → by_ref, and `target` is what is left under them. - let (optional, by_ref, target) = constructed_value_layers(registry.flat(), ¶m_ty); + let (optional, by_ref, target) = constructed_value_layers(¶m_ty); let target_key = TypeKey::from_type(&target); let variants = resolve_constructor(exp, registry, &target_key, ed)?; @@ -367,10 +357,10 @@ fn ctor_signature(registry: &Registry, func: &syn::Ident) -> Result = f + let params: Vec<(syn::Ident, crate::api::core::flat::TypeRef)> = f .params .iter() - .map(|p| (p.name.clone(), p.ty.origin.syntax.clone())) + .map(|p| (p.name.clone(), p.ty.clone())) .collect(); // The model already read this return; `fallible_parts` is that reading, not a // second look at the spelling. @@ -386,7 +376,9 @@ fn ctor_signature(registry: &Registry, func: &syn::Ident) -> Result, + /// Readings, not spellings: they come off `Function::params`, and a consumer + /// that needs the spelling takes it at the point it stores one. + params: Vec<(syn::Ident, crate::api::core::flat::TypeRef)>, target: syn::Type, fallible: bool, } @@ -452,7 +444,7 @@ fn build_plan( let (_pn, pty) = &sig.params[0]; leaves.push(FoldLeaf { name: param.clone(), - ty: opt(pty), + ty: opt(&pty.origin.syntax), }); return Ok(FoldPlan { target: target.clone(), @@ -649,14 +641,14 @@ fn build_arg( exp: &Expansions, registry: &Registry, ed: &ExpandDecl, - pty: &syn::Type, + pty: &crate::api::core::flat::TypeRef, name: syn::Ident, dispatched: bool, leaves: &mut Vec, visited: &mut HashSet, ) -> Result { // The boundary layers down to the parameter's core type. - let (popt, pby_ref, bare) = constructed_value_layers(registry.flat(), pty); + let (popt, pby_ref, bare) = constructed_value_layers(pty); let key = TypeKey::from_type(&bare); // A default constructor for the parameter's type ⇒ recursive nested build. let canon = exp @@ -711,9 +703,9 @@ fn build_arg( leaves.push(FoldLeaf { name, ty: if dispatched && !passthrough { - opt(pty) + opt(&pty.origin.syntax) } else { - pty.clone() + pty.origin.syntax.clone() }, }); Ok(FoldArg::Leaf(idx, passthrough)) @@ -1086,11 +1078,8 @@ fn ctor_call_result(path: &syn::Path, args: &[I], fallible: /// A type the grammar cannot express answers itself — the identity, not a /// fallback classifier. Nothing reaching here can be one: every signature in play /// was accepted by the frontend before the scan registered it. -fn constructed_value(flat: &crate::api::core::flat::Flat, ty: &syn::Type) -> syn::Type { - let Ok(reading) = flat.classify(ty) else { - return ty.clone(); - }; - let after_opt = reading.optional_inner().unwrap_or(&reading); +fn constructed_value(reading: &crate::api::core::flat::TypeRef) -> syn::Type { + let after_opt = reading.optional_inner().unwrap_or(reading); after_opt .borrow_target() .unwrap_or(after_opt) @@ -1100,33 +1089,14 @@ fn constructed_value(flat: &crate::api::core::flat::Flat, ty: &syn::Type) -> syn } /// [`constructed_value`], plus which of the two layers were there. -fn constructed_value_layers( - flat: &crate::api::core::flat::Flat, - ty: &syn::Type, -) -> (bool, bool, syn::Type) { - let Ok(reading) = flat.classify(ty) else { - return (false, false, ty.clone()); - }; +fn constructed_value_layers(reading: &crate::api::core::flat::TypeRef) -> (bool, bool, syn::Type) { let optional = reading.optional_inner().is_some(); - let after_opt = reading.optional_inner().unwrap_or(&reading); + let after_opt = reading.optional_inner().unwrap_or(reading); let by_ref = after_opt.borrow_target().is_some(); let core = after_opt.borrow_target().unwrap_or(after_opt); (optional, by_ref, core.origin.syntax.clone()) } -fn find_param_type(item_fn: &syn::ItemFn, param: &syn::Ident) -> Option { - for input in &item_fn.sig.inputs { - if let syn::FnArg::Typed(pt) = input { - if let syn::Pat::Ident(pi) = &*pt.pat { - if &pi.ident == param { - return Some((*pt.ty).clone()); - } - } - } - } - None -} - fn opt(ty: &syn::Type) -> syn::Type { syn::parse_quote!(Option<#ty>) } diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index bf3c4950..fb930bf8 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -667,6 +667,14 @@ impl Flat { /// item; it is an intermediate in some binding's crossing graph, and it belongs /// in the table that tracks crossings. /// + /// **The scan's entry point, and nowhere else's.** A caller holding an element + /// already has the reading — `Function::ret`, `Param::ty`, `Field::ty` are + /// `TypeRef`s computed at parse time — and re-deriving one from + /// `origin.syntax` is reasoning from the spelling, which is what `origin` is + /// not for. This exists for the one case with no element behind it: a type a + /// build script declared, or one expansion composed. `ensure_entry` is its + /// only caller, and `classify_has_exactly_one_caller` keeps it that way. + /// /// Whoever asks is expected to keep the answer. The registry does: a reading is /// taken once when a type-table cell is born, and lives in that cell. /// diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 38ac1898..080c8c39 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -140,6 +140,16 @@ impl TypeRef { } } + /// This type's identity as a table key. + /// + /// The canonical spelling is what a key *is* (#113), and reading it is + /// legitimate — but it should be the model's answer rather than every caller + /// reaching into [`origin`](Self::origin) for it, since a caller that reaches + /// into `origin` to *reason* is the thing this model exists to stop. + pub fn key(&self) -> crate::api::core::registry::TypeKey { + crate::api::core::registry::TypeKey::from_type(&self.origin.syntax) + } + /// The `Ok` and `Err` sides when this is a `Result`, else `None`. pub fn fallible_parts(&self) -> Option<(&TypeRef, &TypeRef)> { match &self.kind { diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index 6024cbda..f7804c33 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -417,6 +417,33 @@ impl Registry { .entry = entry; } + /// The reading for `ty` — stored if the scan took one, lowered if it did not. + /// + /// **The registry is the authority on what a type means**, because it is the + /// thing that stores readings: `ensure_entry` asks the grammar once when a cell + /// is born, and this hands that answer back. `Flat::classify` is its private + /// tool, and the two calls in this module are its only callers. + /// + /// The fallback is not the round trip this design forbids. That one is a + /// consumer holding an **element** — whose `ret` / `ty` is already a `TypeRef` + /// — reaching into `origin.syntax` and re-deriving what it was handed; the + /// signatures in `unfold` and `expand` now make it impossible. This is a type + /// the *binding* composed, with no element behind it and no cell yet: a value + /// form's field record naming a combination the scan never registered whole. + /// There is nothing to look up, so the grammar is asked — once, here, rather + /// than by each consumer. + pub(crate) fn reading(&self, ty: &syn::Type) -> Option { + let key = TypeKey::from_type(ty); + if let Some(cell) = self + .input_types + .get(&key) + .or_else(|| self.output_types.get(&key)) + { + return Some((*cell.subject).clone()); + } + self.flat.classify(ty).ok() + } + /// Register `ty` (and its nested positions) as a required **input** so /// the resolver produces a converter for it. Used by /// [`crate::api::core::expand`] to pull in the leaf types a fold needs. diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 96d27e11..bb1581ff 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -1654,3 +1654,65 @@ fn a_built_registry_exposes_no_mutation() { "a built `Registry` must be read-only; found public mutation: {offenders:#?}" ); } + +/// `Flat::classify` has no caller outside the registry. +/// +/// The registry is the authority on what a type means, because it is the thing +/// that **stores** readings: `ensure_entry` asks the grammar once when a cell is +/// born, and `Registry::reading` hands that answer back. `classify` is its private +/// tool. +/// +/// Everything else is enforced by signatures rather than by this test — `peel`, +/// `peel_borrow` and their siblings take a `&TypeRef`, so a consumer cannot hand +/// them a spelling it re-derived. What a signature cannot say is "and do not add a +/// second way in", which is what this pins. +/// +/// The failure it exists for is specific and was live: a consumer holding an +/// element — whose `ret` / `ty` is already a `TypeRef` — reaching into +/// `origin.syntax`, digging the type back out, and re-classifying it. The boundary +/// ledger cannot see that at all: the syn matching happens inside `core::flat`, +/// which the ledger excludes by design, so the count falls while the round trip +/// stays. `origin` is for reconstructing Rust, not for reasoning about it. +#[test] +fn classify_has_no_caller_outside_the_registry() { + let mut offenders: Vec = Vec::new(); + + let root = concat!(env!("CARGO_MANIFEST_DIR"), "/src/api"); + let mut dirs = vec![std::path::PathBuf::from(root)]; + while let Some(dir) = dirs.pop() { + for entry in std::fs::read_dir(&dir).expect("api dir") { + let path = entry.expect("dir entry").path(); + if path.is_dir() { + dirs.push(path); + continue; + } + if path.extension().is_none_or(|e| e != "rs") { + continue; + } + let rel = path + .strip_prefix(root) + .expect("under api") + .to_string_lossy() + .to_string(); + // The registry owns readings; `flat` is where the grammar lives and + // may name its own function. Tests may exercise it directly. + if rel.starts_with("core/registry/") + || rel.starts_with("core/flat/") + || rel.contains("tests") + { + continue; + } + let src = std::fs::read_to_string(&path).expect("read source"); + let bare: String = src.chars().filter(|c| !c.is_whitespace()).collect(); + if bare.contains(".classify(") { + offenders.push(rel); + } + } + } + + assert!( + offenders.is_empty(), + "`Flat::classify` is the registry's; a consumer holding an element must read \ + the element, not re-derive it from `origin.syntax`. Found in: {offenders:#?}" + ); +} diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index d9ae01b4..dd291607 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -293,17 +293,19 @@ pub fn apply( // fn's peeled (`Option`/`Vec`/`&`) return type — the typo guard for // `.expand_return(expand_return!(T)…)`. if let Some(declared) = &ed.declared_source { - let item_fn = registry + let ret = registry .flat() .function(&ed.func) - .map(|f| f.origin.syntax.clone()) + .map(|f| f.ret.clone()) .ok_or_else(|| UnfoldError::UnknownFunction(ed.func.clone()))?; - let ret = fn_return(&item_fn); - if !returns_type(registry, &ret, &TypeKey::from_type(declared)) { + if !returns_type(&ret, &TypeKey::from_type(declared)) { return Err(UnfoldError::ReturnTypeMismatch { func: ed.func.clone(), declared: TypeKey::from_type(declared).as_str().to_string(), - actual: quote::quote!(#ret).to_string(), + actual: { + let s = &ret.origin.syntax; + quote::quote!(#s).to_string() + }, }); } } @@ -337,19 +339,12 @@ pub fn apply( if accessor_fns.contains(func) { continue; } - let Some(item_fn) = registry - .flat() - .function(&func) - .map(|f| f.origin.syntax.clone()) - else { + let Some(ret) = registry.flat().function(&func).map(|f| f.ret.clone()) else { continue; }; - let ret = fn_return(&item_fn); // Error position: fn returns `Result<_, E>` and `E == d.target`. - if let Some(err_ty) = fallible_err(registry, &ret) { - if TypeKey::from_type(&err_ty) == dkey - && done.insert((func.clone(), DeconTarget::Error)) - { + if let Some(err_ty) = ret.fallible_parts().map(|(_, e)| e) { + if err_ty.key() == dkey && done.insert((func.clone(), DeconTarget::Error)) { process_decl( registry, acc, @@ -365,7 +360,7 @@ pub fn apply( } // Output position: fn returns `T` / `&T` / `Option` / `Vec` // with `T == d.target` (Result returns keep a handle — factories). - if returns_type(registry, &ret, &dkey) + if returns_type(&ret, &dkey) && !acc.skip_output.contains(func) && done.insert((func.clone(), DeconTarget::Output)) { @@ -394,18 +389,14 @@ pub fn apply( // (there is no return-value lane in a callback invocation). A type without // a default deconstructor gets no plan and is delivered whole. for func in declared_fns { - let Some(item_fn) = registry - .flat() - .function(&func) - .map(|f| f.origin.syntax.clone()) - else { + let Some(params) = registry.flat().function(&func).map(|f| f.params.clone()) else { continue; }; - for input in &item_fn.sig.inputs { - let syn::FnArg::Typed(pt) = input else { - continue; - }; - let Some(args) = crate::api::core::registry::extract_fn_trait_args(&pt.ty) else { + for param in ¶ms { + // The callback's argument types, read off the parameter's + // classification. `TypeKind::Callback` carries them as `TypeRef`s, so + // there is nothing to re-extract from the signature's syntax. + let crate::api::core::flat::TypeKind::Callback { args } = ¶m.ty.kind else { continue; }; for arg_ty in args { @@ -415,22 +406,19 @@ pub fn apply( // (cloned) through the reference instead of by move. The plan is // keyed under the ACTUAL arg type (`&T`) — that is what // `callback_input`/`callback_iface_spec` look up. - let (by_ref, core_ty) = peel_borrow(registry, &arg_ty); + let (by_ref, core_ty) = peel_borrow(arg_ty); // Only a NAMED core can match a deconstructor target: an // `Option` / `Vec` / tuple arg is delivered whole. The model // says which, so a wrapper the language sees through — `Box` — // no longer reads as un-nameable. - if !matches!( - registry.flat().classify(&core_ty).map(|r| r.kind.clone()), - Ok(crate::api::core::flat::TypeKind::Named { .. }) - ) { + if !matches!(core_ty.kind, crate::api::core::flat::TypeKind::Named { .. }) { continue; } - let key = TypeKey::from_type(&arg_ty); + let key = arg_ty.key(); if registry.callback_arg_plans.contains_key(&key) { continue; } - let core_key = TypeKey::from_type(&core_ty); + let core_key = core_ty.key(); let Some(d) = acc .deconstructors .iter() @@ -447,13 +435,13 @@ pub fn apply( }; let decon = decl_id(&core_key, d); let records = d.records.clone(); - register_decon_spec(registry, acc, &decon, &records, &core_ty)?; + register_decon_spec(registry, acc, &decon, &records, core_ty)?; let plan = build_plan( acc, registry, &ed, by_ref, - &core_ty, + core_ty, UnfoldShape::Base, &records, decon, @@ -608,15 +596,10 @@ fn wire_fixed_returns( no_converter: bool, ) { for func in declared_fns { - let Some(item_fn) = registry - .flat() - .function(&func) - .map(|f| f.origin.syntax.clone()) - else { + let Some(ret) = registry.flat().function(&func).map(|f| f.ret.clone()) else { continue; }; - let ret = fn_return(&item_fn); - if !returns_type(registry, &ret, &vd.key) || registry.unfold_plans.contains_key(func) { + if !returns_type(&ret, &vd.key) || registry.unfold_plans.contains_key(func) { continue; } // Shape over the leaf decomposition: peel an outer `Option`, then a @@ -627,7 +610,7 @@ fn wire_fixed_returns( // null result). `element: None` keeps the decomposed-leaf path. The // element/inner borrow-ness sets `by_ref` (the reach clones either // way). - let layers = peel(registry, &ret); + let layers = peel(&ret); let by_ref = layers.by_ref; // The model's layer stack is the plan's shape — `UnfoldShape` is `Shape` // — so there is nothing to rebuild here. @@ -679,18 +662,14 @@ fn wire_fixed_callbacks( declared_fns: &std::collections::HashSet, ) -> Result<(), UnfoldError> { for func in declared_fns { - let Some(item_fn) = registry - .flat() - .function(&func) - .map(|f| f.origin.syntax.clone()) - else { + let Some(params) = registry.flat().function(&func).map(|f| f.params.clone()) else { continue; }; - for input in &item_fn.sig.inputs { - let syn::FnArg::Typed(pt) = input else { - continue; - }; - let Some(args) = crate::api::core::registry::extract_fn_trait_args(&pt.ty) else { + for param in ¶ms { + // The callback's argument types, read off the parameter's + // classification. `TypeKind::Callback` carries them as `TypeRef`s, so + // there is nothing to re-extract from the signature's syntax. + let crate::api::core::flat::TypeKind::Callback { args } = ¶m.ty.kind else { continue; }; for arg_ty in args { @@ -700,21 +679,21 @@ fn wire_fixed_callbacks( // value via `fromParts`); an `impl Fn(&[T])` / `impl Fn([T])` arg // becomes an `Iterable` fixed FOLDER (the trampoline folds each // element's leaves into a foreign list — see the callback emitter). - let (by_ref, after_ref) = peel_borrow(registry, &arg_ty); + let (by_ref, after_ref) = peel_borrow(arg_ty); // A run of `T` is an Iterable fold over the element; anything else // is a Base fold over the value itself. `Sequence` is the one // question, and it covers `[T]` and `Vec` alike. - let (shape, matches_key) = match sequence_elem(registry, &after_ref) { + let (shape, matches_key) = match after_ref.sequence_elem() { Some(elem) => ( UnfoldShape::Iterable(Box::new(UnfoldShape::Base)), - TypeKey::from_type(&elem) == vd.key, + elem.key() == vd.key, ), - None => (UnfoldShape::Base, TypeKey::from_type(&after_ref) == vd.key), + None => (UnfoldShape::Base, after_ref.key() == vd.key), }; if !matches_key { continue; } - let key = TypeKey::from_type(&arg_ty); + let key = arg_ty.key(); if registry.callback_arg_plans.contains_key(&key) { continue; } @@ -764,33 +743,31 @@ pub fn apply_leaf_vec_folds( } let elem_keys: Vec = elements.iter().map(TypeKey::from_type).collect(); // Is the leading-`&`-peeled `bare` one of the nominated single-leaf elements? - let is_nominated = |bare: &syn::Type| elem_keys.contains(&TypeKey::from_type(bare)); + let is_nominated = |bare: &crate::api::core::flat::TypeRef| elem_keys.contains(&bare.key()); for func in declared_fns { - let Some(item_fn) = registry - .flat() - .function(&func) - .map(|f| f.origin.syntax.clone()) - else { + let Some(params) = registry.flat().function(&func).map(|f| f.params.clone()) else { continue; }; // Output position: `Vec` / `Option>` return. Skip if a plan // already exists (declared deconstructor / value-struct fold). if !registry.unfold_plans.contains_key(func) { - let ret = fn_return(&item_fn); - let (optional, after_opt) = match optional_inner(registry, &ret) { + let Some(ret) = registry.flat().function(&func).map(|f| f.ret.clone()) else { + continue; + }; + let (optional, after_opt) = match ret.optional_inner() { Some(inner) => (true, inner), - None => (false, ret.clone()), + None => (false, &ret), }; - if let Some(vec_elem) = sequence_elem(registry, &after_opt) { - let bare = peel_ref(&vec_elem); - if is_nominated(&bare) { + if let Some(vec_elem) = after_opt.sequence_elem() { + let bare = peel_borrow(vec_elem).1; + if is_nominated(bare) { let inner_shape = UnfoldShape::Iterable(Box::new(UnfoldShape::Base)); let shape = if optional { UnfoldShape::Optional((), Box::new(inner_shape)) } else { inner_shape }; - registry.require_output(&vec_elem); + registry.require_output(&vec_elem.origin.syntax); // The fold delivers the return element-by-element, so the // whole `Vec` / `Option>` converter is not needed. // De-require it: for String / scalar elements it still @@ -798,40 +775,36 @@ pub fn apply_leaf_vec_folds( // opaque-handle element it cannot resolve (`jlong` wire isn't // JObject-shaped), and de-requiring keeps that `None` from // being flagged as an unresolved-required error. - registry.unrequire_output(&ret); - registry.unfold_plans.insert( - func.clone(), - whole_leaf_fold_plan(registry, &vec_elem, shape), - ); + registry.unrequire_output(&ret.origin.syntax); + registry + .unfold_plans + .insert(func.clone(), whole_leaf_fold_plan(vec_elem, shape)); } } } // Callback-arg position: `impl Fn(&[T])` / `impl Fn([T])`. - for input in &item_fn.sig.inputs { - let syn::FnArg::Typed(pt) = input else { - continue; - }; - let Some(args) = crate::api::core::registry::extract_fn_trait_args(&pt.ty) else { + for param in ¶ms { + // The callback's argument types, read off the parameter's + // classification. `TypeKind::Callback` carries them as `TypeRef`s, so + // there is nothing to re-extract from the signature's syntax. + let crate::api::core::flat::TypeKind::Callback { args } = ¶m.ty.kind else { continue; }; for arg_ty in args { - let (_, after_ref) = peel_borrow(registry, &arg_ty); - let Some(elem) = sequence_elem(registry, &after_ref) else { + let (_, after_ref) = peel_borrow(arg_ty); + let Some(elem) = after_ref.sequence_elem() else { continue; }; - if !is_nominated(&peel_borrow(registry, &elem).1) { + if !is_nominated(peel_borrow(elem).1) { continue; } - let key = TypeKey::from_type(&arg_ty); + let key = arg_ty.key(); if registry.callback_arg_plans.contains_key(&key) { continue; } - registry.require_output(&elem); - let plan = whole_leaf_fold_plan( - registry, - &elem, - UnfoldShape::Iterable(Box::new(UnfoldShape::Base)), - ); + registry.require_output(&elem.origin.syntax); + let plan = + whole_leaf_fold_plan(elem, UnfoldShape::Iterable(Box::new(UnfoldShape::Base))); registry.callback_arg_plans.insert(key, plan); } } @@ -842,18 +815,17 @@ pub fn apply_leaf_vec_folds( /// Build a fixed-builder whole-element fold [`UnfoldPlan`] for a single-leaf /// element `vec_elem` (the `Vec`/slice element as written, keeping any leading /// `&` so `into_iter()`'s yield matches the element's own output converter). -fn whole_leaf_fold_plan( - registry: &Registry, - vec_elem: &syn::Type, +fn whole_leaf_fold_plan( + vec_elem: &crate::api::core::flat::TypeRef, shape: UnfoldShape, ) -> UnfoldPlan { UnfoldPlan { - source: vec_elem.clone(), + source: vec_elem.origin.syntax.clone(), decon: None, - by_ref: peel_borrow(registry, vec_elem).0, + by_ref: peel_borrow(vec_elem).0, shape, leaves: vec![], - element: Some(vec_elem.clone()), + element: Some(vec_elem.origin.syntax.clone()), delivery: Delivery::Callback, convert_out_ty: None, fixed_builder: true, @@ -908,17 +880,15 @@ fn check_records( /// The arity layers over `ty`, the types they wrap, and the value underneath. /// +/// A thin owned view over /// [`TypeRef::layer_stack`](crate::api::core::flat::TypeRef::layer_stack) and -/// [`layer_types`](crate::api::core::flat::TypeRef::layer_types) with the borrows -/// resolved to clones, because these feed plan fields and registry calls that own -/// their types. The classification is the model's; only the copying is local. +/// [`layer_types`](crate::api::core::flat::TypeRef::layer_types): the borrows are +/// resolved to clones because these feed plan fields and registry calls that own +/// their types. The classification is the model's; only the copying is local, and +/// it happens **once**, where a value is stored — not on every question asked. /// /// The stack **is** the plan's shape — `UnfoldShape` is `Shape` — so a caller /// stores it rather than rebuilding one from flags. -/// -/// A type the grammar cannot express answers `Base` over itself: the identity, -/// not a fallback classifier. Nothing reaching here can be one, since every -/// signature in play was accepted by the frontend before the scan saw it. struct Layered { /// The arity layers, outermost first. shape: UnfoldShape, @@ -930,20 +900,19 @@ struct Layered { by_ref: bool, } -fn peel(registry: &Registry, ty: &syn::Type) -> Layered { - let Ok(reading) = registry.flat().classify(ty) else { - return Layered { - shape: UnfoldShape::Base, - layer_types: vec![ty.clone()], - core: ty.clone(), - by_ref: false, - }; - }; - let (shape, layered) = reading.layer_stack(); +/// The layers of a type the model has **already read**. +/// +/// Takes a `&TypeRef`, not a `&syn::Type`, and that is the whole point: a caller +/// must hold a reading, and the ways to hold one are to take it off an element or +/// to be the scan admitting a type with no element. Re-deriving a reading from +/// `origin.syntax` — the round trip this signature makes impossible — is reasoning +/// from the spelling, which is what `origin` is not for. +fn peel(ty: &crate::api::core::flat::TypeRef) -> Layered { + let (shape, layered) = ty.layer_stack(); let borrowed = layered.borrow_target(); Layered { shape, - layer_types: reading + layer_types: ty .layer_types() .iter() .map(|t| t.origin.syntax.clone()) @@ -953,54 +922,16 @@ fn peel(registry: &Registry, ty: &syn::Type) -> Layered { } } -/// What `Option` wraps, if `ty` is one. -fn optional_inner(registry: &Registry, ty: &syn::Type) -> Option { - use crate::api::core::flat::TypeKind; - match registry.flat().classify(ty).ok()?.kind { - TypeKind::Optional(inner) => Some(inner.origin.syntax.clone()), - _ => None, - } -} - -/// The error side of a `Result`, if `ty` is one. -fn fallible_err(registry: &Registry, ty: &syn::Type) -> Option { - Some( - registry - .flat() - .classify(ty) - .ok()? - .fallible_parts()? - .1 - .origin - .syntax - .clone(), - ) -} - -/// The element of a run of values (`Vec`, `[T]`), if `ty` is one. -fn sequence_elem(registry: &Registry, ty: &syn::Type) -> Option { - use crate::api::core::flat::TypeKind; - match registry.flat().classify(ty).ok()?.kind { - TypeKind::Sequence(elem) => Some(elem.origin.syntax.clone()), - _ => None, - } -} - /// Just the borrow: whether `ty` is one, and what it borrows. /// -/// The model-driven `peel_ref`, and deliberately **not** [`peel`]: a site that -/// peels only the borrow means it, because the layer underneath is the thing it -/// is about to classify. `Vec` answers `(false, Vec)` here and -/// `(iterable, T)` there, and confusing the two turns "this arg is a collection" -/// into "this arg is a T". -fn peel_borrow(registry: &Registry, ty: &syn::Type) -> (bool, syn::Type) { - use crate::api::core::flat::TypeKind; - match registry.flat().classify(ty) { - Ok(reading) => match &reading.kind { - TypeKind::Ref { inner, .. } => (true, inner.origin.syntax.clone()), - _ => (false, ty.clone()), - }, - Err(_) => (false, ty.clone()), +/// Deliberately **not** [`peel`]: a site that peels only the borrow means it, +/// because the layer underneath is the thing it is about to classify. `Vec` +/// answers `(false, Vec)` here and `(iterable, T)` there, and confusing the two +/// turns "this arg is a collection" into "this arg is a T". +fn peel_borrow(ty: &crate::api::core::flat::TypeRef) -> (bool, &crate::api::core::flat::TypeRef) { + match ty.borrow_target() { + Some(inner) => (true, inner), + None => (false, ty), } } @@ -1012,20 +943,12 @@ pub(crate) fn peel_ref(ty: &syn::Type) -> syn::Type { } } -/// The function's return type (or `()` for a unit return). -fn fn_return(item_fn: &syn::ItemFn) -> syn::Type { - match &item_fn.sig.output { - syn::ReturnType::Default => syn::parse_quote!(()), - syn::ReturnType::Type(_, t) => (**t).clone(), - } -} - /// True when `ret` is `T` / `&T` / `Option` / `Vec` with /// `T == key` — the default-output match. `Result<_, _>` is NOT peeled, so a /// fallible factory (`-> Result`) keeps its handle return; the error /// position is matched separately on `E`. -fn returns_type(registry: &Registry, ret: &syn::Type, key: &TypeKey) -> bool { - TypeKey::from_type(&peel(registry, ret).core) == *key +fn returns_type(ret: &crate::api::core::flat::TypeRef, key: &TypeKey) -> bool { + TypeKey::from_type(&peel(ret).core) == *key } /// Build one output/error plan for `ed` and store it in the right registry map. @@ -1035,23 +958,23 @@ fn process_decl( ed: &OutputDecl, ) -> Result<(), UnfoldError> { { - let item_fn = registry + // The value to decompose: the success return (`Output`) or the + // `Result<_, E>` domain error `E` (`Error`). + let fn_ret = registry .flat() .function(&ed.func) - .map(|f| f.origin.syntax.clone()) + .map(|f| f.ret.clone()) .ok_or_else(|| UnfoldError::UnknownFunction(ed.func.clone()))?; - - // The value to decompose: the success return (`Output`) or the - // `Result<_, E>` domain error `E` (`Error`). - let ret_ty: syn::Type = match ed.target { - DeconTarget::Output => fn_return(&item_fn), + let ret_ty = match ed.target { + DeconTarget::Output => fn_ret, DeconTarget::Error => { - fallible_err(registry, &fn_return(&item_fn)).ok_or_else(|| { - UnfoldError::Unsupported { + fn_ret + .fallible_parts() + .map(|(_, e)| e.clone()) + .ok_or_else(|| UnfoldError::Unsupported { func: ed.func.clone(), reason: "convert_error/deconstruct_error on a non-Result return", - } - })? + })? } }; @@ -1062,11 +985,11 @@ fn process_decl( // the historical probe order (the `Vec` probe runs on `E` itself), so // an `Option>` error stays whole. let (optional, after_opt) = match ed.target { - DeconTarget::Output => match optional_inner(registry, &ret_ty) { + DeconTarget::Output => match ret_ty.optional_inner() { Some(inner) => (true, inner), - None => (false, ret_ty.clone()), + None => (false, &ret_ty), }, - DeconTarget::Error => (false, ret_ty.clone()), + DeconTarget::Error => (false, &ret_ty), }; // `Vec` / `Option>` return → `Iterable` (± an `Optional` // layer). Two element-delivery modes: @@ -1076,8 +999,8 @@ fn process_decl( // via its own output converter + projection, fold `(acc, T) -> acc`. // The other shapes (`Option`/scalar) decompose via an accessor // (M1–M3). `Vec>` is not supported. - let plan = if let Some(inner) = sequence_elem(registry, &after_opt) { - if optional_inner(registry, &inner).is_some() { + let plan = if let Some(inner) = after_opt.sequence_elem() { + if inner.optional_inner().is_some() { return Err(UnfoldError::Unsupported { func: ed.func.clone(), reason: "Vec> returns", @@ -1097,20 +1020,20 @@ fn process_decl( // recursive registration also required) — same reasoning as // [`apply_leaf_vec_folds`] for the fixed folds. if ed.target == DeconTarget::Output { - registry.unrequire_output(&ret_ty); + registry.unrequire_output(&ret_ty.origin.syntax); if optional { - registry.unrequire_output(&after_opt); + registry.unrequire_output(&after_opt.origin.syntax); } } // Element type peeled of a leading `&` (accessors take `&Element`). - let (by_ref, element) = peel_borrow(registry, &inner); - let ekey = TypeKey::from_type(&element); + let (by_ref, element) = peel_borrow(inner); + let ekey = element.key(); if let Some(d) = find_deconstructor_by_type(acc, &ekey) { // Decomposed: reuse the shared flatten (M3 nesting composes). let records = d.records.clone(); let decon = decl_id(&ekey, d); - register_decon_spec(registry, acc, &decon, &records, &element)?; - let plan = build_plan(acc, registry, ed, by_ref, &element, shape, &records, decon)?; + register_decon_spec(registry, acc, &decon, &records, element)?; + let plan = build_plan(acc, registry, ed, by_ref, element, shape, &records, decon)?; for leaf in &plan.leaves { registry.require_output(&leaf.out_ty); } @@ -1120,15 +1043,15 @@ fn process_decl( // element's own output converter matches `into_iter()`'s yield. // No declaration is involved (`decon: None`) — the element // crosses whole through its own converter. - let by_ref = peel_borrow(registry, &inner).0; - registry.require_output(&inner); + let by_ref = peel_borrow(inner).0; + registry.require_output(&inner.origin.syntax); UnfoldPlan { - source: inner.clone(), + source: inner.origin.syntax.clone(), decon: None, by_ref, shape, leaves: vec![], - element: Some(inner.clone()), + element: Some(inner.origin.syntax.clone()), delivery: ed.delivery, convert_out_ty: None, fixed_builder: false, @@ -1141,22 +1064,22 @@ fn process_decl( // re-peeled and fails as "no deconstructor" for the inner // `Option`); for `Error` it happens here, unchanged. let (optional, core_ty) = match ed.target { - DeconTarget::Output => (optional, after_opt.clone()), - DeconTarget::Error => match optional_inner(registry, &after_opt) { + DeconTarget::Output => (optional, after_opt), + DeconTarget::Error => match after_opt.optional_inner() { Some(inner) => (true, inner), - None => (false, after_opt.clone()), + None => (false, after_opt), }, }; - let (by_ref, source) = peel_borrow(registry, &core_ty); - let source_key = TypeKey::from_type(&source); + let (by_ref, source) = peel_borrow(core_ty); + let source_key = source.key(); let shape = if optional { UnfoldShape::Optional((), Box::new(UnfoldShape::Base)) } else { UnfoldShape::Base }; let (records, decon) = resolve_deconstructor(acc, &source_key, ed)?; - register_decon_spec(registry, acc, &decon, &records, &source)?; - let plan = build_plan(acc, registry, ed, by_ref, &source, shape, &records, decon)?; + register_decon_spec(registry, acc, &decon, &records, source)?; + let plan = build_plan(acc, registry, ed, by_ref, source, shape, &records, decon)?; for leaf in &plan.leaves { registry.require_output(&leaf.out_ty); } @@ -1226,14 +1149,14 @@ fn register_decon_spec( acc: &Deconstructors, decon: &DeconId, records: &[DeconRecord], - source: &syn::Type, + source: &crate::api::core::flat::TypeRef, ) -> Result<(), UnfoldError> { if registry.decon_plans.contains_key(decon) { return Ok(()); } let mut leaves: Vec = Vec::new(); let mut visited: HashSet = HashSet::new(); - visited.insert(TypeKey::from_type(source)); + visited.insert(source.key()); flatten( acc, registry, @@ -1249,11 +1172,11 @@ fn register_decon_spec( // derived from it, never emitted code — so its hoists are discarded. &mut Vec::new(), )?; - require_unique_leaf_names(source, &leaves)?; + require_unique_leaf_names(&source.origin.syntax, &leaves)?; registry.decon_plans.insert( decon.clone(), DeconSpec { - source: source.clone(), + source: source.origin.syntax.clone(), leaves, }, ); @@ -1304,14 +1227,14 @@ fn build_plan( registry: &Registry, ed: &OutputDecl, by_ref: bool, - source: &syn::Type, + source: &crate::api::core::flat::TypeRef, shape: UnfoldShape, records: &[DeconRecord], decon: DeconId, ) -> Result { let mut leaves: Vec = Vec::new(); let mut visited: HashSet = HashSet::new(); - visited.insert(TypeKey::from_type(source)); + visited.insert(source.key()); let mut hoists: Vec = Vec::new(); flatten( acc, @@ -1326,11 +1249,11 @@ fn build_plan( &mut leaves, &mut hoists, )?; - require_unique_leaf_names(source, &leaves)?; - require_root_identity_last(by_ref, source, &leaves)?; + require_unique_leaf_names(&source.origin.syntax, &leaves)?; + require_root_identity_last(by_ref, &source.origin.syntax, &leaves)?; Ok(UnfoldPlan { - source: source.clone(), + source: source.origin.syntax.clone(), decon: Some(decon), by_ref, shape, @@ -1392,7 +1315,7 @@ fn flatten( acc: &Deconstructors, registry: &Registry, records: &[DeconRecord], - source: &syn::Type, + source: &crate::api::core::flat::TypeRef, path_prefix: &[PathStep], name_prefix: &[String], by_ref: bool, @@ -1401,7 +1324,7 @@ fn flatten( leaves: &mut Vec, hoists: &mut Vec, ) -> Result<(), UnfoldError> { - let source_key = TypeKey::from_type(source); + let source_key = source.key(); // The author-supplied (literal) leaf-name segment at this level, appended // to the inherited chain prefix. Segments are joined with `"__"`. let seg_name = |name: &str| -> Vec { @@ -1430,10 +1353,14 @@ fn flatten( // + projection come from this `out_ty`'s output converter, so // this is what decides whether the leaf is boxed by move or // cloned through the borrowed-opaque one. + // A plan field: the drop to spelling happens here, where the value + // is stored for emission, and the borrowed form is composed rather + // than looked up because no source wrote it. + let src = &source.origin.syntax; let out_ty: syn::Type = if place_is_owned(hoists, path_prefix, by_ref) { - source.clone() + src.clone() } else { - syn::parse_quote!(&#source) + syn::parse_quote!(&#src) }; leaves.push(UnfoldLeaf { name: if path_prefix.is_empty() { @@ -1459,7 +1386,7 @@ fn flatten( // call, so the whole record shares a single `Call` step and the // emitter can hoist it. let (takes, _ret) = accessor_signature(registry, func)?; - check_takes(func, &takes, source)?; + check_takes(func, &takes, &source.origin.syntax)?; // The declarator states whether the value is given away; the // signature has to agree, or the emitted call would not compile // in the consumer's crate. Checked rather than inferred so that @@ -1533,10 +1460,14 @@ fn flatten( }); for fr in fields { + // A field record is adapter-declared, so it has no element — + // but the scan took its reading when the cell was born, and + // that is what this reads. Nothing is re-classified. + let fr_reading = registry.reading(&fr.ty); // A field's own `Option` makes everything under it nullable, // exactly as an `Option`-returning accessor step does. - let (opt, core) = match optional_inner(registry, &fr.ty) { - Some(inner) => (true, inner), + let (opt, core) = match fr_reading.as_ref().and_then(|r| r.optional_inner()) { + Some(inner) => (true, inner.origin.syntax.clone()), None => (false, fr.ty.clone()), }; let child_ty = peel_ref(&core); @@ -1604,7 +1535,13 @@ fn flatten( acc, registry, &child_records, - &child_ty, + // Again the registry's answer, not a new one. + ®istry.reading(&child_ty).ok_or_else(|| { + UnfoldError::Unsupported { + func: func.clone(), + reason: "a field type the language cannot express", + } + })?, &field_path, &seg_name(&fr.name), by_ref, @@ -1642,18 +1579,18 @@ fn flatten( DeconRecord::Identity | DeconRecord::Fields { .. } => unreachable!(), }; let (takes, ret) = accessor_signature(registry, &func)?; - check_takes(&func, &takes, source)?; + check_takes(&func, &takes, &source.origin.syntax)?; // Default unwrap: if the return type has its own deconstructor, // splice it (recurse); otherwise the return is one leaf. Peel an // `Option` (value may be absent) + leading `&` to reach the child. // This site peels an `Option` only — an accessor returning a run // of values is not spliced — so it asks the model for that one // layer rather than the whole stack. - let after_opt = optional_inner(registry, &ret); + let after_opt = ret.optional_inner(); let opt = after_opt.is_some(); - let core = after_opt.unwrap_or_else(|| ret.clone()); - let (core_by_ref, child_ty) = peel_borrow(registry, &core); - let child_key = TypeKey::from_type(&child_ty); + let core = after_opt.unwrap_or(&ret); + let (core_by_ref, child_ty) = peel_borrow(core); + let child_key = child_ty.key(); // A child already on the nesting chain: for a `#[prebindgen]` // accessor that is an authoring cycle (hard error); a // binding-local field re-delivering (part of) its own type @@ -1678,7 +1615,7 @@ fn flatten( acc, registry, &child_records, - &child_ty, + child_ty, &child_path, &seg_name(name), by_ref, @@ -1712,10 +1649,12 @@ fn flatten( } seen_identity = true; } + // A plan field: the spelling is taken here, once, where the + // leaf is stored for emission. let (out_ty, nullable, identity) = if cond_handle { - (core.clone(), true, true) + (core.origin.syntax.clone(), true, true) } else { - (ret, nullable, false) + (ret.origin.syntax.clone(), nullable, false) }; let mut path = path_prefix.to_vec(); path.push(PathStep::call(func.clone(), opt, !core_by_ref)); @@ -1774,7 +1713,7 @@ pub fn dedup_names(names: &mut [String]) { fn accessor_signature( registry: &Registry, func: &syn::Ident, -) -> Result<(syn::Type, syn::Type), UnfoldError> { +) -> Result<(syn::Type, crate::api::core::flat::TypeRef), UnfoldError> { let f = registry .flat() .function(&func) @@ -1791,8 +1730,7 @@ fn accessor_signature( crate::api::core::flat::TypeKind::Ref { inner, .. } => inner.origin.syntax.clone(), _ => first.ty.origin.syntax.clone(), }; - let ret: syn::Type = f.ret.origin.syntax.clone(); - Ok((takes, ret)) + Ok((takes, f.ret.clone())) } /// Whether the value sitting at `path_prefix` is the plan's **to give away**: From 4fed457f948a8f08d8d98e5a970b1f562665bd11 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 14:23:54 +0200 Subject: [PATCH 21/52] L2 is done, and it taught something worth keeping (#262) * L2 is done, and it taught something worth keeping Marks the stage done -- #248, #257, #258, #261 -- and records what the layer read cost, because that is the part a later stage can use. Three defects, one root: a peel that answered more than its caller could represent. Vec matching a T constructor, the stack recursing past its own documented order, and Layers being a fourth copy of Shape encoded as flags so a caller could only ignore a layer rather than decline it. None was visible to the evidence this programme leans on. The suite passed and regen stayed byte-identical through all three, because no in-tree example exercises those shapes -- so "byte-identical" proved less than it sounds. Worth writing down next to the stage that discovered it. The rule that came out outlives L2: the peel is chosen by the consumer's capability, not by the type's structure. L3 and L4 will call the same accessors. Also states where api/core actually ends -- 13, not 0 -- and separates the two reasons: ten helpers blocked on adapter callers, and two in registry/scan that stay for good because they diagnose a spelling a build script author wrote. * Record what L2 taught twice: the ledger measures the wrong thing here #262 marked L2 done and recorded three defects in the layer read. A review then found a fourth, and it is the one worth carrying furthest: fourteen sites still reached into origin.syntax for a fact the element already held, and the ledger could not see any of it. That is not a footnote on the stage, it is a fact about the instrument. The count measures who matches syn variants; it does not measure who reasons from origin, and those came apart the moment the matching moved into one shared classifier. Only the second is what #211 asks for. So the section now says both, and says what follows: a stage reporting only its delta is reporting the proxy, and where a rule can be made structural it should be -- #263's claim is that the round trip does not compile, not that a number fell. --- docs/language-integration.md | 106 ++++++++++++++++++++++++++++++----- 1 file changed, 92 insertions(+), 14 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 8ee8d31f..97dacb6d 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -118,7 +118,7 @@ moves it. | L1 | `Registry` consumes elements | **done** — [#238](https://github.com/milyin/prebindgen/pull/238) | | L1.5 | The model is the only index | **done** — #239–#246 | | L1.75 | The registry becomes describable | **done** — #249–#253, squashed into #248's commit | -| L2 | `api/core` stops classifying source syntax | **in progress** — [#248](https://github.com/milyin/prebindgen/pull/248) took 35 of 71 | +| L2 | `api/core` stops classifying source syntax | **done** — #248, #257, #258, #261, #263 | | L3 | `Cbindgen` consumes elements | not started | | L4 | `JniGen` consumes elements *(the long pole)* | not started | | L5 | Close the seam: the public contract stops being `syn` | not started | @@ -304,7 +304,7 @@ redesign too. Do not read the commit log as the inventory: `flat-drop-pattern-en still reports 28 commits ahead of `language-integration` because a squash records no ancestry, while the trees differ by nothing. Diff the content, not the history. -### L2 — `api/core` stops classifying source syntax — **in progress** +### L2 — `api/core` stops classifying source syntax — **done** - [x] **The pattern engine is deleted** ([#248](https://github.com/milyin/prebindgen/pull/248)): `match_pattern`, @@ -333,20 +333,98 @@ no ancestry, while the trees differ by nothing. Diff the content, not the histor `type_from_ident` and the rest become `core::flat::spelling`. They decide what spelling a type *has* before anything keys on it — the same authority that decides what it *means*. Ledger 158 → 154 +- [x] **One layer read** ([#261](https://github.com/milyin/prebindgen/pull/261)): + the twenty sites in `unfold` and `expand` that peeled `Option`, then `Vec`, + then `&` by taking a spelling apart now read the model's arity stack. + Ledger 154 → 135 +- [x] **The reading is carried, not re-derived** + ([#263](https://github.com/milyin/prebindgen/pull/263)): fourteen sites still + reached into `origin.syntax` for a fact the element already held — a + `Function::ret` that is a `TypeRef`, callback arguments that are `TypeRef`s. + The helpers now take `&TypeRef`, so the round trip does not compile. **The + ledger did not move**, which is the finding, not a footnote — see below **#248 is deletion, not migration**, and the distinction is worth keeping visible: -35 sites left because their code left. The ones that remain are the ones that have -to actually start reading elements, so the rate so far is not the rate to expect. -The same caveat applies to the spelling move above, which is a **move**. - -**What is left, and why it is not all of it.** Every classifying helper still in -`api/core/types_util` is called overwhelmingly from the adapters — -`option_inner_type` 40 times, `bare_path_ident` 22, `is_unit` 18 — and none takes -the model as an argument, so it cannot consult it from the inside. L2 can stop -`api/core` from *calling* them; only L3 and L4 can free them to be deleted. The -remaining migration (`unfold` 16, `expand` 4) and the running plan live in -[#229](https://github.com/milyin/prebindgen/pull/229), which is where stage state -is edited. +35 sites left because their code left. The same caveat applies to the spelling +move, which is a **move**. Only the last item above is a migration in the full +sense, and it is the one that took the most arguing. + +#### What L2 taught: a peel must match what the consumer can build + +Three defects in the layer read, all one root — a peel that answered more than its +caller could represent — and none of them visible to the evidence this programme +usually relies on. The suite passed and regen stayed byte-identical through all +three, because no in-tree example exercises the shapes involved. + +- **`Vec` matched a `T` constructor.** Expansion builds one value; its plan + shape has no iterable arm. A peel that removed the `Sequence` anyway made a + `Vec` parameter match a `T` constructor, and the wrapper would have handed one + reconstructed `T` to a parameter expecting the collection. +- **The stack recursed past its own contract.** `Vec>` read as an + optional inside a run, so a return matched a decomposition target `T` and + installed a fold — for a type the explicit path next to it refuses outright. Two + paths disagreeing about one return, the silent one winning. +- **`Layers` was a fourth copy** of `core::shape::Shape`, whose own module doc says + it replaced three. Encoded as flags, so a caller could only *ignore* a layer it + could not build; the stack lets it **decline** by not matching. + +What came out of it is the rule, and it outlives the stage: **the peel is chosen by +the consumer's capability, not by the type's structure.** `TypeRef` therefore +offers both — `layer_stack` for a consumer that implements every layer, and +`optional_inner` / `sequence_elem` / `borrow_target` for one that composes exactly +what it can honour. + +#### What L2 taught twice: the ledger measures the wrong thing for this + +The fourth defect was the measurement itself, and it is the one worth carrying +furthest. L2 was first reported done on the strength of the count falling 154 → 135. +Then a review pointed at this, which had survived all of it: + +```rust +let item_fn = flat.function(&f).map(|f| f.origin.syntax.clone())?; +let ret = fn_return(&item_fn); // dig the return out of raw syntax +returns_type(registry, &ret, &key) // -> classify() -> re-lower it +``` + +`Function::ret` is **already** a `TypeRef` with `kind` computed at parse time. The +model handed the answer over; the code reached into `origin` and derived it again — +in six places, with five more re-extracting callback arguments the model held as +`TypeRef`s, and three digging parameters out of a cloned `ItemFn`. + +The ledger could not see any of it. It counts **variant mentions of watched syn +enums per file, outside `core::flat`**, so moving a match into one shared classifier +drops the count without changing the data flow. Both facts are real, and they are +different facts: + +> The ledger measures **who matches syn variants**. It does not measure **who +> reasons from `origin`**. Those came apart the moment the matching moved into one +> place, and only the second is what #211 asks for. + +The fix is a signature rather than a checker — `peel`, `peel_borrow` and +`returns_type` take a `&TypeRef`, so a caller must already hold a reading and the +round trip does not compile. `Flat::classify` belongs to the registry, which is the +authority on what a type means because it is the thing that **stores** readings. +`origin.syntax` is read only where a value is stored for emission. + +**So a count is a proxy, and this one has a known blind spot.** A stage that reports +only its delta is reporting the proxy. Where a rule can be made structural, it +should be — the deltas L3 and L4 report are worth exactly as much as the invariants +they can point at underneath them. + +#### Where `api/core` ends, and why it is not zero + +**13 sites**: `types_util` 10, `registry/scan` 2, `unfold` 1. + +Every classifying helper still in `types_util` is called overwhelmingly from the +adapters — `option_inner_type` 40 times, `bare_path_ident` 22, `is_unit` 18 — and +none takes the model as an argument, so it cannot consult it from the inside. L2 +stopped `api/core` from *calling* them; only L3 and L4 can free them to be deleted. +`unfold`'s one is `peel_ref`, in the same position with three jnigen callers. + +The two in `registry/scan` are different and stay for good: they inspect a key a +**build-script author** wrote, to diagnose that spelling — no source type is being +classified, so there is no element to read instead. They are the first entries to +land in the *"legitimately the adapter's business"* category this document predicts. ### L3 — `Cbindgen` consumes elements From 0380d59914db06421cdeaa45bde76103bb753a49 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 15:04:44 +0200 Subject: [PATCH 22/52] L4a: the crossing hands over the reading (#264) * L4a: the crossing hands over the reading The entry point of jnigen's converter dispatch discarded the model at the door: let (dir, key) = crossing; let ty = key.to_type(); // rebuild a spelling self.select_input_type(&ty, built) // and classify it again while the registry holds the reading in the cell the scan filled. jnigen's currency is TypeKey, so the whole dispatch chain ran on spellings that were then re-classified -- the same defect #263 removed from api/core, one layer out, and the reason a file-by-file migration would keep producing new ones. Conversions gains `reading`. That trait is the right seam and its own doc already said so: a helper takes &impl Conversions and works either side of the fill boundary. Building answers during the fill, Registry afterwards, both from the same cell, so the answer does not change across that line. select_input_type takes a &TypeRef and reads its layers off the classification. Deliberately ONE LAYER AT A TIME rather than layer_stack: each arm emits exactly one shape and hands the rest back through `subs` to be selected on its own, so peeling further would claim a shape this selector does not emit. That is the rule L2 paid three defects to learn. The callback fallback stops calling extract_fn_trait_args: TypeKind::Callback carries the argument types as TypeRefs, so there is nothing to re-extract. What stays in the selector is the wildcard patterns it builds -- Option<_>, &_, Vec<_>. Those are the adapter's own shapes, composed from tokens, and composing is spelling rather than reasoning. Six of the file's seven sites are that, which is why the ledger moves by one. One behaviour question the model settled: the slice arm used to match syn::Type::Slice, so `&[T]` took the Vec<_> shape and `&Vec` did not. The model classifies both as Ref(Sequence) -- deliberately, since ownership is the Ref layer's fact -- so they now take the same arm. regen-check is byte-identical, so nothing in tree relied on the distinction, and the model's reading is the more correct one. Reported: regen-check byte-identical on every committed artifact, covertest included -- which is the check that counts here, since this touches emission. Explained: same dispatch, chosen from the reading instead of a re-parse. Asserted: the converter dispatch starts from the model, not from a spelling rebuilt out of a key. Ledger 135 -> 134, and the small number is the point: it measures who matches syn variants, and this moved who reasons from origin. * Two questions at the sequence arm, and only one is kind's The review is right about the model, and the answer to "is this the model or how we read it" is: the reading. TypeKind::Sequence covers Box> and Cow<'_, [T]> as well as Vec and [T] -- deliberately, because no destination language can tell them apart. But the generated Rust glue IS a destination artifact, and it is the one consumer that can: &Vec is not &Box>. TypeRef::origin's own doc says exactly this -- "Box is a Str here ... what Rust needs and no destination language can see lives in the tokens". So I used kind to settle a question that depends on the spelling. The arm now asks both: kind says a run of values makes the Vec shortcut a CANDIDATE, and the spelling says whether a decoded Vec could actually be handed to the function. [T] deref-coerces, Vec is the thing itself, a transparent wrapper is neither and falls through to the plain borrow arm -- which is where the old syntactic slice check left it. The predicted consequence does not occur, and I checked rather than assumed. &Box> is refused before anything is emitted, identically with and without the guard: the scan requires every nested position, nothing converts Box>, and the binding is rejected. That is the right failure, and it is what the new test pins -- an assertion about refusing, not about generated text that never exists. The guard keeps the selector's reasoning honest for the day that over-approximation is relaxed. Ledger 134 -> 136, so L4a is net +1 against the 135 it started from. The guard is two syn matches, and they are the legitimate kind: a spelling question about generated Rust. Worth stating plainly, because it is the same axis problem the doc now records -- this PR removed the re-parse at the dispatch door and the count went UP. --- prebindgen/src/api/core/flat/boundary.ledger | 4 +- prebindgen/src/api/core/registry/declare.rs | 3 + prebindgen/src/api/core/registry/view.rs | 19 +++ .../src/api/lang/jnigen/jni/selector.rs | 109 +++++++++++++----- .../src/api/lang/jnigen/jni/tests/values.rs | 49 ++++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 13 ++- 6 files changed, 163 insertions(+), 34 deletions(-) diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index df15392c..c9441adf 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -67,9 +67,9 @@ 1 api/lang/jnigen/jni/prim.rs 3 api/lang/jnigen/jni/prim_array.rs 8 api/lang/jnigen/jni/render.rs -7 api/lang/jnigen/jni/selector.rs +8 api/lang/jnigen/jni/selector.rs 11 api/lang/jnigen/jni/trait_impl.rs 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 135 +# total: 136 diff --git a/prebindgen/src/api/core/registry/declare.rs b/prebindgen/src/api/core/registry/declare.rs index 54dc3c48..17953371 100644 --- a/prebindgen/src/api/core/registry/declare.rs +++ b/prebindgen/src/api/core/registry/declare.rs @@ -427,6 +427,9 @@ impl RegistryBuilder { /// /// [`conversion`]: Conversions::conversion impl Conversions for RegistryBuilder { + fn reading(&self, ty: &syn::Type) -> Option { + self.registry.reading(ty) + } fn flat(&self) -> &crate::api::core::flat::Flat { &self.registry.flat } diff --git a/prebindgen/src/api/core/registry/view.rs b/prebindgen/src/api/core/registry/view.rs index 7c8dd31c..a6f13b2d 100644 --- a/prebindgen/src/api/core/registry/view.rs +++ b/prebindgen/src/api/core/registry/view.rs @@ -24,6 +24,19 @@ pub trait Conversions { /// The model. fn flat(&self) -> &crate::api::core::flat::Flat; + /// The reading for `ty` — what the frontend made of it. + /// + /// On the trait because it is needed on **both** sides of the fill: a + /// converter is chosen for a type while the registry is still being built, and + /// an emitter asks about the same type afterwards. Both views answer from the + /// cell the scan filled, so the answer does not change across that line. + /// + /// This is what lets a generator take a crossing and reason about it without + /// rebuilding a spelling from the key and classifying that — the round trip + /// `api/core` removed from itself in #263, which is the same defect one layer + /// out. + fn reading(&self, ty: &syn::Type) -> Option; + /// The conversion for `ty` in `dir`, if there is one. fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry>; @@ -80,6 +93,9 @@ impl Conversions for Building<'_, M> { fn flat(&self) -> &crate::api::core::flat::Flat { &self.registry.flat } + fn reading(&self, ty: &syn::Type) -> Option { + self.registry.reading(ty) + } fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { self.built.get(&(dir, TypeKey::from_type(ty))) } @@ -113,6 +129,9 @@ impl Conversions for Registry { fn flat(&self) -> &crate::api::core::flat::Flat { &self.flat } + fn reading(&self, ty: &syn::Type) -> Option { + Registry::reading(self, ty) + } fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { self.type_table(dir) .get(&TypeKey::from_type(ty))? diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index d4d0884a..ee67ecc3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -33,46 +33,80 @@ fn ref_wildcard(r: &syn::TypeReference) -> syn::Type { syn::Type::Reference(pr) } +/// Whether a decoded `Vec` local can be borrowed where `referent` is expected. +/// +/// A **spelling** question, deliberately: it decides what the generated Rust must +/// be able to say, and Rust distinguishes forms the boundary classification does +/// not. `[T]` is reached by deref coercion from `&Vec` and `Vec` is the +/// thing itself; a transparent wrapper such as `Box>` or `Cow<'_, [T]>` +/// classifies identically and cannot be reconstructed from the decoded local. +fn decoded_vec_satisfies(referent: &syn::Type) -> bool { + match referent { + syn::Type::Slice(_) => true, + syn::Type::Path(tp) => tp + .path + .segments + .last() + .is_some_and(|s| s.ident == "Vec" && tp.path.segments.len() == 1), + _ => false, + } +} + impl Declarations { /// Select the input converter for `ty`: terminals, user wrappers, then /// built-in structural wrappers. pub(crate) fn select_input_type( &self, - ty: &syn::Type, + ty: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + use crate::api::core::flat::RefMode; + + // The spelling, for the wildcard patterns this selector builds. Those are + // the adapter's own — `Option<_>`, `&_` — so composing them from tokens is + // spelling, not reasoning. What the type *is* comes from `ty` below. + let syntax = &ty.origin.syntax; + // 1. Terminal categories (incl. the terminal user-wrapper lookup). - if let Some(c) = self.input_terminal(ty, registry) { + if let Some(c) = self.input_terminal(syntax, registry) { return Some(c); } - // 3. Built-in wrapper shapes. `Option<&T>` tries the DEEP `Option<&_>` - // (borrowed-handle → `Option>`) before the shallow - // `Option<_>`; the shape that resolves correctly wins. - if let Some(inner) = option_inner_type(ty) { - if let syn::Type::Reference(r) = &inner { - let pat = with_first_arg(ty, ref_wildcard(r)); - let t1 = (*r.elem).clone(); - if let Some(mut c) = self.input_wrapper_shape(&pat, &t1, registry) { - c.subs = vec![t1]; - return Some(c); + // 3. Built-in wrapper shapes, read one layer at a time rather than as a + // whole stack: each arm handles exactly one, and hands the rest back + // through `subs` to be selected on its own. Peeling further here would + // claim a shape this selector does not emit. + if let Some(inner) = ty.optional_inner() { + // `Option<&T>` tries the DEEP `Option<&_>` (borrowed-handle → + // `Option>`) before the shallow `Option<_>`; the shape + // that resolves correctly wins. + if let Some(target) = inner.borrow_target() { + if let syn::Type::Reference(r) = &inner.origin.syntax { + let pat = with_first_arg(syntax, ref_wildcard(r)); + let t1 = target.origin.syntax.clone(); + if let Some(mut c) = self.input_wrapper_shape(&pat, &t1, registry) { + c.subs = vec![t1]; + return Some(c); + } } } - let pat = with_first_arg(ty, syn::parse_quote!(_)); - if let Some(mut c) = self.input_wrapper_shape(&pat, &inner, registry) { - c.subs = vec![inner]; + let pat = with_first_arg(syntax, syn::parse_quote!(_)); + let inner_ty = inner.origin.syntax.clone(); + if let Some(mut c) = self.input_wrapper_shape(&pat, &inner_ty, registry) { + c.subs = vec![inner_ty]; return Some(c); } return None; } - if let Some(elem) = vec_inner_type(ty) { - let pat = with_first_arg(ty, syn::parse_quote!(_)); - if let Some(mut c) = self.input_wrapper_shape(&pat, &elem, registry) { - c.subs = vec![elem]; + if let Some(elem) = ty.sequence_elem() { + let pat = with_first_arg(syntax, syn::parse_quote!(_)); + let elem_ty = elem.origin.syntax.clone(); + if let Some(mut c) = self.input_wrapper_shape(&pat, &elem_ty, registry) { + c.subs = vec![elem_ty]; return Some(c); } return None; } - if let syn::Type::Reference(r) = ty { + if let crate::api::core::flat::TypeKind::Ref { mode, inner } = &ty.kind { // `&[T]` shared slice borrow: there is no owned `[T]` to decode, so // reuse the `Vec<_>` shape — decode the Java `List` into an owned // `Vec`; the call site borrows it (`&Vec` deref-coerces to @@ -80,22 +114,37 @@ impl Declarations { // `Vec` input (the writer dedupes the shared converter fn by ident, // so the two can coexist). `&mut [T]` is intentionally not supported // (no write-back of the decoded Vec). - if r.mutability.is_none() { - if let syn::Type::Slice(s) = &*r.elem { - let elem = (*s.elem).clone(); + // Two questions, and only the first is `kind`'s. That it is a run of + // values makes this arm a *candidate*; whether the decoded `Vec` + // can be handed to the Rust function is a question about the + // **spelling**, because the generated glue is the one consumer that + // can tell `&Vec` from `&Box>` — the exact thing + // `TypeRef::origin` exists to carry. + // + // `&[T]` deref-coerces from `&Vec` and `&Vec` is already it, so + // both are satisfied by the decoded local. A transparent wrapper — + // `Box>`, `Cow<'_, [T]>` — is `Sequence` all the same and is + // NOT: passing `&Vec` there does not compile. Those fall through to + // the plain borrow arm below, which hands the whole spelling on as the + // sub, exactly as the old syntactic slice check did. + if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(&inner.origin.syntax) { + if let Some(elem) = inner.sequence_elem() { + let elem_ty = elem.origin.syntax.clone(); let pat: syn::Type = syn::parse_quote!(Vec<_>); - if let Some(mut c) = self.input_wrapper_shape(&pat, &elem, registry) { - c.subs = vec![elem]; + if let Some(mut c) = self.input_wrapper_shape(&pat, &elem_ty, registry) { + c.subs = vec![elem_ty]; return Some(c); } return None; } } - let pat = ref_wildcard(r); - let t1 = (*r.elem).clone(); - if let Some(mut c) = self.input_wrapper_shape(&pat, &t1, registry) { - c.subs = vec![t1]; - return Some(c); + if let syn::Type::Reference(r) = syntax { + let pat = ref_wildcard(r); + let t1 = inner.origin.syntax.clone(); + if let Some(mut c) = self.input_wrapper_shape(&pat, &t1, registry) { + c.subs = vec![t1]; + return Some(c); + } } } None diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs index 263d2bc8..9fe7d992 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs @@ -1561,3 +1561,52 @@ fn check_array_length_qualification(loc: SourceLocation, module: &str) { assert!(!rc.contains(&format!("{module}::env,")), "{rust}"); assert!(!rc.contains(&format!("&mut{module}::env")), "{rust}"); } + +/// A borrowed **transparent wrapper** around a sequence is refused, not decoded as +/// a bare `Vec`. +/// +/// `Box>` and `Cow<'_, [T]>` classify as `TypeKind::Sequence` — correctly: +/// no destination language can tell them from `Vec`, which is exactly why the +/// model folds them together. But the generated glue **is** a destination +/// artifact, and it is the one consumer that can tell: `&Vec` is not +/// `&Box>`, and `TypeRef::origin` exists to carry precisely that. +/// +/// So the selector asks two questions, and only the first is `kind`'s: that this +/// is a run of values makes the `Vec` shortcut a *candidate*, and the **spelling** +/// says whether a decoded `Vec` could actually be handed to the function. +/// `&[T]` deref-coerces and `&Vec` is the thing itself; a wrapper is neither. +/// +/// **What this pins is the refusal.** A miscompile is not reachable today even +/// without the spelling guard, because the scan requires every nested position and +/// nothing converts `Box>` either — so the binding is rejected before +/// anything is emitted, which is the right failure. The guard keeps the selector's +/// reasoning honest for the day that over-approximation is relaxed; this test +/// pins the behaviour a user sees now, and it is deliberately an assertion about +/// *refusing*, not about generated text that no longer exists. +#[test] +fn a_borrowed_transparent_sequence_wrapper_is_not_decoded_as_a_vec() { + let loc = crate::SourceLocation::default(); + let items: Vec<(syn::Item, crate::SourceLocation)> = vec![( + syn::Item::Fn(syn::parse_quote!( + pub fn z_take_boxed(v: &Box>) -> i64 { + v.len() as i64 + } + )), + loc.clone(), + )]; + let decls = crate::package!("ops").fun(crate::lang::FunctionDecl::new( + syn::parse_str("z_take_boxed").unwrap(), + )); + let registry = crate::api::test_util::reg_from_items(items).expect("index"); + let err = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package(decls) + .build_with(registry) + .expect_err("a borrowed transparent sequence wrapper has no conversion"); + + let msg = err.to_string(); + assert!( + msg.contains("Box < Vec < i32 > >"), + "the refusal must name the wrapper spelling the binding cannot convert:\n{msg}" + ); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index acab91e8..401b855b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -997,12 +997,21 @@ impl Declarations { ) -> Option> { let (dir, key) = crossing; let ty = key.to_type(); + // The reading the scan already took for this crossing. Rebuilding a + // spelling from the key and classifying *that* is the round trip #263 + // removed from `api/core`; this is the same door, one layer out. + let reading = built.reading(&ty)?; match dir { - Direction::Input => self.select_input_type(&ty, built).or_else(|| { + Direction::Input => self.select_input_type(&reading, built).or_else(|| { // `impl Fn(args)` that nothing else claimed. Callback args cross // in the OPPOSITE direction, which is why their required-ness // rides `immediate_edges` rather than this converter's `subs`. - let args = crate::api::core::flat::extract_fn_trait_args(&ty)?; + // The arguments are `TypeRef`s on the classification, so nothing + // is re-extracted from the signature's syntax. + let crate::api::core::flat::TypeKind::Callback { args } = &reading.kind else { + return None; + }; + let args: Vec = args.iter().map(|a| a.origin.syntax.clone()).collect(); self.dispatch_fn_input(&args, built) }), Direction::Output => self.select_output_type(&ty, built), From 8e1153ca703adfd3ee4a6eccb30340519665a310 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 17:02:16 +0200 Subject: [PATCH 23/52] Take the reading from the declaration; `reading` becomes a lookup (#267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #266. `Registry::reading` fell back to `Flat::classify` on a table miss, and the fallback fired constantly — `i64` x48, `String` x25, `bool` x16 across the test suite. Scalars reaching a lookup miss was the tell: those are certainly registered by the time a binding is built, so the misses were an inverted order, not unknown types. `flatten` asked about exactly the leaves its caller registers one loop later. Because `classify` answered correctly, the output was right and nothing showed it. The fix is not to register earlier. A `FieldRecord` is adapter-declared and is built from an element whose every field already has a reading — there was never anything to look up. `FieldRecord::ty` becomes a `TypeRef`, which removes the reason the fallback existed. That needs per-field readings, so `TypeKind::DataStruct` carries `&flat::Struct` instead of its `syn::ItemStruct` — the one leak #229's L4 list names by hand. The probes follow: `optional_inner` / `sequence_elem` / `borrow_target` off `kind` instead of re-matching syntax, retiring `unfold::peel_ref` and taking `api/core/unfold.rs` off the boundary ledger (136 -> 135). `api/core/unfold.rs` now calls `reading` zero times; the sole consumer left is `convert_crossing`, whose key comes from `Registry::crossings()` and so always hits a cell. Review catch, addressed here: a value-form field spelled `Box>` peels to `T` and records an optional access step that drops the wrapper, so the emitter matches `Option`'s patterns against a value still typed `Box` (E0308). Verified pre-existing — byte-identical generated Rust on the base commit — so it is rejected at the declaration as a RESERVED shape naming #268, rather than silently mis-emitted. #269 records the reason nothing caught it: the suite asserts on generated text and never compiles it. Verified: 605 lib tests, `cargo test` and `--all --all-features`; regen byte-identical after a forced rebuild; covertest-kotlin 48/48 on the JVM; the issue's probe silent on a clean workspace build. --- prebindgen/src/api/core/flat/boundary.ledger | 3 +- prebindgen/src/api/core/flat/mod.rs | 8 +- prebindgen/src/api/core/registry/scan.rs | 32 ++-- prebindgen/src/api/core/registry/tests.rs | 39 +++++ prebindgen/src/api/core/unfold.rs | 49 +++--- prebindgen/src/api/lang/jnigen/jni/builder.rs | 139 +++++++++++------- .../src/api/lang/jnigen/jni/classify.rs | 15 +- .../api/lang/jnigen/jni/emit/flat_input.rs | 2 +- .../api/lang/jnigen/jni/emit/struct_out.rs | 2 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 2 +- .../api/lang/jnigen/jni/tests/value_form.rs | 103 +++++++++++++ 11 files changed, 281 insertions(+), 113 deletions(-) diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index c9441adf..558e9c96 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -46,7 +46,6 @@ 2 api/core/registry/scan.rs 10 api/core/types_util.rs -1 api/core/unfold.rs 8 api/lang/cbindgen/builder.rs 1 api/lang/cbindgen/convert.rs 5 api/lang/cbindgen/emit.rs @@ -72,4 +71,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 136 +# total: 135 diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index fb930bf8..44864f94 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -673,10 +673,14 @@ impl Flat { /// `origin.syntax` is reasoning from the spelling, which is what `origin` is /// not for. This exists for the one case with no element behind it: a type a /// build script declared, or one expansion composed. `ensure_entry` is its - /// only caller, and `classify_has_exactly_one_caller` keeps it that way. + /// only caller — the single call in the whole crate — and + /// `classify_has_no_caller_outside_the_registry` keeps it that way. /// /// Whoever asks is expected to keep the answer. The registry does: a reading is - /// taken once when a type-table cell is born, and lives in that cell. + /// taken once when a type-table cell is born, and lives in that cell — and + /// [`Registry::reading`](crate::api::core::registry::Registry::reading) hands + /// back only what is in one, so a second source of readings cannot reappear + /// here (#266). /// /// `Err` means the spelling is outside the accepted grammar — a real diagnosis /// about a type the *binding* built, not a cache miss. diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index f7804c33..2b0a023a 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -417,31 +417,31 @@ impl Registry { .entry = entry; } - /// The reading for `ty` — stored if the scan took one, lowered if it did not. + /// The reading the scan stored for `ty` — **a lookup, and only a lookup**. /// /// **The registry is the authority on what a type means**, because it is the /// thing that stores readings: `ensure_entry` asks the grammar once when a cell /// is born, and this hands that answer back. `Flat::classify` is its private - /// tool, and the two calls in this module are its only callers. + /// tool, and `ensure_entry` is its only caller. /// - /// The fallback is not the round trip this design forbids. That one is a - /// consumer holding an **element** — whose `ret` / `ty` is already a `TypeRef` - /// — reaching into `origin.syntax` and re-deriving what it was handed; the - /// signatures in `unfold` and `expand` now make it impossible. This is a type - /// the *binding* composed, with no element behind it and no cell yet: a value - /// form's field record naming a combination the scan never registered whole. - /// There is nothing to look up, so the grammar is asked — once, here, rather - /// than by each consumer. + /// This used to classify on a miss, which meant there were two sources of + /// readings and no way to tell them apart. The fallback fired constantly, and + /// on **scalars** — `i64`, `String`, `bool` — which are certainly registered by + /// the time a binding is built. That was the tell: the misses were not unknown + /// types but an inverted order, [`unfold`](crate::api::core::unfold) asking + /// about the leaves its caller registers one loop later. Because `classify` + /// answered correctly, nothing downstream was wrong and nothing showed it + /// (#266). The declarations now carry their own readings, so there is no such + /// caller left. + /// + /// `None` therefore means the type never entered the pipeline — a caller + /// asking out of order, not a cache miss to paper over. pub(crate) fn reading(&self, ty: &syn::Type) -> Option { let key = TypeKey::from_type(ty); - if let Some(cell) = self - .input_types + self.input_types .get(&key) .or_else(|| self.output_types.get(&key)) - { - return Some((*cell.subject).clone()); - } - self.flat.classify(ty).ok() + .map(|cell| (*cell.subject).clone()) } /// Register `ty` (and its nested positions) as a required **input** so diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index bb1581ff..4dbf494d 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -797,6 +797,45 @@ fn a_source_type_cell_carries_the_models_typeref() { assert!(matches!(inner.subject.kind, TypeKind::Scalar(_))); } +/// `Registry::reading` is a **lookup**. A type with no cell answers `None`, even +/// when the grammar would classify it happily. +/// +/// It used to fall back to `Flat::classify`, and the failure that hid is the one +/// this pins: `i64` is not an exotic spelling, it is a scalar every binding +/// registers. A miss on one could only mean the caller asked *before* +/// registration — which is exactly what the value-form walk was doing, for every +/// leaf its caller registered one loop later (#266). Because `classify` returned +/// the right answer, the ordering bug produced correct output and no signal at +/// all. +/// +/// This cannot be caught by `classify_has_no_caller_outside_the_registry`: that +/// scan excludes `core/registry/` by design, since the registry is where +/// `classify` legitimately lives. The second door was inside the room. +#[test] +fn reading_is_a_lookup_not_a_classification() { + let items = vec![fn_item("fn f(x: u64) -> u64 { x }")]; + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); + let mut ext = StubExt::default(); + ext.functions.insert(syn::parse_str("f").unwrap()); + let reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .unwrap(); + + // Registered by the declared fn — the lookup hits. + assert!( + reg.reading(&syn::parse_quote!(u64)).is_some(), + "a type the scan registered has its reading in a cell" + ); + // Never registered, and perfectly expressible. The grammar's answer is not + // this method's to give. + assert!( + reg.reading(&syn::parse_quote!(i64)).is_none(), + "`reading` answers from the type table; it must not classify on a miss" + ); +} + /// A type only the binding authored is **classified but placeless**: it has a /// reading, because it is a type in this language, and no location, because no /// source wrote it. Declaring a type the source never mentions is the ordinary diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index dd291607..9bc6698a 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -105,8 +105,17 @@ pub struct FieldRecord { pub members: Vec, /// The leaf name (already `__`-joined across inlined nesting). pub name: String, - /// The field's type as written, `Option` / `Vec` layers included. - pub ty: syn::Type, + /// The field's **reading**, `Option` / `Vec` layers included — and its + /// syntax with it, which is what a leaf's `out_ty` spells. + /// + /// The declaration carries this rather than naming a `syn::Type` for the + /// walk to look up, because there was nothing to look up: a field record is + /// built from an element whose every field already has a reading, and the + /// types it names are the ones the caller registers *after* the walk + /// returns. Asking the registry here was asking before registration + /// (#266) — a lookup that could only miss, answered by a second source of + /// readings that hid the ordering. + pub ty: crate::api::core::flat::TypeRef, /// How this field decomposes. pub decon: FieldDecon, } @@ -935,14 +944,6 @@ fn peel_borrow(ty: &crate::api::core::flat::TypeRef) -> (bool, &crate::api::core } } -/// Strip a single leading `&` (one level) from a type. -pub(crate) fn peel_ref(ty: &syn::Type) -> syn::Type { - match ty { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other.clone(), - } -} - /// True when `ret` is `T` / `&T` / `Option` / `Vec` with /// `T == key` — the default-output match. `Result<_, _>` is NOT peeled, so a /// fallible factory (`-> Result`) keeps its handle return; the error @@ -1460,18 +1461,17 @@ fn flatten( }); for fr in fields { - // A field record is adapter-declared, so it has no element — - // but the scan took its reading when the cell was born, and - // that is what this reads. Nothing is re-classified. - let fr_reading = registry.reading(&fr.ty); + // The declaration carries the field's reading, so nothing is + // looked up and nothing is re-classified. + // // A field's own `Option` makes everything under it nullable, // exactly as an `Option`-returning accessor step does. - let (opt, core) = match fr_reading.as_ref().and_then(|r| r.optional_inner()) { - Some(inner) => (true, inner.origin.syntax.clone()), - None => (false, fr.ty.clone()), + let (opt, core) = match fr.ty.optional_inner() { + Some(inner) => (true, inner), + None => (false, &fr.ty), }; - let child_ty = peel_ref(&core); - let child_key = TypeKey::from_type(&child_ty); + let child_ty = core.borrow_target().unwrap_or(core); + let child_key = child_ty.key(); // Same three-way choice a `.field()` record makes: declared // override, else the field type's own deconstructor, else @@ -1535,13 +1535,8 @@ fn flatten( acc, registry, &child_records, - // Again the registry's answer, not a new one. - ®istry.reading(&child_ty).ok_or_else(|| { - UnfoldError::Unsupported { - func: func.clone(), - reason: "a field type the language cannot express", - } - })?, + // The declaration's reading, peeled — not a new one. + child_ty, &field_path, &seg_name(&fr.name), by_ref, @@ -1559,7 +1554,7 @@ fn flatten( leaves.push(UnfoldLeaf { name: seg_name(&fr.name).join("__"), path: field_path, - out_ty: fr.ty.clone(), + out_ty: fr.ty.origin.syntax.clone(), identity: false, nullable, source: LeafSource::Field, diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index e531738a..ef57364c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -838,33 +838,31 @@ impl Declarations { decl: &FieldsDecl, ) -> Vec { let func = &decl.func; - let item_fn = registry - .flat() - .function(&func) - .map(|func| &func.origin.syntax) - .unwrap_or_else(|| { - panic!( - "expand_return!({}).fields(fields!({func})): no `#[prebindgen]` function \ + let accessor = registry.flat().function(&func).unwrap_or_else(|| { + panic!( + "expand_return!({}).fields(fields!({func})): no `#[prebindgen]` function \ `{func}` — a value form is an accessor `fn {func}(v: &{}) -> {}Struct`", - key.as_str(), - key.as_str(), - key.as_str(), - ) - }); - let ret: syn::Type = match &item_fn.sig.output { - syn::ReturnType::Type(_, t) => crate::api::core::unfold::peel_ref(t), - syn::ReturnType::Default => panic!( - "expand_return!({}).fields(fields!({func})): `{func}` returns nothing — a \ - value form returns the struct holding this type's fields", - key.as_str() - ), - }; - let TypeKind::DataStruct { st, .. } = self.type_kind(registry, &ret) else { + key.as_str(), + key.as_str(), + key.as_str(), + ) + }); + // The accessor's return as the model read it, peeled of a leading `&`. + // An elided return and a written `-> ()` are one thing here, because the + // model already normalized them. + let ret = accessor.ret.borrow_target().unwrap_or(&accessor.ret); + assert!( + !matches!(ret.kind, crate::api::core::flat::TypeKind::Unit), + "expand_return!({}).fields(fields!({func})): `{func}` returns nothing — a \ + value form returns the struct holding this type's fields", + key.as_str(), + ); + let TypeKind::DataStruct { st, .. } = self.type_kind(registry, &ret.origin.syntax) else { panic!( "expand_return!({}).fields(fields!({func})): `{func}` returns `{}`, which is \ not a struct — a value form returns a struct whose fields become the leaves", key.as_str(), - ret.to_token_stream(), + ret.origin.syntax.to_token_stream(), ) }; let st = st.clone(); @@ -890,7 +888,7 @@ impl Declarations { named.contains(field), "fields!({func}).field(\"{field}\", ...): `{}` has no field `{field}` \ (fields: {})", - st.ident, + st.name, named.iter().cloned().collect::>().join(", "), ); } @@ -899,7 +897,7 @@ impl Declarations { named.contains(field), "fields!({func}).name(\"{field}\", ...): `{}` has no field `{field}` \ (fields: {})", - st.ident, + st.name, named.iter().cloned().collect::>().join(", "), ); } @@ -916,22 +914,13 @@ impl Declarations { registry: &impl Conversions, key: &TypeKey, decl: &FieldsDecl, - st: &syn::ItemStruct, + st: &crate::api::core::flat::Struct, members: &[syn::Ident], name_prefix: &str, depth: usize, out: &mut Vec, ) { use crate::api::core::unfold::{FieldDecon, FieldRecord}; - let syn::Fields::Named(named) = &st.fields else { - panic!( - "expand_return!({}).fields(fields!({})): `{}` has no named fields — a value \ - form is a plain struct whose fields become the leaves", - key.as_str(), - decl.func, - st.ident, - ) - }; // A value form holding itself would expand forever; the cycle rule for // everything reachable BELOW a field is core's `visited` check. assert!( @@ -940,10 +929,13 @@ impl Declarations { deep — is a value form holding itself?", key.as_str(), decl.func, - st.ident, + st.name, ); - for field in &named.named { - let Some(fname) = field.ident.as_ref() else { + // A tuple struct is an `Extern` rather than a `Struct`, so it never + // reaches here — `lower_value_form`'s "not a struct" diagnosis catches + // it at the return type, which is where the author wrote it. + for field in &st.fields { + let Some(fname) = field.name.as_ref() else { continue; }; let mut member_path = members.to_vec(); @@ -966,6 +958,38 @@ impl Declarations { format!("{name_prefix}__{name}") }; + // An `Option` the READING sees but the SYNTAX still wraps — `Box< + // Option>`. `Box` *is* `T` in the model, deliberately, so the + // peel below reaches the `T` and records an optional access step. + // That step carries the field name and the optional flag and + // nothing else: the wrapper is dropped, and the emitter applies + // `Option`'s match to a value whose Rust type still spells `Box`, + // which does not compile (match ergonomics does not deref a `Box`). + // + // Rejected here, at the declaration, rather than emitted and + // discovered by rustc. This is a RESERVED shape, not a refused one + // — see #268, which owns teaching the path algebra to deref a + // transparent wrapper. + assert!( + field.ty.optional_inner().is_none() + || option_inner_type(&field.ty.origin.syntax).is_some(), + "expand_return!({}).fields(fields!({})): field `{}.{dotted}` is `{}` — an \ + `Option` behind a transparent wrapper. The wrapper is invisible to the \ + model and load-bearing in the generated Rust, and the leaf access path \ + cannot yet say `deref, then match` (reserved — see \ + https://github.com/milyin/prebindgen/issues/268). Spell the field \ + `Option<{}>`, or override it with .field(\"{dotted}\", ...)", + key.as_str(), + decl.func, + st.name, + field.ty.origin.syntax.to_token_stream(), + field + .ty + .optional_inner() + .map(|i| i.origin.syntax.to_token_stream().to_string()) + .unwrap_or_default(), + ); + // An explicit override replaces the field type's default // decomposition wholesale — including any nesting it would have had. if let Some((_, ovr)) = decl.overrides.iter().find(|(f, _)| *f == dotted) { @@ -980,17 +1004,16 @@ impl Declarations { // Mirror that exact normalization here; peeling `Vec` would // accept `expand_return!(T)` and only fail later when core // applies its records to `Vec`. - let peeled = option_inner_type(&field.ty) - .map(|t| crate::api::core::unfold::peel_ref(&t)) - .unwrap_or_else(|| crate::api::core::unfold::peel_ref(&field.ty)); - let actual = TypeKey::from_type(&peeled); + let under_opt = field.ty.optional_inner().unwrap_or(&field.ty); + let peeled = under_opt.borrow_target().unwrap_or(under_opt); + let actual = peeled.key(); assert!( actual == ovr.key, "fields!({}).field(\"{dotted}\", expand_return!({})): `{}.{dotted}` is \ `{}`, not `{}` — a per-field override names the field's own type", decl.func, ovr.key.as_str(), - st.ident, + st.name, actual.as_str(), ovr.key.as_str(), ); @@ -1008,12 +1031,12 @@ impl Declarations { // object (the rule `synth_value_struct_leaves` already follows). // A `sealed_class!` field has no whole-value converter at all, so it // must decompose into its selector and groups wherever it appears. - let bare = option_inner_type(&field.ty).unwrap_or_else(|| field.ty.clone()); - let probe = vec_inner_type(&bare).unwrap_or_else(|| bare.clone()); - match self.type_kind(registry, &probe) { + let bare = field.ty.optional_inner().unwrap_or(&field.ty); + let probe = bare.sequence_elem().unwrap_or(bare); + match self.type_kind(registry, &probe.origin.syntax) { TypeKind::DataStruct { st, cfg: Some(_) } - if option_inner_type(&field.ty).is_none() - && vec_inner_type(&field.ty).is_none() => + if field.ty.optional_inner().is_none() + && field.ty.sequence_elem().is_none() => { let child = st.clone(); self.walk_value_form( @@ -1036,18 +1059,18 @@ impl Declarations { // (the `fromParts` bridge's `PlanFieldKind::Sum` does — a // data-class field can be `Option`). assert!( - vec_inner_type(&bare).is_none(), + bare.sequence_elem().is_none(), "expand_return!({}).fields(fields!({})): field `{}.{}` is a \ `Vec<{}>` — a sequence of tag-gated groups has variable arity and \ cannot be laid out in a fixed leaf list", key.as_str(), decl.func, - st.ident, + st.name, dotted, - probe.to_token_stream(), + probe.origin.syntax.to_token_stream(), ); assert!( - option_inner_type(&field.ty).is_none(), + field.ty.optional_inner().is_none(), "expand_return!({}).fields(fields!({})): field `{}.{}` is an \ `Option<{}>` — an optional sum would need a present flag beside its \ tag, which an output leaf list cannot carry. Give the field a \ @@ -1055,17 +1078,21 @@ impl Declarations { or override it with .field(\"{}\", ...)", key.as_str(), decl.func, - st.ident, + st.name, dotted, - probe.to_token_stream(), + probe.origin.syntax.to_token_stream(), dotted, ); - let ident = bare_path_ident(&probe).expect("a sum type is a path type"); + // The name is the reading's, not a path taken apart to + // re-derive one. + let crate::api::core::flat::TypeKind::Named { id } = &probe.kind else { + panic!("a sum type is a named type") + }; let item_enum = registry .flat() - .enum_item(&ident) + .enum_item(&id.name) .expect("TypeKind::Sum implies an indexed enum"); - let sum_cfg = self.types[&TypeKey::from_type(&probe)] + let sum_cfg = self.types[&probe.key()] .sum() .expect("TypeKind::Sum implies a sealed-class config"); out.push(FieldRecord { diff --git a/prebindgen/src/api/lang/jnigen/jni/classify.rs b/prebindgen/src/api/lang/jnigen/jni/classify.rs index 3a7e69d3..e1aead89 100644 --- a/prebindgen/src/api/lang/jnigen/jni/classify.rs +++ b/prebindgen/src/api/lang/jnigen/jni/classify.rs @@ -25,8 +25,13 @@ pub(crate) enum TypeKind<'r, 'c> { Sum, /// A `#[prebindgen]` struct from the source crate that is none of the /// special kinds; flattens field-by-field when emitters support it. + /// + /// The **element**, not its `syn::ItemStruct`. A flattening emitter wants + /// each field's reading, and the model already decided one per field; going + /// through the syntax means asking some other authority for it again. An + /// emitter that only re-emits the struct reads `st.origin.syntax`. DataStruct { - st: &'r syn::ItemStruct, + st: &'r crate::api::core::flat::Struct, cfg: Option<&'c TypeConfig>, }, /// Scalars, `String`, undeclared / non-path types. @@ -59,16 +64,12 @@ impl Declarations { DeclaredKind::Sealed(_) => return TypeKind::Sum, // A data class is exactly a declared source struct — fall // through to the registry probe below, which supplies the - // `syn::ItemStruct` its emitters flatten. + // element its emitters flatten. DeclaredKind::Data => {} } } if let Some(name) = bare_path_ident(bare) { - if let Some(st) = registry - .flat() - .struct_type(&name) - .map(|st| &st.origin.syntax) - { + if let Some(st) = registry.flat().struct_type(&name) { return TypeKind::DataStruct { st, cfg }; } } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index cb09c501..bc41ce08 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -1287,7 +1287,7 @@ fn build_flat_struct_node( let node = build_flat_struct_node( ext, registry, - child, + &child.origin.syntax, child_optional, &child_native, &field_ref, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index 908dfa47..f6cf835b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -127,7 +127,7 @@ pub(crate) fn synth_value_struct_leaves( // converter for it, failing the resolve with the sum named rather // than the unsupported position. TypeKind::Handle | TypeKind::Enum | TypeKind::Sum => return None, - TypeKind::DataStruct { st, cfg: Some(_) } => Some(st.clone()), + TypeKind::DataStruct { st, cfg: Some(_) } => Some(st.origin.syntax.clone()), _ => None, }; if let Some(child) = nested { diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 1ccde249..15557e89 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -300,7 +300,7 @@ pub(crate) fn classify_field( let child_fqn = cfg .and_then(|c| c.name_spec.as_ref()) .map(|s| ext.fqn_of(s)); - let plan = build_struct_plan(ext, registry, &st.clone(), depth + 1)?; + let plan = build_struct_plan(ext, registry, &st.origin.syntax, depth + 1)?; return Some(PlanFieldKind::Nested { optional: option_inner_type(&effective_ty).is_some(), child_fqn, diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index 255371c0..dcacc8e3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -531,6 +531,109 @@ fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { } } +/// An `Option` the reading sees through a transparent wrapper is **reserved**, +/// and says so by name. +/// +/// `Box` *is* `T` in the model, deliberately — so a field spelled +/// `Box>` peels to `Child` and records an optional access step +/// that has no way to say "deref first". The emitter then matches `Option`'s +/// patterns against a value still typed `Box>`, which is `E0308`. +/// +/// The shape used to be *accepted* and mis-emitted: nothing in this suite +/// compiles the generated Rust, so `contains` assertions passed over it happily. +/// Rejecting at the declaration is what makes it visible — see #268, which owns +/// teaching the path algebra the wrapper. +/// +/// The plain `Option` case is asserted alongside, because a guard that +/// also rejected the ordinary spelling would be worse than no guard. +#[test] +fn an_option_behind_a_transparent_wrapper_is_reserved_by_name() { + let loc = myflat_loc(); + let build = |field_ty: syn::Type| { + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZSampleStruct { + pub kex: #field_ty, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_to_struct(s: &ZSample) -> ZSampleStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_keyexpr_as_str(k: &ZKeyExpr) -> &str { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_sub(cb: impl Fn(ZSample) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .class(crate::ptr_class!(ZKeyExpr)) + .fun(crate::fun!(z_sample_sub)), + ) + .expand(crate::expand_return!(ZKeyExpr).field(crate::fun!(z_keyexpr_as_str))) + .expand(crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_struct))); + let dir = unique_test_dir("jnigen_vf_boxed_opt"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let _ = jni + .build_with(registry) + .map(|g| g.write_rust(dir.join("g.rs"))); + }; + + let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + build(syn::parse_quote!(Box>)) + })) + .expect_err("an Option behind a transparent wrapper must be rejected"); + let msg = err + .downcast_ref::() + .cloned() + .or_else(|| err.downcast_ref::<&str>().map(|s| s.to_string())) + .unwrap_or_default(); + assert!( + msg.contains("transparent wrapper"), + "the message names the shape: {msg}" + ); + assert!( + msg.contains("ZSampleStruct.kex"), + "the message names the offending field: {msg}" + ); + // Reserved, not refused: the diagnosis says where the work is tracked. + assert!( + msg.contains("issues/268"), + "a reserved shape names its issue: {msg}" + ); + + // The ordinary spelling is untouched. + std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + build(syn::parse_quote!(Option)) + })) + .expect("a plain `Option` field is the supported shape"); +} + /// Naming a field the value form does not have is the very drift this /// declarator exists to catch, so it is an error rather than a silent no-op. #[test] From 520c9894108bd4a873f7ebe50dc491c3aaac8444 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 18:29:56 +0200 Subject: [PATCH 24/52] Don't assume how Rust spells an optional (#271) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #268. A value-form field spelled `Box>` emitted a match against a place still typed `&Box>` — E0308. The model is not at fault, and the obvious fix would have made it so. `kind` states the destination-language invariant: it does not care whether a reference is `&T` or `Cow`, a sequence `[T]` or `Vec`, an optional `Option` or `Box>`. Recording a wrapper depth on `TypeRef` would push a Rust representation detail back into the one thing built to abstract it away. The side INTERPRETING the model is the side that must accept any representation. So this is `classify off kind, spell off syntax` broken in the direction nothing was watching: the emitter classified off `kind` correctly — optional needs a Some/None split — and then spelled off `kind` too. The fix costs nothing. A type-ascribed `let` is a coercion site, and deref coercion is transitive AND a no-op when the types already match, so `let o: &Option<_> = ;` serves every representation and leaves the plain spelling behaving identically. Applied where the destructured expression is a BORROW. An owned position keeps its direct match and is documented as doing so: deref coercion applies to references, and moving a payload out of a wrapper is something only some of them permit (`Box` can, `Rc` cannot). `fold_steps` composes a field read to a reference and an accessor call to an owned value, so the two are told apart by `PathStep::is_field` — borrowing an accessor's return would have changed ownership downstream, which the existing tests caught. Three source-place destructures found and coerced: `reach_leaf`, and both present-flag splits in `struct_out`. Everything else emitting `Option::Some` is CONSTRUCTION — a value whose type the emitter owns. A committed census of destructuring sites keeps a new one from appearing silently. It walks tokens rather than text, so it cannot be evaded by spelling the pattern differently, and it is checked against the directory listing, so a new emitter module cannot sit outside it. Both properties proved by violating them. It counts patterns, not constructions, and cannot tell a coerced destructure from a raw one — a still count is not proof, only a new site is forced through review. Two review catches, both real: `struct_out` had a second destructure spelled bare `Some(..)` in live covertest code that the first census could not see, and `convert.rs` sat outside the census entirely with two more. Goldens move by exactly the three coercion sites. covertest 48/48 on the JVM. Known gap, tracked by #269: nothing here compiles the generated Rust — the reason this bug survived. Split out as #270: converter identity keys on the spelling, so a non-decomposed `Box>` still fails to resolve. --- .../src/generated_bindings.rs | 13 +- prebindgen/src/api/lang/jnigen/jni/builder.rs | 32 --- .../src/api/lang/jnigen/jni/emit/delivery.rs | 58 ++++- .../src/api/lang/jnigen/jni/emit/mod.rs | 228 ++++++++++++++++++ .../api/lang/jnigen/jni/emit/struct_out.rs | 20 +- .../api/lang/jnigen/jni/tests/value_form.rs | 94 ++++---- 6 files changed, 356 insertions(+), 89 deletions(-) diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index 0ae34291..5d2b9aab 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -418,8 +418,9 @@ pub(crate) unsafe fn Annotated_to_JObject_b543f0d9<'a>( let ___alternate_o2: jni::sys::jdouble; let ___alternate_o3: jni::sys::jboolean; let ___alternate_o4: jni::objects::JObject; - match &v.alternate { - Some(__c0) => { + let __on0: &::core::option::Option<_> = &v.alternate; + match __on0 { + ::core::option::Option::Some(__c0) => { let ___alternate_id: jni::sys::jlong = i64_to_jlong_fbf9a9bc( env, __c0.id.clone(), @@ -448,7 +449,7 @@ pub(crate) unsafe fn Annotated_to_JObject_b543f0d9<'a>( ___alternate_o3 = ___alternate_flag; ___alternate_o4 = ___alternate_label; } - None => { + ::core::option::Option::None => { ___alternate_present = 0u8; ___alternate_o0 = 0i64; ___alternate_o1 = 0i32; @@ -836,7 +837,8 @@ pub(crate) unsafe fn HoldPolicy_to_JObject_d2a5bcc4<'a>( let ___grace_present: jni::sys::jboolean; let ___grace__tag: jni::sys::jint; let ___grace_g0: jni::sys::jlong; - match &v.grace { + let __oc0: &::core::option::Option<_> = &v.grace; + match __oc0 { ::core::option::Option::Some(__o0) => { ___grace_present = 1u8; match __o0 { @@ -7444,7 +7446,8 @@ pub(crate) unsafe fn Observation_to_JObject_435b0724<'a>( let ___fallback_g3: jni::objects::JObject; let ___fallback_g4: jni::sys::jint; let ___fallback_g5: jni::sys::jlong; - match &v.fallback { + let __oc0: &::core::option::Option<_> = &v.fallback; + match __oc0 { ::core::option::Option::Some(__o0) => { ___fallback_present = 1u8; match __o0 { diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index ef57364c..a91b755c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -958,38 +958,6 @@ impl Declarations { format!("{name_prefix}__{name}") }; - // An `Option` the READING sees but the SYNTAX still wraps — `Box< - // Option>`. `Box` *is* `T` in the model, deliberately, so the - // peel below reaches the `T` and records an optional access step. - // That step carries the field name and the optional flag and - // nothing else: the wrapper is dropped, and the emitter applies - // `Option`'s match to a value whose Rust type still spells `Box`, - // which does not compile (match ergonomics does not deref a `Box`). - // - // Rejected here, at the declaration, rather than emitted and - // discovered by rustc. This is a RESERVED shape, not a refused one - // — see #268, which owns teaching the path algebra to deref a - // transparent wrapper. - assert!( - field.ty.optional_inner().is_none() - || option_inner_type(&field.ty.origin.syntax).is_some(), - "expand_return!({}).fields(fields!({})): field `{}.{dotted}` is `{}` — an \ - `Option` behind a transparent wrapper. The wrapper is invisible to the \ - model and load-bearing in the generated Rust, and the leaf access path \ - cannot yet say `deref, then match` (reserved — see \ - https://github.com/milyin/prebindgen/issues/268). Spell the field \ - `Option<{}>`, or override it with .field(\"{dotted}\", ...)", - key.as_str(), - decl.func, - st.name, - field.ty.origin.syntax.to_token_stream(), - field - .ty - .optional_inner() - .map(|i| i.origin.syntax.to_token_stream().to_string()) - .unwrap_or_default(), - ); - // An explicit override replaces the field type's default // decomposition wholesale — including any nesting it would have had. if let Some((_, ovr)) = decl.overrides.iter().find(|(f, _)| *f == dotted) { diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index 4f8f1924..7c181019 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -722,6 +722,33 @@ pub(crate) fn bind_hoists( out } +/// Bind `e` so it can be destructured as an `Option` **whatever Rust +/// representation the source used for it**. +/// +/// `kind` says a position is optional; it deliberately does not say whether +/// Rust spells that `Option`, `Box>`, or something else — the flat +/// model states the destination-language invariant, and the side interpreting +/// it is the side that must accept any representation. Matching the reached +/// place directly assumed one, which is `classify off kind, spell off syntax` +/// broken in the direction nothing was watching: the classification was right +/// and the *spelling* came from it too. `Box>` then produced +/// `match &place { Some(..) => .. }` and `E0308` (#268). +/// +/// A type-ascribed `let` is a coercion site, and deref coercion is transitive +/// **and** a no-op when the types already match — so this one shape serves +/// every representation, and the plain `Option` case is unchanged in +/// behaviour. The payload stays `_`: what it is, is the source's business. +/// +/// `e` is expected to be a **reference** already — [`compose_step`] composes a +/// field read as `&(e).f` — so nothing is borrowed here. Borrowing only: an +/// owned position cannot be made representation-agnostic this way, because +/// deref coercion applies to references and moving out of a wrapper is +/// something only some of them permit (`Box` does, `Rc` cannot). A site that +/// must MOVE the payload keeps its direct match; see `owned_place` below. +pub(crate) fn bind_as_option(e: &TokenStream, bind: &syn::Ident) -> TokenStream { + quote! { let #bind: &::core::option::Option<_> = #e; } +} + /// Reach a leaf's input by folding its `path` over `base`, then hand the /// reached expression to `body` (which renders the encode and yields /// `JObject`). Every optional nesting step becomes a `match`: its `None` arm @@ -765,10 +792,33 @@ fn reach_leaf( depth + 1, body, ); - quote! { - match #opt_e { - ::core::option::Option::Some(#nested) => { #inner } - ::core::option::Option::None => jni::objects::JObject::null(), + // A FIELD read composes to a borrow (`&(e).f`), so it goes through + // a coercion site and the destructuring stops caring which + // representation the source spelled the optional as. + // + // A CALL composes to the accessor's own returned value, which is + // owned and whose payload downstream may move. Borrowing it to + // coerce would change that ownership, so it keeps its direct match + // — and an owned position could not be made representation-agnostic + // this way regardless (see [`bind_as_option`]). + if path[k].is_field() { + let opt_bind = format_ident!("__o{}", depth); + let coerce = bind_as_option(&opt_e, &opt_bind); + quote! { + { + #coerce + match #opt_bind { + ::core::option::Option::Some(#nested) => { #inner } + ::core::option::Option::None => jni::objects::JObject::null(), + } + } + } + } else { + quote! { + match #opt_e { + ::core::option::Option::Some(#nested) => { #inner } + ::core::option::Option::None => jni::objects::JObject::null(), + } } } } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs b/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs index 15e6e2f3..5392477e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs @@ -28,3 +28,231 @@ pub(crate) use struct_out::*; pub(crate) use sum_out::*; pub(crate) use vec_build::*; pub(crate) use wrapper::*; + +#[cfg(test)] +mod destructure_ledger { + //! A committed census of every place this adapter **destructures an + //! `Option` in emitted Rust**. + //! + //! `kind` says a position is optional. It deliberately does not say whether + //! Rust spells that `Option`, `Box>`, `Cow<'_, Option>` or + //! anything else — the flat model states the destination-language + //! invariant, and the side reading it is the side that must accept any + //! representation. So an emitter may classify off `kind`, but it must not + //! *spell* off it, and matching a reached place against `Option`'s patterns + //! does exactly that. + //! + //! That is how #268 happened: `match &place { Some(..) => .. }` is `E0308` + //! the moment the source spells the field `Box>`, and no test + //! could see it because this suite asserts on generated text and never + //! compiles it. The fix is [`bind_as_option`] — a type-ascribed `let` is a + //! coercion site, deref coercion is transitive and a no-op when the types + //! already match, so one shape serves every representation. + //! + //! ## Why this walks tokens + //! + //! The first version of this check matched the text `option::Option::Some(` + //! and was wrong in the way that matters: `struct_out.rs` emitted a second + //! destructure spelled bare `Some(#cbind) =>`, and the census could not see + //! it. That site was a real unfixed instance of the very bug — an + //! unqualified match on a source place — so the guard's blind spot was + //! exactly the bug's hiding place. A guard that can be evaded by choosing a + //! different spelling of the same pattern is not a guard. + //! + //! So the scan parses each file's tokens, descends only into `quote!` + //! bodies — which is what "emitted Rust" means, and keeps the emitter's own + //! `if let Some(x)` out of the count — and recognizes a `Some ( … ) =>` + //! pattern whatever path prefix it carries. + //! + //! ## What it counts, and what it cannot + //! + //! Only **patterns**, never constructions. Building an `Option` the emitter + //! itself owns says nothing about the source's representation and is always + //! safe, which is why `flat_input.rs` and `wrapper.rs` sit at zero despite + //! emitting `Option::Some` freely. + //! + //! It cannot tell a *coerced* destructure from a raw one, so a count that + //! does not move is not proof of correctness. Its job is to make a NEW + //! destructure impossible to add silently: the number moves, and review has + //! to say which of the three it is — + //! + //! * destructuring a coerced binding (fine — that is the fix), + //! * destructuring a value the emitter itself bound (fine — the emitter + //! owns its type), + //! * destructuring a place read from the source (**the bug**; route it + //! through [`bind_as_option`]). + //! + //! Owned positions are the standing exception: deref coercion applies to + //! references, and moving a payload out of a wrapper is something only some + //! of them permit (`Box` can, `Rc` cannot), so a site that must MOVE keeps + //! its direct match and is correct only for representations that allow it. + //! + //! ## Coverage + //! + //! The table is checked against the **directory listing**, not kept by + //! hand. A hand-listed census had already let `convert.rs` sit outside the + //! guard entirely with two uncounted destructures, and would have let any + //! new emitter module do the same — "a new destructure cannot be added + //! silently" is only true if a new *file* cannot be either. + //! + //! To change it deliberately: update the table below in the same commit, + //! and say in review which category the new site is. + + use proc_macro2::{Delimiter, TokenStream, TokenTree}; + + /// `(file, destructuring-pattern count)` — **every** `.rs` in this + /// directory, checked against the directory listing so a new emitter module + /// cannot sit outside the census. + const LEDGER: &[(&str, usize)] = &[ + ("callback.rs", 0), + // The `Option` output converters' niche and boxed-primitive arms. + // They destructure `v`, the converter's own parameter — whose Rust type + // is the crossing's SPELLING. Correct today only because a wrapped + // spelling gets no converter at all (#270); if that is fixed and + // `Box>` becomes a crossing, these two need coercing. + ("convert.rs", 2), + // 2 fn-return matches (owned), 2 leaf reaches (one coerced, one owned + // accessor return), 1 owned identity move, 1 emitter-bound local. + ("delivery.rs", 6), + ("flat_input.rs", 0), + // This file. The spellings in `the_census_is_spelling_independent` are + // string literals, which tokenize as one `Literal` and are never walked. + ("mod.rs", 0), + ("names.rs", 0), + // Both coerced: the `Option` present-flag split, and the nested + // plan's present-flag split. + ("struct_out.rs", 2), + ("sum_out.rs", 0), + ("vec_build.rs", 0), + ("wrapper.rs", 0), + ]; + + /// Count `Some ( … ) =>` patterns inside `quote!` / `parse_quote!` bodies. + /// + /// `in_quote` is what keeps the emitter's own `if let Some(..)` out of the + /// count: only tokens below one of those macros are emitted Rust. + fn count(ts: TokenStream, in_quote: bool, n: &mut usize) { + let toks: Vec = ts.into_iter().collect(); + let mut i = 0; + while i < toks.len() { + if let TokenTree::Ident(id) = &toks[i] { + let bang = + matches!(toks.get(i + 1), Some(TokenTree::Punct(p)) if p.as_char() == '!'); + if (id == "quote" || id == "parse_quote") && bang { + if let Some(TokenTree::Group(g)) = toks.get(i + 2) { + count(g.stream(), true, n); + i += 3; + continue; + } + } + // A `Some( … ) =>` arm. The path in front is not inspected, so + // `Some`, `Option::Some` and `::core::option::Option::Some` all + // land here — the spelling is what the first version of this + // check wrongly keyed on. + if in_quote && id == "Some" { + let parens = matches!( + toks.get(i + 1), + Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis + ); + // `=>` is two Puncts: '=' joint, then '>'. + let fat_arrow = matches!(toks.get(i + 2), Some(TokenTree::Punct(p)) if p.as_char() == '=') + && matches!(toks.get(i + 3), Some(TokenTree::Punct(p)) if p.as_char() == '>'); + if parens && fat_arrow { + *n += 1; + } + } + } + if let TokenTree::Group(g) = &toks[i] { + count(g.stream(), in_quote, n); + } + i += 1; + } + } + + #[test] + fn option_destructuring_sites_are_accounted_for() { + let dir = std::path::Path::new(concat!( + env!("CARGO_MANIFEST_DIR"), + "/src/api/lang/jnigen/jni/emit" + )); + + // The census is over the DIRECTORY, not a hand-kept list: a new emitter + // module that hand-listing would have let slip past the guard entirely + // shows up here as an unlisted file. + let mut on_disk: Vec = std::fs::read_dir(dir) + .expect("emit dir") + .map(|e| e.expect("dir entry").path()) + .filter(|p| p.extension().is_some_and(|x| x == "rs")) + .map(|p| { + p.file_name() + .expect("file name") + .to_string_lossy() + .into_owned() + }) + .collect(); + on_disk.sort(); + let mut listed: Vec = LEDGER.iter().map(|(f, _)| (*f).to_string()).collect(); + listed.sort(); + assert_eq!( + listed, on_disk, + "the destructure census must cover every emitter file — add the new module to LEDGER with its count (see this module's docs for what counts)" + ); + + let mut drift: Vec = Vec::new(); + for (file, expected) in LEDGER { + let src = std::fs::read_to_string(dir.join(file)) + .unwrap_or_else(|e| panic!("read {file}: {e}")); + let ts: TokenStream = src + .parse() + .unwrap_or_else(|e| panic!("tokenize {file}: {e}")); + let mut found = 0usize; + count(ts, false, &mut found); + if found != *expected { + drift.push(format!(" {file}: {expected} -> {found}")); + } + } + assert!( + drift.is_empty(), + "OPTION-DESTRUCTURING LEDGER DRIFT:\n{}\n\n\ + An emitter must not assume how Rust spells an optional — see this \ + module's docs. If the new site destructures a place read from the \ + source, route it through `bind_as_option`; if it is a coerced \ + binding, an emitter-owned value, or an owned move, update the \ + table and say which in review.", + drift.join("\n"), + ); + } + + /// The guard catches a destructure **however it is spelled** — the failure + /// the first version had, and the reason this one walks tokens. + /// + /// A ledger that has never been seen to fail is not evidence of anything, + /// so this proves it on both spellings rather than asserting it in prose. + #[test] + fn the_census_is_spelling_independent() { + let one = |body: &str| { + let src = format!("fn f() {{ quote! {{ match x {{ {body} }} }}; }}"); + let mut n = 0usize; + count(src.parse().expect("tokenize"), false, &mut n); + n + }; + for spelling in [ + "Some(v) => {}", + "Option::Some(v) => {}", + "::core::option::Option::Some(v) => {}", + ] { + assert_eq!(one(spelling), 1, "not counted: {spelling}"); + } + // A construction is not a destructure, and the emitter's own control + // flow is not emitted Rust. + let mut n = 0usize; + count( + "fn f() { if let Some(x) = y { quote! { Some(#x) }; } }" + .parse() + .expect("tokenize"), + false, + &mut n, + ); + assert_eq!(n, 0, "constructions and host-code matches must not count"); + } +} diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index f6cf835b..c8a329e0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -334,16 +334,22 @@ fn encode_field( child_slots.iter().map(|sl| sl.ident.clone()).collect(); let defaults: Vec = child_slots.iter().map(|sl| sl.default.clone()).collect(); + // Destructured through a coercion site: `kind` says this + // field is optional, and how Rust spells that is the + // source's business (#268). + let obind = format_ident!("__on{}", depth); + let coerce = bind_as_option("e!(&#value), &obind); preludes.extend(quote! { let #flag_id: jni::sys::jboolean; #( let #outer_ids: #outer_tys; )* - match &#value { - Some(#cbind) => { + #coerce + match #obind { + ::core::option::Option::Some(#cbind) => { #child_pre #flag_id = 1u8; #( #outer_ids = #inner_ids; )* } - None => { + ::core::option::Option::None => { #flag_id = 0u8; #( #outer_ids = #defaults; )* } @@ -485,10 +491,16 @@ fn encode_field( let sbind = format_ident!("__o{}", depth); let inner_arms: Vec = arm_code.iter().map(|a| quote! { #a }).collect(); + // Destructured through a coercion site: `kind` says this + // field is optional, and how Rust spells that is the + // source's business (#268). + let obind = format_ident!("__oc{}", depth); + let coerce = bind_as_option("e!(&#value), &obind); preludes.extend(quote! { let #flag_id: jni::sys::jboolean; #decls - match &#value { + #coerce + match #obind { ::core::option::Option::Some(#sbind) => { #flag_id = 1u8; match #sbind { #(#inner_arms)* } diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index dcacc8e3..a623b044 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -531,25 +531,29 @@ fn a_sum_field_behind_option_or_vec_is_rejected_by_name() { } } -/// An `Option` the reading sees through a transparent wrapper is **reserved**, -/// and says so by name. +/// A field's optional-ness is read off `kind`; **how Rust spells it is the +/// source's business**, and the emitter must accept any of the spellings. /// -/// `Box` *is* `T` in the model, deliberately — so a field spelled -/// `Box>` peels to `Child` and records an optional access step -/// that has no way to say "deref first". The emitter then matches `Option`'s -/// patterns against a value still typed `Box>`, which is `E0308`. +/// `Box` *is* `T` in the model, deliberately: the flat model states the +/// destination-language invariant, and no target language can tell an +/// `Option` field from a `Box>` one. The emitter used to classify +/// off `kind` correctly ("optional ⇒ needs a `Some`/`None` split") and then +/// *spell* off `kind` too, matching `Option`'s patterns against a place still +/// typed `Box>` — `E0308` (#268). /// -/// The shape used to be *accepted* and mis-emitted: nothing in this suite -/// compiles the generated Rust, so `contains` assertions passed over it happily. -/// Rejecting at the declaration is what makes it visible — see #268, which owns -/// teaching the path algebra the wrapper. +/// So the destructuring goes through a coercion site. Deref coercion is +/// transitive and a no-op when the types already match, which is why one shape +/// serves every representation and the plain spelling is unchanged. /// -/// The plain `Option` case is asserted alongside, because a guard that -/// also rejected the ordinary spelling would be worse than no guard. +/// Both spellings are asserted to reach the SAME leaf surface: if the wrapper +/// changed what crosses, the model would be leaking a Rust detail it exists to +/// hide. (What this cannot yet assert is that the result compiles — nothing in +/// this suite compiles generated Rust, which is exactly how the bug survived. +/// #269 owns that, and names this test.) #[test] -fn an_option_behind_a_transparent_wrapper_is_reserved_by_name() { +fn an_optional_field_crosses_the_same_however_rust_spells_it() { let loc = myflat_loc(); - let build = |field_ty: syn::Type| { + let build = |field_ty: syn::Type| -> String { let items = vec![ ( syn::Item::Struct(syn::parse_quote!( @@ -599,39 +603,41 @@ fn an_option_behind_a_transparent_wrapper_is_reserved_by_name() { let dir = unique_test_dir("jnigen_vf_boxed_opt"); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); - let _ = jni - .build_with(registry) - .map(|g| g.write_rust(dir.join("g.rs"))); + let gen = jni.build_with(registry).expect("resolve"); + std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust")) + .expect("read rust") }; - let err = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - build(syn::parse_quote!(Box>)) - })) - .expect_err("an Option behind a transparent wrapper must be rejected"); - let msg = err - .downcast_ref::() - .cloned() - .or_else(|| err.downcast_ref::<&str>().map(|s| s.to_string())) - .unwrap_or_default(); - assert!( - msg.contains("transparent wrapper"), - "the message names the shape: {msg}" - ); - assert!( - msg.contains("ZSampleStruct.kex"), - "the message names the offending field: {msg}" - ); - // Reserved, not refused: the diagnosis says where the work is tracked. - assert!( - msg.contains("issues/268"), - "a reserved shape names its issue: {msg}" - ); + let plain = build(syn::parse_quote!(Option)); + let boxed = build(syn::parse_quote!(Box>)); - // The ordinary spelling is untouched. - std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - build(syn::parse_quote!(Option)) - })) - .expect("a plain `Option` field is the supported shape"); + for (label, rust) in [("Option", &plain), ("Box>", &boxed)] { + let rc: String = rust.split_whitespace().collect(); + // The destructuring is coerced, never applied to the raw place — the + // one thing that makes it representation-agnostic. + assert!( + rc.contains("let__o0:&::core::option::Option<_>=&"), + "{label}: the optional field is reached through a coercion site:\n{rust}" + ); + assert!( + !rc.contains("match&(&__vf0).kex"), + "{label}: the raw place is never destructured directly:\n{rust}" + ); + // …and the field still crosses as the child's own expansion. + assert!( + rc.contains("myflat::z_keyexpr_as_str(__n0)"), + "{label}: the child's boundary still applies:\n{rust}" + ); + } + + // The wrapper changes the Rust spelling and NOTHING that crosses. Compared + // after normalizing the one legitimate difference — the field's own type is + // spelled in the generated converter signatures. + assert_eq!( + plain.replace("Box>", "Option"), + boxed.replace("Box>", "Option"), + "a transparent wrapper must not change what crosses the boundary" + ); } /// Naming a field the value form does not have is the very drift this From 237ee8c5d08165e835b05a5320f1468117b37c20 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 21:41:38 +0200 Subject: [PATCH 25/52] The selector reads the model instead of guessing types by name (#272) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #270. A `Box>` field with no deconstructor failed to resolve. The issue guessed the cause was `TypeKey` identity; it was not. The selector decided what a type IS by manufacturing a string from its spelling and comparing it, then rebuilt the type it generates BY NAME: * `with_first_arg` rebuilt a wildcard off the spelling's last segment, so `Box>` became `Box<_>`; * ten `pat_match(pat, "Option < _ >")` dispatched by STRING COMPARISON; * seven `parse_quote!(Option<#t1>)` rebuilt the generated fn's signature; * `option_inner_type`/`vec_inner_type` peeled by segment ident; * two `TypeKey == "Box < String >"` arms, one per direction. `select_output_type` was the clearest case: `convert_crossing` fetched the reading and threw it away. Dispatch now comes from `TypeKind` and the signature from `origin.syntax` — the classify-off-kind, spell-off-syntax split #268 restored one layer up. #268 fixed an emitter that classified correctly and then SPELLED off `kind`; this is the same rule broken the other way. Transparent wrappers are handled by one table rather than per type, because the question that matters is what adding a new one costs. `core::flat` owns TRANSPARENT_WRAPPERS — the set it erases — and `lower_path` consults it, so the model cannot drift from its own list. The adapter owns WRAPPER_OPS: what Rust can do with each (read the inner value out, build it back, or neither), so `Box` derefs, `Cow` is refused rather than mis-generated, and nesting is a chain rather than an assumption. `every_erased_wrapper_has_ops` fails if the two lists disagree, verified by adding `Rc` and watching it fail with the row it wants. An unsupported representation is an UNRESOLVED crossing naming the type, never a resolved one emitting code the consumer cannot build: successful resolution is what tells a binding its type is supported. Both `Box` arms are deleted — the evidence the mechanism generalizes. `pat_match` is gone, with `with_first_arg` and `ref_wildcard`. Three review catches, all reproduced before fixing: `Cow` payloads and nested boxes resolved and emitted a single `*v`; wrapped borrow shapes (`Box<&T>`, `Box>`) resolved and passed an owned `T`; and refusing the latter opened a fall-through where the shallow optional handler decoded a handle as `*mut &T` — well-typed and wrong. Two corrections the tests forced: `Rc` is not transparent in the model (only `Box` and `Cow`), and a bare `[T]` is `Sequence` but unsized, so it gets no by-value converter. Boundary ledger 135 -> 130, all of it `selector.rs` (8 -> 3) — the #229 L4 "layer questions" item. Verified: 613 lib tests, `cargo test --all --all-features` 14/14, clippy, fmt. regen-check BUILDS the four example crates, which `include!` the generated bindings, so the changed output type-checks; `boxed_note_echo` returns `Box>>` there as a compile-level case beside its unwrapped control. covertest 48/48 on the JVM. Follow-ups filed: #273 (Kotlin nullability decided by `is_option_type`, a by-name check with ten callers). --- examples/covertest-kotlin/build.rs | 7 + examples/covertest-kotlin/kotlin/REPORT.md | 2 + .../generated/io/prebindgen/covertest.kt | 4 + .../io/prebindgen/covertest/model.kt | 39 + .../src/generated_bindings.rs | 954 ++++++++++++++---- examples/perftest-flat/src/ext.rs | 31 + .../perftest-kotlin/src/generated_bindings.rs | 209 +++- prebindgen/src/api/core/flat/boundary.ledger | 4 +- prebindgen/src/api/core/flat/mod.rs | 5 +- prebindgen/src/api/core/flat/ty.rs | 53 +- .../src/api/lang/jnigen/jni/emit/mod.rs | 10 +- .../src/api/lang/jnigen/jni/emit/names.rs | 9 +- prebindgen/src/api/lang/jnigen/jni/mod.rs | 2 +- .../src/api/lang/jnigen/jni/selector.rs | 200 ++-- .../api/lang/jnigen/jni/tests/value_form.rs | 350 +++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 385 +++++-- 16 files changed, 1841 insertions(+), 423 deletions(-) diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index 81ccfde4..0ddb8d8d 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -515,6 +515,13 @@ fn main() { // crossing covers the borrowed payload, the owned one, and the // sum each report carries. .fun(fun!(ledger_each)) + // A transparent wrapper (`Box>`) in and out. The + // model erases the `Box`, so this must cross exactly as a + // `String?` — and because this crate compiles its generated + // binding, a converter that named the wrong type or bridged it + // with the wrong number of dereferences fails the build (#270). + .fun(fun!(boxed_note_echo)) + .fun(fun!(plain_note_echo)) .fun(fun!(ledger_new)) .fun(fun!(archive_set_reading)) .fun(fun!(archive_reading)) diff --git a/examples/covertest-kotlin/kotlin/REPORT.md b/examples/covertest-kotlin/kotlin/REPORT.md index 987da200..ba557452 100644 --- a/examples/covertest-kotlin/kotlin/REPORT.md +++ b/examples/covertest-kotlin/kotlin/REPORT.md @@ -63,6 +63,7 @@ Base package: `io.prebindgen.covertest` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) - `blob_value_new` — `fun blobValueNew(secs: Long, id: ByteArray, chunks: List, onError: JniErrorHandler): BlobValue` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) +- `boxed_note_echo` — `fun boxedNoteEcho(note: String?, onError: JniErrorHandler): String?` - `cache_config_weight` — `fun cacheConfigWeight(cache: CacheConfig?, onError: JniErrorHandler): Int` - `celsius_double` — `fun celsiusDouble(c: Int, onError: JniErrorHandler): Int` - `duration_boundary_echo` — `fun durationBoundaryEcho(value: DurationBoundary, onError: JniErrorHandler): DurationBoundary` @@ -90,6 +91,7 @@ Base package: `io.prebindgen.covertest` - `percent_invalid_output` — `fun percentInvalidOutput(onError: JniErrorHandler): Int?` - `percent_optional` — `fun percentOptional(p: Int?, onError: JniErrorHandler): Int?` - `percent_scale` — `fun percentScale(p: Int, factor: Int, onError: JniErrorHandler): Int` +- `plain_note_echo` — `fun plainNoteEcho(note: String?, onError: JniErrorHandler): String?` - `priority_or` — `fun priorityOr(p: Priority?, fallback: Priority, onError: JniErrorHandler): Priority` - `priority_weight` — `fun priorityWeight(p: Priority, onError: JniErrorHandler): Int` - `reading_each` — `fun readingEach(n: Int, sink: ReadingCallback, onError: JniErrorHandler)` diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt index 240c6ead..451fc850 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt @@ -894,6 +894,8 @@ internal object CovNative { errorSink: Any, ): Any? + external fun boxedNoteEcho(note: String?, errorSink: Any): String? + external fun cacheConfigWeight( cachePresent: Boolean, cacheRepliesPriority: Int, @@ -995,6 +997,8 @@ internal object CovNative { external fun percentScale(p: Int, factor: Int, errorSink: Any): Int + external fun plainNoteEcho(note: String?, errorSink: Any): String? + external fun priorityOr(pPresent: Boolean, pValue: Int, fallback: Int, errorSink: Any): Int external fun priorityWeight(p: Int, errorSink: Any): Int diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt index f6cfd61b..faf9da4e 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt @@ -1576,6 +1576,45 @@ public fun ledgerEach(n: Long, sink: LedgerCallback, onError: JniErrorHandler>>` classifies `Optional` + * exactly as a bare `Option` does — one thing to every destination + * language, two spellings to Rust. That gap is the whole of #270: the adapter + * used to decide what a type *was* by rebuilding a pattern from its spelling, + * so a wrapped `Option` reconstructed as `Box<_>`, matched nothing, and got no + * converter at all. + * + * Declared here rather than only in a unit test because this crate's generated + * binding is `include!`d and **compiled**: a converter that named + * `Option` for a `Box>>` value, or bridged it with + * the wrong number of dereferences, fails to build. Nested deliberately — one + * dereference leaves a `Box>`, which still compiles as a + * *type* and would only fail here. + * + * A `Cow` payload is the other half and cannot appear in a compiled fixture: + * it must be REFUSED, which only + * `a_transparent_wrapper_is_bridged_only_where_it_can_be` can assert. + */ +public fun boxedNoteEcho(note: String?, onError: JniErrorHandler): String? { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.boxedNoteEcho(note, __bcap) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + +/** + * The same crossing with nothing wrapped — the control the wrapped form must + * match, since the model says the two returns are the same type. + */ +public fun plainNoteEcho(note: String?, onError: JniErrorHandler): String? { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.plainNoteEcho(note, __bcap) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + /** * Build a [`Ledger`]; `n` selects which of the two slots are filled (bit 0 = * `filed`, bit 1 = `archived`), so a caller can drive every arm of the diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index 5d2b9aab..1987cebe 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -380,7 +380,9 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_constGetCoverBan unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -499,7 +501,9 @@ pub(crate) unsafe fn Annotated_to_JObject_b543f0d9<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -516,7 +520,9 @@ pub(crate) unsafe fn Archive_to_jlong_cd73502c<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -589,7 +595,9 @@ pub(crate) unsafe fn Arrays_to_JObject_71120c08<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -641,7 +649,62 @@ pub(crate) unsafe fn BlobValue_to_JObject_89b5dab7<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn Box_Box_Option_String_to_JString_299999e0<'a>( + env: &mut jni::JNIEnv<'a>, + v: Box>>, +) -> ::core::result::Result, __JniErr> { + Ok({ + let v: Option = (*(*v)); + { + match v { + Some(value) => String_to_JString_c7f3ca43(env, value)?, + None => jni::objects::JObject::null().into(), + } + } + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn Box_String_to_JString_027f6250<'a>( + env: &mut jni::JNIEnv<'a>, + v: Box, +) -> ::core::result::Result, __JniErr> { + Ok({ + env.new_string(v.as_str()) + .map_err(|e| { + <__JniErr as ::core::convert::From< + String, + >>::from(format!("encode_str: {}", e)) + })? + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -684,7 +747,9 @@ pub(crate) unsafe fn CacheConfig_to_JObject_db89a97c<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -701,7 +766,9 @@ pub(crate) unsafe fn Celsius_to_i32_88c8e884<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -745,7 +812,9 @@ pub(crate) unsafe fn DurationBoundary_to_JObject_9c5bf9bc<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -787,7 +856,9 @@ pub(crate) unsafe fn Duration_to_u64_e3980876<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -804,7 +875,9 @@ pub(crate) unsafe fn EscapeProbe_to_jlong_416aab42<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -893,7 +966,9 @@ pub(crate) unsafe fn HoldPolicy_to_JObject_d2a5bcc4<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -940,7 +1015,9 @@ pub(crate) unsafe fn JBooleanArray_to_bool_3_3f960c58<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -964,7 +1041,9 @@ pub(crate) unsafe fn JByteArray_to_Vec_u8_7936d5de<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1001,7 +1080,9 @@ pub(crate) unsafe fn JByteArray_to_u8_2_9ca14e44<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1038,7 +1119,9 @@ pub(crate) unsafe fn JByteArray_to_u8_4_39abedfa<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1085,7 +1168,9 @@ pub(crate) unsafe fn JDoubleArray_to_f64_2_dc30d1f9<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1132,7 +1217,9 @@ pub(crate) unsafe fn JIntArray_to_i32_3_60e5e35a<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1179,7 +1266,9 @@ pub(crate) unsafe fn JLongArray_to_i64_2_73596912<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1226,7 +1315,9 @@ pub(crate) unsafe fn JLongArray_to_u64_2_60bcc6a5<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1290,7 +1381,9 @@ pub(crate) unsafe fn JObject_to_Annotated_b543f0d9<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1373,7 +1466,9 @@ pub(crate) unsafe fn JObject_to_Arrays_71120c08<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1418,7 +1513,9 @@ pub(crate) unsafe fn JObject_to_BlobValue_89b5dab7<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1454,7 +1551,9 @@ pub(crate) unsafe fn JObject_to_CacheConfig_db89a97c<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1507,7 +1606,9 @@ pub(crate) unsafe fn JObject_to_DurationBoundary_9c5bf9bc<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1543,7 +1644,9 @@ pub(crate) unsafe fn JObject_to_HoldPolicy_d2a5bcc4<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1616,7 +1719,9 @@ pub(crate) unsafe fn JObject_to_Hold_5f85caaf<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1727,7 +1832,9 @@ pub(crate) unsafe fn JObject_to_Lookup_94ada15e<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1807,7 +1914,9 @@ pub(crate) unsafe fn JObject_to_Marker_3dc81334<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1843,7 +1952,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary16_e9d41606<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1879,7 +1990,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary2_a8f288cc<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1915,7 +2028,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary32_ed80fac3<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1951,7 +2066,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary4_ea3fd497<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2019,7 +2136,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary63_29aa82ff<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2055,7 +2174,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary64_b2751ca5<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2091,7 +2212,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary8_55b82b02<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2119,7 +2242,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundaryLeaf_93531764<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2155,7 +2280,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary_dc5ac22b<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2208,7 +2335,9 @@ pub(crate) unsafe fn JObject_to_Observation_435b0724<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2219,7 +2348,14 @@ pub(crate) unsafe fn JObject_to_Option_CacheConfig_a6be794d<'env, 'v>( v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { Ok({ - if v.is_null() { None } else { Some(JObject_to_CacheConfig_db89a97c(env, v)?) } + let __v: ::core::option::Option = { + if v.is_null() { + None + } else { + Some(JObject_to_CacheConfig_db89a97c(env, v)?) + } + }; + __v }) } #[allow( @@ -2227,7 +2363,9 @@ pub(crate) unsafe fn JObject_to_Option_CacheConfig_a6be794d<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2237,14 +2375,21 @@ pub(crate) unsafe fn JObject_to_Option_Hold_230d7f9b<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { - Ok({ if v.is_null() { None } else { Some(JObject_to_Hold_5f85caaf(env, v)?) } }) + Ok({ + let __v: ::core::option::Option = { + if v.is_null() { None } else { Some(JObject_to_Hold_5f85caaf(env, v)?) } + }; + __v + }) } #[allow( non_snake_case, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2254,14 +2399,21 @@ pub(crate) unsafe fn JObject_to_Option_Payload_97036642<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { - Ok({ if v.is_null() { None } else { Some(JObject_to_Payload_98f64326(env, v)?) } }) + Ok({ + let __v: ::core::option::Option = { + if v.is_null() { None } else { Some(JObject_to_Payload_98f64326(env, v)?) } + }; + __v + }) } #[allow( non_snake_case, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2272,25 +2424,28 @@ pub(crate) unsafe fn JObject_to_Option_Percent_544dd364<'env, 'v>( v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { Ok({ - if !v.is_null() { - let __unboxed: jni::sys::jint = env - .call_method(&v, "intValue", "()I", &[]) - .and_then(|val| val.i()) - .map(|__x| __x as jni::sys::jint) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option unbox: {}", e)))?; - Some({ - let __inner_s0 = jint_to_i32_a3e3b6ef(env, &__unboxed)?; - let __inner_s1 = i32_to_Percent_db3641cc(env, __inner_s0) - .map_err(|__e| <__JniErr as ::core::convert::From< + let __v: ::core::option::Option = { + if !v.is_null() { + let __unboxed: jni::sys::jint = env + .call_method(&v, "intValue", "()I", &[]) + .and_then(|val| val.i()) + .map(|__x| __x as jni::sys::jint) + .map_err(|e| <__JniErr as ::core::convert::From< String, - >>::from(__e.to_string()))?; - __inner_s1 - }) - } else { - None - } + >>::from(format!("Option unbox: {}", e)))?; + Some({ + let __inner_s0 = jint_to_i32_a3e3b6ef(env, &__unboxed)?; + let __inner_s1 = i32_to_Percent_db3641cc(env, __inner_s0) + .map_err(|__e| <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()))?; + __inner_s1 + }) + } else { + None + } + }; + __v }) } #[allow( @@ -2298,7 +2453,9 @@ pub(crate) unsafe fn JObject_to_Option_Percent_544dd364<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2309,18 +2466,21 @@ pub(crate) unsafe fn JObject_to_Option_Priority_ad5cbb32<'env, 'v>( v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { Ok({ - if !v.is_null() { - let __unboxed: jni::sys::jint = env - .call_method(&v, "intValue", "()I", &[]) - .and_then(|val| val.i()) - .map(|__x| __x as jni::sys::jint) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option unbox: {}", e)))?; - Some(jint_to_Priority_447102d2(env, &__unboxed)?) - } else { - None - } + let __v: ::core::option::Option = { + if !v.is_null() { + let __unboxed: jni::sys::jint = env + .call_method(&v, "intValue", "()I", &[]) + .and_then(|val| val.i()) + .map(|__x| __x as jni::sys::jint) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option unbox: {}", e)))?; + Some(jint_to_Priority_447102d2(env, &__unboxed)?) + } else { + None + } + }; + __v }) } #[allow( @@ -2328,7 +2488,9 @@ pub(crate) unsafe fn JObject_to_Option_Priority_ad5cbb32<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2338,14 +2500,21 @@ pub(crate) unsafe fn JObject_to_Option_Reading_80df84a9<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { - Ok({ if v.is_null() { None } else { Some(JObject_to_Reading_2261050f(env, v)?) } }) + Ok({ + let __v: ::core::option::Option = { + if v.is_null() { None } else { Some(JObject_to_Reading_2261050f(env, v)?) } + }; + __v + }) } #[allow( non_snake_case, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2356,18 +2525,21 @@ pub(crate) unsafe fn JObject_to_Option_f64_b3f3e9a9<'env, 'v>( v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { Ok({ - if !v.is_null() { - let __unboxed: jni::sys::jdouble = env - .call_method(&v, "doubleValue", "()D", &[]) - .and_then(|val| val.d()) - .map(|__x| __x as jni::sys::jdouble) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option unbox: {}", e)))?; - Some(jdouble_to_f64_9e4a8f70(env, &__unboxed)?) - } else { - None - } + let __v: ::core::option::Option = { + if !v.is_null() { + let __unboxed: jni::sys::jdouble = env + .call_method(&v, "doubleValue", "()D", &[]) + .and_then(|val| val.d()) + .map(|__x| __x as jni::sys::jdouble) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option unbox: {}", e)))?; + Some(jdouble_to_f64_9e4a8f70(env, &__unboxed)?) + } else { + None + } + }; + __v }) } #[allow( @@ -2375,7 +2547,9 @@ pub(crate) unsafe fn JObject_to_Option_f64_b3f3e9a9<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2386,18 +2560,21 @@ pub(crate) unsafe fn JObject_to_Option_i64_2ba9a5ed<'env, 'v>( v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { Ok({ - if !v.is_null() { - let __unboxed: jni::sys::jlong = env - .call_method(&v, "longValue", "()J", &[]) - .and_then(|val| val.j()) - .map(|__x| __x as jni::sys::jlong) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option unbox: {}", e)))?; - Some(jlong_to_i64_fbf9a9bc(env, &__unboxed)?) - } else { - None - } + let __v: ::core::option::Option = { + if !v.is_null() { + let __unboxed: jni::sys::jlong = env + .call_method(&v, "longValue", "()J", &[]) + .and_then(|val| val.j()) + .map(|__x| __x as jni::sys::jlong) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option unbox: {}", e)))?; + Some(jlong_to_i64_fbf9a9bc(env, &__unboxed)?) + } else { + None + } + }; + __v }) } #[allow( @@ -2405,7 +2582,9 @@ pub(crate) unsafe fn JObject_to_Option_i64_2ba9a5ed<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2416,18 +2595,21 @@ pub(crate) unsafe fn JObject_to_Option_u64_32be16a2<'env, 'v>( v: &jni::objects::JObject<'v>, ) -> ::core::result::Result, __JniErr> { Ok({ - if !v.is_null() { - let __unboxed: jni::sys::jlong = env - .call_method(&v, "longValue", "()J", &[]) - .and_then(|val| val.j()) - .map(|__x| __x as jni::sys::jlong) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option unbox: {}", e)))?; - Some(jlong_to_u64_4384a5d6(env, &__unboxed)?) - } else { - None - } + let __v: ::core::option::Option = { + if !v.is_null() { + let __unboxed: jni::sys::jlong = env + .call_method(&v, "longValue", "()J", &[]) + .and_then(|val| val.j()) + .map(|__x| __x as jni::sys::jlong) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option unbox: {}", e)))?; + Some(jlong_to_u64_4384a5d6(env, &__unboxed)?) + } else { + None + } + }; + __v }) } #[allow( @@ -2435,7 +2617,9 @@ pub(crate) unsafe fn JObject_to_Option_u64_32be16a2<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2496,7 +2680,9 @@ pub(crate) unsafe fn JObject_to_Payload_98f64326<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2654,7 +2840,9 @@ pub(crate) unsafe fn JObject_to_Reading_2261050f<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2696,7 +2884,9 @@ pub(crate) unsafe fn JObject_to_RepliesConfig_eb8e9079<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2732,7 +2922,9 @@ pub(crate) unsafe fn JObject_to_Stamp_f6b1e942<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2768,7 +2960,9 @@ pub(crate) unsafe fn JObject_to_Tagged_641b984c<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2838,7 +3032,9 @@ pub(crate) unsafe fn JObject_to_Unsigned_7e3cc618<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2884,7 +3080,9 @@ pub(crate) unsafe fn JObject_to_Vec_Label_3fdf860d<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2926,7 +3124,9 @@ pub(crate) unsafe fn JObject_to_Vec_Payload_8b7084d2<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2965,7 +3165,9 @@ pub(crate) unsafe fn JObject_to_Vec_Vec_u8_43404875<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -3058,7 +3260,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Duration_Send_Sync_static_98c9f460<'env, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -3721,7 +3925,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Ledger_Send_Sync_static_c76008cc<'env, ' unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -3859,7 +4065,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Lookup_Send_Sync_static_4a65bc23<'env, ' unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -4134,7 +4342,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Payload_Send_Sync_static_95073668<'env, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -4308,7 +4518,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Payload_Send_Sync_static_96d50906<'env, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -4544,7 +4756,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Reading_Send_Sync_static_5964f1fc<'env, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -4793,7 +5007,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Report_Send_Sync_static_eb5ca515<'env, ' unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -4878,7 +5094,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Storage_Send_Sync_static_2f26edcf<'env, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -4958,7 +5176,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_u64_Send_Sync_static_c7830b57<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5005,7 +5225,37 @@ pub(crate) unsafe fn JShortArray_to_i16_2_098f4ad5<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn JString_to_Box_String_027f6250<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JString<'v>, +) -> ::core::result::Result, __JniErr> { + Ok({ + let s = env + .get_string(v) + .map_err(|e| { + <__JniErr as ::core::convert::From< + String, + >>::from(format!("decode_string: {}", e)) + })?; + ::std::string::String::from(s).into() + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5016,11 +5266,14 @@ pub(crate) unsafe fn JString_to_Option_Box_String_071e4c8c<'env, 'v>( v: &jni::objects::JString<'v>, ) -> ::core::result::Result>, __JniErr> { Ok({ - if v.is_null() { - None - } else { - Some(JString_to_std_boxed_Box_std_string_String_cfbab680(env, v)?) - } + let __v: ::core::option::Option> = { + if v.is_null() { + None + } else { + Some(JString_to_Box_String_027f6250(env, v)?) + } + }; + __v }) } #[allow( @@ -5028,25 +5281,23 @@ pub(crate) unsafe fn JString_to_Option_Box_String_071e4c8c<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, clippy::eq_op )] -pub(crate) unsafe fn JString_to_String_c7f3ca43<'env, 'v>( +pub(crate) unsafe fn JString_to_Option_String_56d5e304<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JString<'v>, -) -> ::core::result::Result { +) -> ::core::result::Result, __JniErr> { Ok({ - let s = env - .get_string(v) - .map_err(|e| { - <__JniErr as ::core::convert::From< - String, - >>::from(format!("decode_string: {}", e)) - })?; - s.into() + let __v: ::core::option::Option = { + if v.is_null() { None } else { Some(JString_to_String_c7f3ca43(env, v)?) } + }; + __v }) } #[allow( @@ -5054,16 +5305,18 @@ pub(crate) unsafe fn JString_to_String_c7f3ca43<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, clippy::eq_op )] -pub(crate) unsafe fn JString_to_std_boxed_Box_std_string_String_cfbab680<'env, 'v>( +pub(crate) unsafe fn JString_to_String_c7f3ca43<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JString<'v>, -) -> ::core::result::Result<::std::boxed::Box<::std::string::String>, __JniErr> { +) -> ::core::result::Result { Ok({ let s = env .get_string(v) @@ -5072,7 +5325,7 @@ pub(crate) unsafe fn JString_to_std_boxed_Box_std_string_String_cfbab680<'env, ' String, >>::from(format!("decode_string: {}", e)) })?; - ::std::boxed::Box::new(::std::string::String::from(s)) + s.into() }) } #[allow( @@ -5080,7 +5333,9 @@ pub(crate) unsafe fn JString_to_std_boxed_Box_std_string_String_cfbab680<'env, ' unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5097,7 +5352,9 @@ pub(crate) unsafe fn Label_to_String_63dec766<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5114,7 +5371,9 @@ pub(crate) unsafe fn Millis_to_i64_61ecf054<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5225,7 +5484,9 @@ pub(crate) unsafe fn ObjectBoundary16_to_JObject_e9d41606<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5266,7 +5527,9 @@ pub(crate) unsafe fn ObjectBoundary2_to_JObject_a8f288cc<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5457,7 +5720,9 @@ pub(crate) unsafe fn ObjectBoundary32_to_JObject_ed80fac3<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5508,7 +5773,9 @@ pub(crate) unsafe fn ObjectBoundary4_to_JObject_ea3fd497<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -5920,7 +6187,9 @@ pub(crate) unsafe fn ObjectBoundary63_to_JObject_29aa82ff<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -6355,7 +6624,9 @@ pub(crate) unsafe fn ObjectBoundary64_to_JObject_b2751ca5<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -6426,7 +6697,9 @@ pub(crate) unsafe fn ObjectBoundary8_to_JObject_55b82b02<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -6457,7 +6730,9 @@ pub(crate) unsafe fn ObjectBoundaryLeaf_to_JObject_93531764<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7347,7 +7622,9 @@ pub(crate) unsafe fn ObjectBoundary_to_JObject_dc5ac22b<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7576,7 +7853,9 @@ pub(crate) unsafe fn Observation_to_JObject_435b0724<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7587,11 +7866,12 @@ pub(crate) unsafe fn Option_Box_String_to_JString_071e4c8c<'a>( v: Option>, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => { - std_boxed_Box_std_string_String_to_JString_cfbab680(env, value)? + let v: Option> = v; + { + match v { + Some(value) => Box_String_to_JString_027f6250(env, value)?, + None => jni::objects::JObject::null().into(), } - None => jni::objects::JObject::null().into(), } }) } @@ -7600,7 +7880,9 @@ pub(crate) unsafe fn Option_Box_String_to_JString_071e4c8c<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7611,15 +7893,18 @@ pub(crate) unsafe fn Option_Duration_to_jlong_1cfa4d44<'a>( v: Option, ) -> ::core::result::Result { Ok({ - match v { - Some(value) => { - let __inner_s0 = Duration_to_u64_e3980876(env, value) - .map_err(|__e| <__JniErr as ::core::convert::From< - String, - >>::from(__e.to_string()))?; - u64_to_jlong_4384a5d6(env, __inner_s0)? + let v: Option = v; + { + match v { + Some(value) => { + let __inner_s0 = Duration_to_u64_e3980876(env, value) + .map_err(|__e| <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()))?; + u64_to_jlong_4384a5d6(env, __inner_s0)? + } + None => -1i64, } - None => -1i64, } }) } @@ -7628,7 +7913,9 @@ pub(crate) unsafe fn Option_Duration_to_jlong_1cfa4d44<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7639,9 +7926,12 @@ pub(crate) unsafe fn Option_Payload_to_JObject_97036642<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => Payload_to_JObject_98f64326(env, value)?, - None => jni::objects::JObject::null().into(), + let v: Option = v; + { + match v { + Some(value) => Payload_to_JObject_98f64326(env, value)?, + None => jni::objects::JObject::null().into(), + } } }) } @@ -7650,7 +7940,9 @@ pub(crate) unsafe fn Option_Payload_to_JObject_97036642<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7661,21 +7953,24 @@ pub(crate) unsafe fn Option_Percent_to_JObject_544dd364<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => { - let __raw: jni::sys::jint = { - let __inner_s0 = Percent_to_i32_01484801(env, value) - .map_err(|__e| <__JniErr as ::core::convert::From< + let v: Option = v; + { + match v { + Some(value) => { + let __raw: jni::sys::jint = { + let __inner_s0 = Percent_to_i32_01484801(env, value) + .map_err(|__e| <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()))?; + i32_to_jint_a3e3b6ef(env, __inner_s0)? + }; + ::prebindgen::lang::box_jint(env, __raw) + .map_err(|e| <__JniErr as ::core::convert::From< String, - >>::from(__e.to_string()))?; - i32_to_jint_a3e3b6ef(env, __inner_s0)? - }; - ::prebindgen::lang::box_jint(env, __raw) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option box: {}", e)))? + >>::from(format!("Option box: {}", e)))? + } + None => jni::objects::JObject::null(), } - None => jni::objects::JObject::null(), } }) } @@ -7684,7 +7979,9 @@ pub(crate) unsafe fn Option_Percent_to_JObject_544dd364<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7695,15 +7992,18 @@ pub(crate) unsafe fn Option_Priority_to_JObject_ad5cbb32<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => { - let __raw: jni::sys::jint = Priority_to_jint_447102d2(env, value)?; - ::prebindgen::lang::box_jint(env, __raw) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option box: {}", e)))? + let v: Option = v; + { + match v { + Some(value) => { + let __raw: jni::sys::jint = Priority_to_jint_447102d2(env, value)?; + ::prebindgen::lang::box_jint(env, __raw) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option box: {}", e)))? + } + None => jni::objects::JObject::null(), } - None => jni::objects::JObject::null(), } }) } @@ -7712,7 +8012,9 @@ pub(crate) unsafe fn Option_Priority_to_JObject_ad5cbb32<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7723,9 +8025,39 @@ pub(crate) unsafe fn Option_Stamp_to_JObject_6375b503<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => Stamp_to_JObject_f6b1e942(env, value)?, - None => jni::objects::JObject::null().into(), + let v: Option = v; + { + match v { + Some(value) => Stamp_to_JObject_f6b1e942(env, value)?, + None => jni::objects::JObject::null().into(), + } + } + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn Option_String_to_JString_56d5e304<'a>( + env: &mut jni::JNIEnv<'a>, + v: Option, +) -> ::core::result::Result, __JniErr> { + Ok({ + let v: Option = v; + { + match v { + Some(value) => String_to_JString_c7f3ca43(env, value)?, + None => jni::objects::JObject::null().into(), + } } }) } @@ -7734,7 +8066,9 @@ pub(crate) unsafe fn Option_Stamp_to_JObject_6375b503<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7745,9 +8079,12 @@ pub(crate) unsafe fn Option_Summary_to_jlong_828826f3<'a>( v: Option<&perftest_flat::Summary>, ) -> ::core::result::Result { Ok({ - match v { - Some(value) => Summary_to_jlong_ccacdeac(env, value)?, - None => 0i64, + let v: Option<&perftest_flat::Summary> = v; + { + match v { + Some(value) => Summary_to_jlong_ccacdeac(env, value)?, + None => 0i64, + } } }) } @@ -7756,7 +8093,9 @@ pub(crate) unsafe fn Option_Summary_to_jlong_828826f3<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7767,9 +8106,12 @@ pub(crate) unsafe fn Option_Vec_Payload_to_JObject_b9a4637e<'a>( v: Option>, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => Vec_Payload_to_JObject_8b7084d2(env, value)?, - None => jni::objects::JObject::null().into(), + let v: Option> = v; + { + match v { + Some(value) => Vec_Payload_to_JObject_8b7084d2(env, value)?, + None => jni::objects::JObject::null().into(), + } } }) } @@ -7778,7 +8120,9 @@ pub(crate) unsafe fn Option_Vec_Payload_to_JObject_b9a4637e<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7789,15 +8133,18 @@ pub(crate) unsafe fn Option_f64_to_JObject_b3f3e9a9<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => { - let __raw: jni::sys::jdouble = f64_to_jdouble_9e4a8f70(env, value)?; - ::prebindgen::lang::box_jdouble(env, __raw) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option box: {}", e)))? + let v: Option = v; + { + match v { + Some(value) => { + let __raw: jni::sys::jdouble = f64_to_jdouble_9e4a8f70(env, value)?; + ::prebindgen::lang::box_jdouble(env, __raw) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option box: {}", e)))? + } + None => jni::objects::JObject::null(), } - None => jni::objects::JObject::null(), } }) } @@ -7806,7 +8153,9 @@ pub(crate) unsafe fn Option_f64_to_JObject_b3f3e9a9<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7817,15 +8166,18 @@ pub(crate) unsafe fn Option_i64_to_JObject_2ba9a5ed<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => { - let __raw: jni::sys::jlong = i64_to_jlong_fbf9a9bc(env, value)?; - ::prebindgen::lang::box_jlong(env, __raw) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option box: {}", e)))? + let v: Option = v; + { + match v { + Some(value) => { + let __raw: jni::sys::jlong = i64_to_jlong_fbf9a9bc(env, value)?; + ::prebindgen::lang::box_jlong(env, __raw) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option box: {}", e)))? + } + None => jni::objects::JObject::null(), } - None => jni::objects::JObject::null(), } }) } @@ -7834,7 +8186,9 @@ pub(crate) unsafe fn Option_i64_to_JObject_2ba9a5ed<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7845,15 +8199,18 @@ pub(crate) unsafe fn Option_u64_to_JObject_32be16a2<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => { - let __raw: jni::sys::jlong = u64_to_jlong_4384a5d6(env, value)?; - ::prebindgen::lang::box_jlong(env, __raw) - .map_err(|e| <__JniErr as ::core::convert::From< - String, - >>::from(format!("Option box: {}", e)))? + let v: Option = v; + { + match v { + Some(value) => { + let __raw: jni::sys::jlong = u64_to_jlong_4384a5d6(env, value)?; + ::prebindgen::lang::box_jlong(env, __raw) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option box: {}", e)))? + } + None => jni::objects::JObject::null(), } - None => jni::objects::JObject::null(), } }) } @@ -7862,7 +8219,9 @@ pub(crate) unsafe fn Option_u64_to_JObject_32be16a2<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7879,7 +8238,9 @@ pub(crate) unsafe fn PayloadHandler_to_jlong_d61fd890<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7896,7 +8257,9 @@ pub(crate) unsafe fn PayloadVecHandler_to_jlong_b32d2812<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7936,7 +8299,9 @@ pub(crate) unsafe fn Payload_to_JObject_25cd94ea<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -7984,7 +8349,9 @@ pub(crate) unsafe fn Payload_to_JObject_98f64326<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8001,7 +8368,9 @@ pub(crate) unsafe fn Percent_to_i32_01484801<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8018,7 +8387,9 @@ pub(crate) unsafe fn Priority_to_jint_447102d2<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8059,7 +8430,9 @@ pub(crate) unsafe fn RepliesConfig_to_JObject_eb8e9079<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8076,7 +8449,9 @@ pub(crate) unsafe fn Report_to_jlong_eaed4ba1<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8093,7 +8468,9 @@ pub(crate) unsafe fn Result_Storage_StorageError_to_Storage_7ccce404<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8110,7 +8487,9 @@ pub(crate) unsafe fn Result_Summary_String_to_Summary_dfdf7f9e<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8145,7 +8524,9 @@ pub(crate) unsafe fn Stamp_to_JObject_f6b1e942<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8162,7 +8543,9 @@ pub(crate) unsafe fn StorageError_to_jlong_26b2d298<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8179,7 +8562,9 @@ pub(crate) unsafe fn StorageHandler_to_jlong_3b4d3ed3<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8196,7 +8581,9 @@ pub(crate) unsafe fn Storage_to_jlong_1b233abd<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8211,7 +8598,7 @@ pub(crate) unsafe fn String_to_JString_c7f3ca43<'a>( .map_err(|e| { <__JniErr as ::core::convert::From< String, - >>::from(format!("encode_string: {}", e)) + >>::from(format!("encode_str: {}", e)) })? }) } @@ -8220,7 +8607,9 @@ pub(crate) unsafe fn String_to_JString_c7f3ca43<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8237,7 +8626,9 @@ pub(crate) unsafe fn String_to_Label_c1a79668<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8254,7 +8645,9 @@ pub(crate) unsafe fn Summary_to_jlong_3cb103b9<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8271,7 +8664,9 @@ pub(crate) unsafe fn Summary_to_jlong_ccacdeac<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8322,7 +8717,9 @@ pub(crate) unsafe fn Tagged_to_JObject_641b984c<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8366,7 +8763,9 @@ pub(crate) unsafe fn Unsigned_to_JObject_7e3cc618<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8377,6 +8776,7 @@ pub(crate) unsafe fn Vec_Label_to_JObject_3fdf860d<'a>( v: Vec, ) -> ::core::result::Result, __JniErr> { Ok({ + let v: Vec = v; let __list_obj = env .new_object("java/util/ArrayList", "()V", &[]) .map_err(|e| <__JniErr as ::core::convert::From< @@ -8409,7 +8809,9 @@ pub(crate) unsafe fn Vec_Label_to_JObject_3fdf860d<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8420,6 +8822,7 @@ pub(crate) unsafe fn Vec_Payload_to_JObject_8b7084d2<'a>( v: Vec, ) -> ::core::result::Result, __JniErr> { Ok({ + let v: Vec = v; let __list_obj = env .new_object("java/util/ArrayList", "()V", &[]) .map_err(|e| <__JniErr as ::core::convert::From< @@ -8446,7 +8849,9 @@ pub(crate) unsafe fn Vec_Payload_to_JObject_8b7084d2<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8457,6 +8862,7 @@ pub(crate) unsafe fn Vec_Stamp_to_JObject_8954d9be<'a>( v: Vec, ) -> ::core::result::Result, __JniErr> { Ok({ + let v: Vec = v; let __list_obj = env .new_object("java/util/ArrayList", "()V", &[]) .map_err(|e| <__JniErr as ::core::convert::From< @@ -8483,7 +8889,9 @@ pub(crate) unsafe fn Vec_Stamp_to_JObject_8954d9be<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8494,6 +8902,7 @@ pub(crate) unsafe fn Vec_String_to_JObject_1e282499<'a>( v: Vec, ) -> ::core::result::Result, __JniErr> { Ok({ + let v: Vec = v; let __list_obj = env .new_object("java/util/ArrayList", "()V", &[]) .map_err(|e| <__JniErr as ::core::convert::From< @@ -8520,7 +8929,9 @@ pub(crate) unsafe fn Vec_String_to_JObject_1e282499<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8531,6 +8942,7 @@ pub(crate) unsafe fn Vec_Vec_u8_to_JObject_43404875<'a>( v: Vec>, ) -> ::core::result::Result, __JniErr> { Ok({ + let v: Vec> = v; let __list_obj = env .new_object("java/util/ArrayList", "()V", &[]) .map_err(|e| <__JniErr as ::core::convert::From< @@ -8557,7 +8969,9 @@ pub(crate) unsafe fn Vec_Vec_u8_to_JObject_43404875<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8581,7 +8995,9 @@ pub(crate) unsafe fn Vec_u8_to_JByteArray_7936d5de<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8617,7 +9033,9 @@ pub(crate) unsafe fn bool_3_to_JBooleanArray_3f960c58<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8634,7 +9052,9 @@ pub(crate) unsafe fn bool_to_jboolean_31306d98<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8670,7 +9090,9 @@ pub(crate) unsafe fn f64_2_to_JDoubleArray_dc30d1f9<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8687,7 +9109,9 @@ pub(crate) unsafe fn f64_to_jdouble_9e4a8f70<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8723,7 +9147,9 @@ pub(crate) unsafe fn i16_2_to_JShortArray_098f4ad5<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8759,7 +9185,9 @@ pub(crate) unsafe fn i32_3_to_JIntArray_60e5e35a<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8776,7 +9204,9 @@ pub(crate) unsafe fn i32_to_Celsius_8c363100<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8796,7 +9226,9 @@ pub(crate) unsafe fn i32_to_Percent_db3641cc<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8813,7 +9245,9 @@ pub(crate) unsafe fn i32_to_jint_a3e3b6ef<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8849,7 +9283,9 @@ pub(crate) unsafe fn i64_2_to_JLongArray_73596912<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8866,7 +9302,9 @@ pub(crate) unsafe fn i64_to_Millis_bb88777a<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8883,7 +9321,9 @@ pub(crate) unsafe fn i64_to_jlong_fbf9a9bc<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8900,7 +9340,9 @@ pub(crate) unsafe fn jboolean_to_bool_31306d98<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8917,7 +9359,9 @@ pub(crate) unsafe fn jdouble_to_f64_9e4a8f70<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8947,7 +9391,9 @@ pub(crate) unsafe fn jint_to_Priority_447102d2<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8964,7 +9410,9 @@ pub(crate) unsafe fn jint_to_i32_a3e3b6ef<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -8988,7 +9436,9 @@ pub(crate) unsafe fn jint_to_u16_28edf527<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9012,7 +9462,9 @@ pub(crate) unsafe fn jint_to_u8_553cf6ec<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9036,7 +9488,9 @@ pub(crate) unsafe fn jlong_to_Archive_cd73502c<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9060,7 +9514,9 @@ pub(crate) unsafe fn jlong_to_EscapeProbe_416aab42<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9071,18 +9527,21 @@ pub(crate) unsafe fn jlong_to_Option_Duration_1cfa4d44<'env, 'v>( v: &jni::sys::jlong, ) -> ::core::result::Result, __JniErr> { Ok({ - if *v == -1i64 { - None - } else { - Some({ - let __inner_s0 = jlong_to_u64_4384a5d6(env, v)?; - let __inner_s1 = u64_to_Duration_7c0845f9(env, __inner_s0) - .map_err(|__e| <__JniErr as ::core::convert::From< - String, - >>::from(__e.to_string()))?; - __inner_s1 - }) - } + let __v: ::core::option::Option = { + if *v == -1i64 { + None + } else { + Some({ + let __inner_s0 = jlong_to_u64_4384a5d6(env, v)?; + let __inner_s1 = u64_to_Duration_7c0845f9(env, __inner_s0) + .map_err(|__e| <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()))?; + __inner_s1 + }) + } + }; + __v }) } #[allow( @@ -9090,7 +9549,9 @@ pub(crate) unsafe fn jlong_to_Option_Duration_1cfa4d44<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9101,7 +9562,7 @@ pub(crate) unsafe fn jlong_to_Option_Summary_252ef2ba<'env, 'v>( v: &jni::sys::jlong, ) -> ::core::result::Result, __JniErr> { Ok({ - if *v == 0 { + let __v: ::core::option::Option = if *v == 0 { None } else if (*v & 1) == 1 { return ::core::result::Result::Err( @@ -9111,7 +9572,8 @@ pub(crate) unsafe fn jlong_to_Option_Summary_252ef2ba<'env, 'v>( ); } else { Some(*std::boxed::Box::from_raw(*v as *mut perftest_flat::Summary)) - } + }; + __v }) } #[allow( @@ -9119,7 +9581,9 @@ pub(crate) unsafe fn jlong_to_Option_Summary_252ef2ba<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9136,7 +9600,9 @@ pub(crate) unsafe fn jlong_to_Option_Summary_828826f3<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9160,7 +9626,9 @@ pub(crate) unsafe fn jlong_to_PayloadHandler_d61fd890<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9184,7 +9652,9 @@ pub(crate) unsafe fn jlong_to_PayloadVecHandler_b32d2812<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9208,7 +9678,9 @@ pub(crate) unsafe fn jlong_to_Report_eaed4ba1<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9232,7 +9704,9 @@ pub(crate) unsafe fn jlong_to_StorageError_26b2d298<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9256,7 +9730,9 @@ pub(crate) unsafe fn jlong_to_StorageHandler_3b4d3ed3<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9280,7 +9756,9 @@ pub(crate) unsafe fn jlong_to_Storage_1b233abd<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9304,7 +9782,9 @@ pub(crate) unsafe fn jlong_to_Summary_3cb103b9<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9321,7 +9801,9 @@ pub(crate) unsafe fn jlong_to_i64_fbf9a9bc<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9345,7 +9827,9 @@ pub(crate) unsafe fn jlong_to_u32_9594a230<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9362,31 +9846,9 @@ pub(crate) unsafe fn jlong_to_u64_4384a5d6<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, - clippy::needless_question_mark, - clippy::let_and_return, - clippy::nonminimal_bool, - clippy::eq_op -)] -pub(crate) unsafe fn std_boxed_Box_std_string_String_to_JString_cfbab680<'a>( - env: &mut jni::JNIEnv<'a>, - v: ::std::boxed::Box<::std::string::String>, -) -> ::core::result::Result, __JniErr> { - Ok({ - env.new_string(v.as_str()) - .map_err(|e| { - <__JniErr as ::core::convert::From< - String, - >>::from(format!("encode_str: {}", e)) - })? - }) -} -#[allow( - non_snake_case, - unused_mut, - unused_variables, - unused_braces, - dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9410,7 +9872,9 @@ pub(crate) unsafe fn str_to_JString_7b77dc67<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9427,7 +9891,9 @@ pub(crate) unsafe fn u16_to_jint_28edf527<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9444,7 +9910,9 @@ pub(crate) unsafe fn u32_to_jlong_9594a230<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9480,7 +9948,9 @@ pub(crate) unsafe fn u64_2_to_JLongArray_60bcc6a5<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9511,7 +9981,9 @@ pub(crate) unsafe fn u64_to_Duration_7c0845f9<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9528,7 +10000,9 @@ pub(crate) unsafe fn u64_to_jlong_4384a5d6<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9552,7 +10026,9 @@ pub(crate) unsafe fn u8_4_to_JByteArray_39abedfa<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -9569,7 +10045,9 @@ pub(crate) unsafe fn u8_to_jint_553cf6ec<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -12206,6 +12684,48 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_blobValueNew<'a> } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedNoteEcho<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + note: jni::objects::JString<'a>, + __error_sink: jni::objects::JObject<'a>, +) -> jni::objects::JString<'a> { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let note = match JString_to_Option_String_56d5e304(&mut env, ¬e) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return jni::objects::JObject::null().into(); + } + }; + let __out = perftest_flat::boxed_note_echo(note); + match Box_Box_Option_String_to_JString_299999e0(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + jni::objects::JObject::null().into() + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_cacheConfigWeight<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, @@ -15144,6 +15664,48 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_percentScale<'a> } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_plainNoteEcho<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + note: jni::objects::JString<'a>, + __error_sink: jni::objects::JObject<'a>, +) -> jni::objects::JString<'a> { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let note = match JString_to_Option_String_56d5e304(&mut env, ¬e) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return jni::objects::JObject::null().into(); + } + }; + let __out = perftest_flat::plain_note_echo(note); + match Option_String_to_JString_56d5e304(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + jni::objects::JObject::null().into() + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_priorityOr<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index 0e0ffb30..ca6b0383 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -1579,6 +1579,37 @@ pub fn ledger_archived(l: &Ledger) -> Option { l.archived.map(ledger_report) } +/// A **transparent wrapper**, and its unwrapped control. +/// +/// The model erases `Box`, so `Box>>` classifies `Optional` +/// exactly as a bare `Option` does — one thing to every destination +/// language, two spellings to Rust. That gap is the whole of #270: the adapter +/// used to decide what a type *was* by rebuilding a pattern from its spelling, +/// so a wrapped `Option` reconstructed as `Box<_>`, matched nothing, and got no +/// converter at all. +/// +/// Declared here rather than only in a unit test because this crate's generated +/// binding is `include!`d and **compiled**: a converter that named +/// `Option` for a `Box>>` value, or bridged it with +/// the wrong number of dereferences, fails to build. Nested deliberately — one +/// dereference leaves a `Box>`, which still compiles as a +/// *type* and would only fail here. +/// +/// A `Cow` payload is the other half and cannot appear in a compiled fixture: +/// it must be REFUSED, which only +/// `a_transparent_wrapper_is_bridged_only_where_it_can_be` can assert. +#[prebindgen] +pub fn boxed_note_echo(note: Option) -> Box>> { + Box::new(Box::new(note)) +} + +/// The same crossing with nothing wrapped — the control the wrapped form must +/// match, since the model says the two returns are the same type. +#[prebindgen] +pub fn plain_note_echo(note: Option) -> Option { + note +} + /// Deliver a [`Ledger`] to a callback, so both conditional decompositions cross /// in ONE call — including the sum (`Report::outcome`) each one carries, whose /// `match` belongs inside the arm that binds the report. diff --git a/examples/perftest-kotlin/src/generated_bindings.rs b/examples/perftest-kotlin/src/generated_bindings.rs index f82ee799..a8bba3e9 100644 --- a/examples/perftest-kotlin/src/generated_bindings.rs +++ b/examples/perftest-kotlin/src/generated_bindings.rs @@ -248,7 +248,35 @@ pub(crate) unsafe extern "C" fn Java_io_prebindgen_perftest_JNINative_payloadVec unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn Box_String_to_JString_027f6250<'a>( + env: &mut jni::JNIEnv<'a>, + v: Box, +) -> ::core::result::Result, __JniErr> { + Ok({ + env.new_string(v.as_str()) + .map_err(|e| { + <__JniErr as ::core::convert::From< + String, + >>::from(format!("encode_str: {}", e)) + })? + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -284,7 +312,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary16_e9d41606<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -320,7 +350,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary2_a8f288cc<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -356,7 +388,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary32_ed80fac3<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -392,7 +426,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary4_ea3fd497<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -428,7 +464,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary64Object_ecaf00ac<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -464,7 +502,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary64_b2751ca5<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -500,7 +540,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundary8_55b82b02<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -528,7 +570,9 @@ pub(crate) unsafe fn JObject_to_ObjectBoundaryLeaf_93531764<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -589,7 +633,9 @@ pub(crate) unsafe fn JObject_to_Payload_98f64326<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -631,7 +677,9 @@ pub(crate) unsafe fn JObject_to_Vec_Payload_8b7084d2<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -906,7 +954,9 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Payload_Send_Sync_static_95073668<'env, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1080,22 +1130,27 @@ pub(crate) unsafe fn JObject_to_impl_Fn_Payload_Send_Sync_static_96d50906<'env, unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, clippy::eq_op )] -pub(crate) unsafe fn JString_to_Option_Box_String_071e4c8c<'env, 'v>( +pub(crate) unsafe fn JString_to_Box_String_027f6250<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JString<'v>, -) -> ::core::result::Result>, __JniErr> { +) -> ::core::result::Result, __JniErr> { Ok({ - if v.is_null() { - None - } else { - Some(JString_to_std_boxed_Box_std_string_String_cfbab680(env, v)?) - } + let s = env + .get_string(v) + .map_err(|e| { + <__JniErr as ::core::convert::From< + String, + >>::from(format!("decode_string: {}", e)) + })?; + ::std::string::String::from(s).into() }) } #[allow( @@ -1103,25 +1158,27 @@ pub(crate) unsafe fn JString_to_Option_Box_String_071e4c8c<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, clippy::eq_op )] -pub(crate) unsafe fn JString_to_std_boxed_Box_std_string_String_cfbab680<'env, 'v>( +pub(crate) unsafe fn JString_to_Option_Box_String_071e4c8c<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JString<'v>, -) -> ::core::result::Result<::std::boxed::Box<::std::string::String>, __JniErr> { +) -> ::core::result::Result>, __JniErr> { Ok({ - let s = env - .get_string(v) - .map_err(|e| { - <__JniErr as ::core::convert::From< - String, - >>::from(format!("decode_string: {}", e)) - })?; - ::std::boxed::Box::new(::std::string::String::from(s)) + let __v: ::core::option::Option> = { + if v.is_null() { + None + } else { + Some(JString_to_Box_String_027f6250(env, v)?) + } + }; + __v }) } #[allow( @@ -1129,7 +1186,9 @@ pub(crate) unsafe fn JString_to_std_boxed_Box_std_string_String_cfbab680<'env, ' unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1240,7 +1299,9 @@ pub(crate) unsafe fn ObjectBoundary16_to_JObject_e9d41606<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1281,7 +1342,9 @@ pub(crate) unsafe fn ObjectBoundary2_to_JObject_a8f288cc<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1472,7 +1535,9 @@ pub(crate) unsafe fn ObjectBoundary32_to_JObject_ed80fac3<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1523,7 +1588,9 @@ pub(crate) unsafe fn ObjectBoundary4_to_JObject_ea3fd497<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -1958,7 +2025,9 @@ pub(crate) unsafe fn ObjectBoundary64Object_to_JObject_ecaf00ac<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2393,7 +2462,9 @@ pub(crate) unsafe fn ObjectBoundary64_to_JObject_b2751ca5<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2464,7 +2535,9 @@ pub(crate) unsafe fn ObjectBoundary8_to_JObject_55b82b02<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2495,7 +2568,9 @@ pub(crate) unsafe fn ObjectBoundaryLeaf_to_JObject_93531764<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2506,11 +2581,12 @@ pub(crate) unsafe fn Option_Box_String_to_JString_071e4c8c<'a>( v: Option>, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => { - std_boxed_Box_std_string_String_to_JString_cfbab680(env, value)? + let v: Option> = v; + { + match v { + Some(value) => Box_String_to_JString_027f6250(env, value)?, + None => jni::objects::JObject::null().into(), } - None => jni::objects::JObject::null().into(), } }) } @@ -2519,7 +2595,9 @@ pub(crate) unsafe fn Option_Box_String_to_JString_071e4c8c<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2530,9 +2608,12 @@ pub(crate) unsafe fn Option_Payload_to_JObject_97036642<'a>( v: Option, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => Payload_to_JObject_98f64326(env, value)?, - None => jni::objects::JObject::null().into(), + let v: Option = v; + { + match v { + Some(value) => Payload_to_JObject_98f64326(env, value)?, + None => jni::objects::JObject::null().into(), + } } }) } @@ -2541,7 +2622,9 @@ pub(crate) unsafe fn Option_Payload_to_JObject_97036642<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2552,9 +2635,12 @@ pub(crate) unsafe fn Option_Vec_Payload_to_JObject_b9a4637e<'a>( v: Option>, ) -> ::core::result::Result, __JniErr> { Ok({ - match v { - Some(value) => Vec_Payload_to_JObject_8b7084d2(env, value)?, - None => jni::objects::JObject::null().into(), + let v: Option> = v; + { + match v { + Some(value) => Vec_Payload_to_JObject_8b7084d2(env, value)?, + None => jni::objects::JObject::null().into(), + } } }) } @@ -2563,7 +2649,9 @@ pub(crate) unsafe fn Option_Vec_Payload_to_JObject_b9a4637e<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2580,7 +2668,9 @@ pub(crate) unsafe fn PayloadHandler_to_jlong_d61fd890<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2597,7 +2687,9 @@ pub(crate) unsafe fn PayloadVecHandler_to_jlong_b32d2812<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2637,7 +2729,9 @@ pub(crate) unsafe fn Payload_to_JObject_25cd94ea<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2685,7 +2779,9 @@ pub(crate) unsafe fn Payload_to_JObject_98f64326<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2702,7 +2798,9 @@ pub(crate) unsafe fn Storage_to_jlong_1b233abd<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2719,7 +2817,9 @@ pub(crate) unsafe fn TokenGc_to_jlong_5e58352a<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2736,7 +2836,9 @@ pub(crate) unsafe fn Token_to_jlong_4f7adafa<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2747,6 +2849,7 @@ pub(crate) unsafe fn Vec_Payload_to_JObject_8b7084d2<'a>( v: Vec, ) -> ::core::result::Result, __JniErr> { Ok({ + let v: Vec = v; let __list_obj = env .new_object("java/util/ArrayList", "()V", &[]) .map_err(|e| <__JniErr as ::core::convert::From< @@ -2773,7 +2876,9 @@ pub(crate) unsafe fn Vec_Payload_to_JObject_8b7084d2<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2790,7 +2895,9 @@ pub(crate) unsafe fn bool_to_jboolean_31306d98<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2807,7 +2914,9 @@ pub(crate) unsafe fn f64_to_jdouble_9e4a8f70<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2824,7 +2933,9 @@ pub(crate) unsafe fn i32_to_jint_a3e3b6ef<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2841,7 +2952,9 @@ pub(crate) unsafe fn i64_to_jlong_fbf9a9bc<'a>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2858,7 +2971,9 @@ pub(crate) unsafe fn jboolean_to_bool_31306d98<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2875,7 +2990,9 @@ pub(crate) unsafe fn jdouble_to_f64_9e4a8f70<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2892,7 +3009,9 @@ pub(crate) unsafe fn jint_to_i32_a3e3b6ef<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2916,7 +3035,9 @@ pub(crate) unsafe fn jlong_to_PayloadHandler_d61fd890<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2940,7 +3061,9 @@ pub(crate) unsafe fn jlong_to_PayloadVecHandler_b32d2812<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2964,7 +3087,9 @@ pub(crate) unsafe fn jlong_to_Storage_1b233abd<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -2988,7 +3113,9 @@ pub(crate) unsafe fn jlong_to_TokenGc_5e58352a<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -3012,7 +3139,9 @@ pub(crate) unsafe fn jlong_to_Token_4f7adafa<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -3029,31 +3158,9 @@ pub(crate) unsafe fn jlong_to_i64_fbf9a9bc<'env, 'v>( unused_mut, unused_variables, unused_braces, + unused_parens, dead_code, - clippy::needless_question_mark, - clippy::let_and_return, - clippy::nonminimal_bool, - clippy::eq_op -)] -pub(crate) unsafe fn std_boxed_Box_std_string_String_to_JString_cfbab680<'a>( - env: &mut jni::JNIEnv<'a>, - v: ::std::boxed::Box<::std::string::String>, -) -> ::core::result::Result, __JniErr> { - Ok({ - env.new_string(v.as_str()) - .map_err(|e| { - <__JniErr as ::core::convert::From< - String, - >>::from(format!("encode_str: {}", e)) - })? - }) -} -#[allow( - non_snake_case, - unused_mut, - unused_variables, - unused_braces, - dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 558e9c96..fcd49138 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -66,9 +66,9 @@ 1 api/lang/jnigen/jni/prim.rs 3 api/lang/jnigen/jni/prim_array.rs 8 api/lang/jnigen/jni/render.rs -8 api/lang/jnigen/jni/selector.rs +3 api/lang/jnigen/jni/selector.rs 11 api/lang/jnigen/jni/trait_impl.rs 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 135 +# total: 130 diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index 44864f94..54b7fb18 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -181,7 +181,10 @@ pub use self::{ }, origin::Origin, spelling::{canonical_spelling, canonical_type, type_from_ident}, - ty::{RefMode, ScalarKind, TypeId, TypeKind, TypeRef, UnsupportedType, UnsupportedTypeReason}, + ty::{ + peel_transparent, RefMode, ScalarKind, TypeId, TypeKind, TypeRef, UnsupportedType, + UnsupportedTypeReason, TRANSPARENT_WRAPPERS, + }, }; use crate::SourceLocation; diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 080c8c39..4d227651 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -548,6 +548,38 @@ impl std::error::Error for UnsupportedType {} /// `at` is the origin of the item this type was written in — the location every /// node lowered from that item shares, and the crate an array extent's const /// must come from. +/// The wrappers this language **erases**: `W` classifies as whatever `T` +/// classifies as, because no destination language can tell them apart. +/// +/// The single source of truth for that set. [`lower_type`] erases exactly these, +/// and an adapter that has to *undo* one in generated Rust reads the same list — +/// so the question "which wrappers are transparent?" has one answer instead of a +/// copy per consumer that can drift out of step. +/// +/// Adding one is adding a row here. What it means for a given destination is +/// that adapter's business: erasing a wrapper says nothing about whether Rust +/// can move a value out of it, which is why `Cow` is on this list and is still +/// refused where a converter would have to move its payload. +pub const TRANSPARENT_WRAPPERS: &[&str] = &["Box", "Cow"]; + +/// Strip one [transparent wrapper](TRANSPARENT_WRAPPERS) from a **spelling**, +/// naming the one removed — `Box>` → `("Box", Option)`. +/// +/// Spelling in, spelling out: this is the inverse of the erasure, for a consumer +/// that must reconstruct in Rust what the classification dropped. +pub fn peel_transparent(ty: &syn::Type) -> Option<(&'static str, syn::Type)> { + let syn::Type::Path(tp) = ty else { return None }; + let seg = tp.path.segments.last()?; + let name = TRANSPARENT_WRAPPERS.iter().find(|w| seg.ident == **w)?; + let syn::PathArguments::AngleBracketed(ab) = &seg.arguments else { + return None; + }; + ab.args.iter().find_map(|a| match a { + syn::GenericArgument::Type(inner) => Some((*name, inner.clone())), + _ => None, + }) +} + pub(crate) fn lower_type( ty: &syn::Type, consts: &ConstIndex, @@ -715,23 +747,10 @@ fn lower_path( // `Box` survives in `TypeRef::origin`, which is what generated // Rust spells. (A shared-ownership handle would classify as a // `Ref` for the same reason, when the language accepts one.) - "Box" => { - arity(1)?; - return Ok(args.remove(0).kind); - } - // `Cow<'_, T>` **is** `T`, for the same reason `Box` is: borrowed - // or owned, and no destination language can tell. Both adapters - // already say exactly that — cbindgen lowers it "just like `Vec` - // outputs", and jnigen's converter body is - // `byte_array_from_slice(&v)`, which works by deref and is identical - // to the `Vec` one. So this classification *predicts* their - // behaviour instead of leaving it a special case. - // - // The `Cow` survives in `TypeRef::origin`, which is where an adapter - // reads the param type its generated fn must spell — and it must, - // since `Cow<'_, [u8]>` is not interchangeable with `Vec` in a - // Rust signature. - "Cow" => { + // `Box` and `Cow` are erased — see `TRANSPARENT_WRAPPERS`, + // which is the list this arm consults so the set cannot drift + // from the one adapters undo. + w if TRANSPARENT_WRAPPERS.contains(&w) => { arity(1)?; return Ok(args.remove(0).kind); } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs b/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs index 5392477e..87c58cde 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/mod.rs @@ -106,10 +106,12 @@ mod destructure_ledger { const LEDGER: &[(&str, usize)] = &[ ("callback.rs", 0), // The `Option` output converters' niche and boxed-primitive arms. - // They destructure `v`, the converter's own parameter — whose Rust type - // is the crossing's SPELLING. Correct today only because a wrapped - // spelling gets no converter at all (#270); if that is fixed and - // `Box>` becomes a crossing, these two need coercing. + // They destructure `v`, the converter's own parameter — and #270 made a + // wrapped spelling a real crossing, so that prediction came due. They + // are correct now for the reason the note said they were not: the + // caller (`output_wrapper_shape`) binds the CANONICAL `Option` from + // the spelling before handing the body `v`, so what these destructure + // is an `Option` by construction rather than by luck. ("convert.rs", 2), // 2 fn-return matches (owned), 2 leaf reaches (one coerced, one owned // accessor return), 1 owned identity move, 1 emitter-bound local. diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs index 156b8f5f..eba5dab9 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs @@ -236,9 +236,12 @@ pub(crate) fn annotate_jobject_with_lifetime(ty: &syn::Type, life: &str) -> syn: // Helpers // ────────────────────────────────────────────────────────────────────── -pub(crate) fn pat_match(ty: &syn::Type, pat: &str) -> bool { - ty.to_token_stream().to_string() == pat -} +// `pat_match` lived here — `ty.to_token_stream().to_string() == pat` — and was +// how the converter selector decided what a type WAS: rebuild a wildcard +// pattern from the spelling, render it to a string, compare. That made the +// answer depend on how Rust happened to spell the type, so `Box>` +// reconstructed as `Box<_>`, matched nothing, and got no converter at all +// (#270). Dispatch reads `TypeKind` now; nothing needs it. /// `true` if `ty` is a path whose final segment is `name` (e.g. `Vec<_>` for /// `name = "Vec"`, `Option<&T>` for `name = "Option"`). Ignores generic args. diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index f898510c..3dc48ba5 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -635,7 +635,7 @@ mod prim_array; mod selector; #[cfg(test)] mod tests; -mod trait_impl; +pub(crate) mod trait_impl; mod fn_plan; mod fold; diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index ee67ecc3..cb29bc52 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -1,38 +1,8 @@ //! Structural converter-selection policy for [`Declarations`]. -use super::*; +use super::{trait_impl::WrapperShape, *}; use crate::api::core::registry::Conversions; -/// Clone a single-type-arg generic (`Option` / `Vec` / any `Path`) -/// replacing its last segment's first type argument with `repl` — yielding the -/// canonical shape (`Option<_>`) the built-in wrapper handlers key on, with the -/// type's own path/qualification preserved exactly. -fn with_first_arg(ty: &syn::Type, repl: syn::Type) -> syn::Type { - let mut out = ty.clone(); - if let syn::Type::Path(tp) = &mut out { - if let Some(seg) = tp.path.segments.last_mut() { - if let syn::PathArguments::AngleBracketed(ab) = &mut seg.arguments { - for a in ab.args.iter_mut() { - if let syn::GenericArgument::Type(t) = a { - *t = repl; - break; - } - } - } - } - } - out -} - -/// Clone a reference type replacing its referent with the `_` wildcard, -/// preserving the lifetime and mutability (`&'a T` → `&'a _`, `&mut T` → -/// `&mut _`) so the reconstructed pattern matches what the enumerator emitted. -fn ref_wildcard(r: &syn::TypeReference) -> syn::Type { - let mut pr = r.clone(); - *pr.elem = syn::parse_quote!(_); - syn::Type::Reference(pr) -} - /// Whether a decoded `Vec` local can be borrowed where `referent` is expected. /// /// A **spelling** question, deliberately: it decides what the generated Rust must @@ -52,6 +22,20 @@ fn decoded_vec_satisfies(referent: &syn::Type) -> bool { } } +/// Whether a spelling has no size, so no by-value converter can name it. +/// +/// A **spelling** question, like [`decoded_vec_satisfies`]: `[T]` and `Vec` +/// are one concept to the model — both `Sequence` — and Rust can return only +/// one of them. A bare slice is reached exclusively through a borrow, whose own +/// arm handles it; claiming it here would generate `fn f(..) -> [T]`. +/// +/// `str` is the same shape of fact and is handled the same way, one layer up: +/// its terminal arm resolves it to the borrowed `&str` converter rather than +/// pretending an owned `str` exists. +fn is_unsized_spelling(ty: &syn::Type) -> bool { + matches!(ty, syn::Type::Slice(_)) +} + impl Declarations { /// Select the input converter for `ty`: terminals, user wrappers, then /// built-in structural wrappers. @@ -62,13 +46,14 @@ impl Declarations { ) -> Option> { use crate::api::core::flat::RefMode; - // The spelling, for the wildcard patterns this selector builds. Those are - // the adapter's own — `Option<_>`, `&_` — so composing them from tokens is - // spelling, not reasoning. What the type *is* comes from `ty` below. + // What the type IS comes from `kind`; what generated Rust must SPELL it + // comes from here. The converter yields this spelling, so a + // `Box>` crossing produces a `Box>` — the shape it + // is dispatched as no longer decides what it is called. let syntax = &ty.origin.syntax; // 1. Terminal categories (incl. the terminal user-wrapper lookup). - if let Some(c) = self.input_terminal(syntax, registry) { + if let Some(c) = self.input_terminal(ty, registry) { return Some(c); } // 3. Built-in wrapper shapes, read one layer at a time rather than as a @@ -76,31 +61,57 @@ impl Declarations { // through `subs` to be selected on its own. Peeling further here would // claim a shape this selector does not emit. if let Some(inner) = ty.optional_inner() { - // `Option<&T>` tries the DEEP `Option<&_>` (borrowed-handle → - // `Option>`) before the shallow `Option<_>`; the shape + // `Option<&T>` tries the DEEP `OptionRef` (borrowed-handle → + // `Option>`) before the shallow `Optional`; the shape // that resolves correctly wins. if let Some(target) = inner.borrow_target() { - if let syn::Type::Reference(r) = &inner.origin.syntax { - let pat = with_first_arg(syntax, ref_wildcard(r)); - let t1 = target.origin.syntax.clone(); - if let Some(mut c) = self.input_wrapper_shape(&pat, &t1, registry) { - c.subs = vec![t1]; - return Some(c); + let mutable = matches!( + inner.kind, + crate::api::core::flat::TypeKind::Ref { + mode: RefMode::Exclusive, + .. } + ); + let t1 = target.origin.syntax.clone(); + if let Some(mut c) = self.input_wrapper_shape( + WrapperShape::OptionRef { mutable }, + syntax, + &t1, + registry, + ) { + c.subs = vec![t1]; + return Some(c); + } + } + // An optional BORROW is the deep handler's alone. It declined — + // either the inner is not a handle (then the shallow handler below + // is right, and only for the canonical spelling) or the spelling + // carries a wrapper it cannot bridge. The shallow handler cannot + // tell those apart and would decode the jlong as a `*mut &T`, so a + // wrapped optional borrow stops here rather than resolving wrong. + if inner.borrow_target().is_some() { + let canonical: syn::Type = { + let b = &inner.origin.syntax; + syn::parse_quote!(Option<#b>) + }; + if syntax.to_token_stream().to_string() != canonical.to_token_stream().to_string() { + return None; } } - let pat = with_first_arg(syntax, syn::parse_quote!(_)); let inner_ty = inner.origin.syntax.clone(); - if let Some(mut c) = self.input_wrapper_shape(&pat, &inner_ty, registry) { + if let Some(mut c) = + self.input_wrapper_shape(WrapperShape::Optional, syntax, &inner_ty, registry) + { c.subs = vec![inner_ty]; return Some(c); } return None; } - if let Some(elem) = ty.sequence_elem() { - let pat = with_first_arg(syntax, syn::parse_quote!(_)); + if let Some(elem) = ty.sequence_elem().filter(|_| !is_unsized_spelling(syntax)) { let elem_ty = elem.origin.syntax.clone(); - if let Some(mut c) = self.input_wrapper_shape(&pat, &elem_ty, registry) { + if let Some(mut c) = + self.input_wrapper_shape(WrapperShape::Sequence, syntax, &elem_ty, registry) + { c.subs = vec![elem_ty]; return Some(c); } @@ -130,21 +141,29 @@ impl Declarations { if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(&inner.origin.syntax) { if let Some(elem) = inner.sequence_elem() { let elem_ty = elem.origin.syntax.clone(); - let pat: syn::Type = syn::parse_quote!(Vec<_>); - if let Some(mut c) = self.input_wrapper_shape(&pat, &elem_ty, registry) { + // The one place `produced` is NOT the crossing's spelling: + // there is no owned `[T]` to decode into, so the converter + // yields an owned `Vec` and the call site borrows it. + let produced: syn::Type = syn::parse_quote!(Vec<#elem_ty>); + if let Some(mut c) = self.input_wrapper_shape( + WrapperShape::Sequence, + &produced, + &elem_ty, + registry, + ) { c.subs = vec![elem_ty]; return Some(c); } return None; } } - if let syn::Type::Reference(r) = syntax { - let pat = ref_wildcard(r); - let t1 = inner.origin.syntax.clone(); - if let Some(mut c) = self.input_wrapper_shape(&pat, &t1, registry) { - c.subs = vec![t1]; - return Some(c); - } + let mutable = matches!(mode, RefMode::Exclusive); + let t1 = inner.origin.syntax.clone(); + if let Some(mut c) = + self.input_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, &t1, registry) + { + c.subs = vec![t1]; + return Some(c); } } None @@ -154,9 +173,19 @@ impl Declarations { /// built-in structural wrappers. pub(crate) fn select_output_type( &self, - ty: &syn::Type, + ty: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + use crate::api::core::flat::RefMode; + + // What the type IS comes from `kind`; the spelling is what generated + // Rust must say. This direction used to be handed only the spelling — + // `convert_crossing` fetched the reading and threw it away — so it + // detected its layers with `option_inner_type`/`vec_inner_type`, which + // read the last path segment's ident. A `Box>` answered + // "neither", and got no converter at all (#270). + let syntax = &ty.origin.syntax; + // 1. Terminal categories (incl. the terminal user-wrapper lookup). if let Some(c) = self.output_terminal(ty, registry) { return Some(c); @@ -165,42 +194,51 @@ impl Declarations { // Read off the model, which calls this shape `TypeKind::Fallible`. // `result_parts` covers a `Result` the adapter composed itself, which // the frontend never read. - if let Some((ok, err)) = fallible_parts(ty, registry) { - if let Some(c) = self.result_peel(ty, &ok, &err, registry) { + if let Some((ok, err)) = fallible_parts(syntax, registry) { + if let Some(c) = self.result_peel(syntax, &ok, &err, registry) { return Some(c); } } - // 3. Built-in wrapper shapes (`Option<_>`, `Vec<_>`, `&T` borrow). An - // `Option<&Handle>` resolves via the shallow `Option<_>` whose inner - // converter is the `&Handle` borrow entry (no deep output handler). - if let Some(inner) = option_inner_type(ty) { - let pat = with_first_arg(ty, syn::parse_quote!(_)); - if let Some(mut c) = self.output_wrapper_shape(&pat, &inner, registry) { - c.subs = vec![inner]; + // 3. Built-in wrapper shapes, dispatched on what the model says the + // type IS. An `Option<&Handle>` resolves via the shallow `Optional` + // whose inner converter is the `&Handle` borrow entry (no deep + // output handler). + if let Some(inner) = ty.optional_inner() { + let inner_ty = inner.origin.syntax.clone(); + if let Some(mut c) = + self.output_wrapper_shape(WrapperShape::Optional, syntax, &inner_ty, registry) + { + c.subs = vec![inner_ty]; return Some(c); } return None; } - if let Some(elem) = vec_inner_type(ty) { - let pat = with_first_arg(ty, syn::parse_quote!(_)); - if let Some(mut c) = self.output_wrapper_shape(&pat, &elem, registry) { - c.subs = vec![elem]; + if let Some(elem) = ty.sequence_elem().filter(|_| !is_unsized_spelling(syntax)) { + let elem_ty = elem.origin.syntax.clone(); + if let Some(mut c) = + self.output_wrapper_shape(WrapperShape::Sequence, syntax, &elem_ty, registry) + { + c.subs = vec![elem_ty]; return Some(c); } return None; } - if let syn::Type::Reference(r) = ty { - // `&[T]` shared slice (a callback argument crossing native→JVM): build a - // `List` from the borrowed slice. Dual of the `&[T]` input branch. - if r.mutability.is_none() { - if let syn::Type::Slice(s) = &*r.elem { - let elem = (*s.elem).clone(); - return self.output_slice(&elem, registry); + if let crate::api::core::flat::TypeKind::Ref { mode, inner } = &ty.kind { + // `&[T]` shared slice (a callback argument crossing native→JVM): + // build a `List` from the borrowed slice. Dual of the `&[T]` + // input branch, and the same split: `kind` says it is a borrow of a + // run of values; whether the generated Rust can iterate the borrow + // directly is a question about the SPELLING. + if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(&inner.origin.syntax) { + if let Some(elem) = inner.sequence_elem() { + return self.output_slice(&elem.origin.syntax, registry); } } - let pat = ref_wildcard(r); - let t1 = (*r.elem).clone(); - if let Some(mut c) = self.output_wrapper_shape(&pat, &t1, registry) { + let mutable = matches!(mode, RefMode::Exclusive); + let t1 = inner.origin.syntax.clone(); + if let Some(mut c) = + self.output_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, &t1, registry) + { c.subs = vec![t1]; return Some(c); } diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index a623b044..c358f13e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -2308,3 +2308,353 @@ fn the_declarator_and_the_accessor_s_receiver_must_agree() { "`.fields` on a by-value accessor must be refused, naming it: {msg:?}" ); } + +/// A field's optional-ness is `kind`'s; how Rust spells it is the source's, +/// and a **whole-value crossing** must not care either — the half #268 could +/// not reach. +/// +/// #268 fixed the *access path* for a decomposed child. A field with no +/// deconstructor needs its own converter, and converter selection dispatched by +/// rebuilding a pattern from the spelling: `with_first_arg(Box>)` +/// yielded `Box<_>`, which matched no handler, so the crossing got no converter +/// at all and failed resolution by name (#270). +/// +/// Both spellings now resolve, and the converter takes the type the source +/// actually wrote — declaring `Option` for a `Box>` value would +/// mismatch its own call site. +#[test] +fn a_whole_value_crossing_ignores_how_rust_spells_it() { + let loc = myflat_loc(); + let build = |field_ty: syn::Type| -> String { + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZStamp { + pub secs: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZSampleStruct { + pub stamp: #field_ty, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_to_struct(s: &ZSample) -> ZSampleStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_sub(cb: impl Fn(ZSample) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .class(crate::data_class!(ZStamp)) + .fun(crate::fun!(z_sample_sub)), + ) + .expand(crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_struct))); + let dir = unique_test_dir("jnigen_vf_whole_boxed"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + // Resolving at all is half the assertion: this is what #270 reported as + // `Unresolved { key: "Box < Option < ZStamp > >" }`. + let gen = jni.build_with(registry).expect("resolve"); + std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust")) + .expect("read rust") + }; + + let plain = build(syn::parse_quote!(Option)); + let boxed = build(syn::parse_quote!(Box>)); + + let bc: String = boxed.split_whitespace().collect(); + // The converter takes what the source wrote... + assert!( + bc.contains("v:Box>"), + "the converter takes the spelled type:\n{boxed}" + ); + // ...and reads the canonical shape out of it before destructuring. + assert!( + bc.contains("letv:Option="), + "the spelling is read as the canonical shape:\n{boxed}" + ); + // The Kotlin surface is the wrapper's business only in Rust: both + // spellings deliver the same nullable data class. + for (label, rust) in [("Option", &plain), ("Box>", &boxed)] { + assert!( + rust.contains("ZStamp_to_JObject"), + "{label}: the field still crosses as its own converter:\n{rust}" + ); + } +} + +/// An owned string crosses the same however Rust spells it, with no +/// per-spelling arm behind it. +/// +/// `Box` used to work only because two `TypeKey == "Box < String >"` +/// matches were written by hand — one per direction. That is what a +/// spelling-keyed converter table costs: a hardcoded case for every +/// representation someone happens to write. Both are deleted; `kind == Str` +/// dispatches, the signature comes from the spelling, and `.into()` constructs +/// it. +#[test] +fn an_owned_string_crosses_the_same_however_rust_spells_it() { + let loc = myflat_loc(); + let build = |field_ty: syn::Type| -> String { + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZSampleStruct { + pub label: #field_ty, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_to_struct(s: &ZSample) -> ZSampleStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_sub(cb: impl Fn(ZSample) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .fun(crate::fun!(z_sample_sub)), + ) + .expand(crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_struct))); + let dir = unique_test_dir("jnigen_vf_boxed_string"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = jni.build_with(registry).expect("resolve"); + let (rust, kotlin) = ( + std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust")) + .expect("read rust"), + gen.write_kotlin(&dir.join("kotlin")) + .expect("write_kotlin") + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"), + ); + format!("{rust}\n// ---KOTLIN---\n{kotlin}") + }; + + for spelling in [ + syn::parse_quote!(String), + syn::parse_quote!(Box), + syn::parse_quote!(Cow<'static, str>), + ] { + let out = build(spelling); + assert!( + out.contains("String"), + "every owned-string spelling crosses as a Kotlin String:\n{out}" + ); + } +} + +/// A transparent wrapper is bridged only where generated Rust **can** bridge +/// it, and an unsupported representation is refused at selection rather than +/// emitted as code that will not compile. +/// +/// The model erases more than `Box`: `Cow<'_, T>` *is* `T` too. But a converter +/// that MOVES its payload has to undo the exact wrapper the source wrote, and +/// there is no trait for that — `Box → T` is `*b`, `Cow<'_, T> → T::Owned` +/// is `into_owned()`, and a `Cow` cannot be moved through at all (`E0507`). +/// Layers are counted, too: `Box>>` is `Optional`, and one +/// dereference leaves `Box>`. +/// +/// Both shapes used to RESOLVE and emit `let v: Option = (*v);` — the +/// worst outcome available, because resolution succeeding is what tells the +/// binding its type is supported. Failing to resolve names the type; emitting +/// unbuildable Rust names nothing (#270 review). +#[test] +fn a_transparent_wrapper_is_bridged_only_where_it_can_be() { + let loc = myflat_loc(); + let build = |field_ty: syn::Type| -> Result { + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZSampleStruct { + pub f: #field_ty, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_to_struct(s: &ZSample) -> ZSampleStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_sub(cb: impl Fn(ZSample) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .fun(crate::fun!(z_sample_sub)), + ) + .expand(crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_struct))); + let dir = unique_test_dir("jnigen_vf_bridge"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + match jni.build_with(registry) { + Ok(g) => Ok(std::fs::read_to_string( + g.write_rust(dir.join("g.rs")).expect("write_rust"), + ) + .expect("read rust")), + Err(e) => Err(format!("{e}")), + } + }; + + // One box: bridged with one dereference. + let one = build(syn::parse_quote!(Box>)).expect("a single box is bridgeable"); + let oc: String = one.split_whitespace().collect(); + assert!( + oc.contains("letv:Option=(*v);"), + "one layer, one dereference:\n{one}" + ); + + // Two boxes: bridged with TWO. This is the case a single deref got wrong, + // silently — `(*v)` on `Box>` is still a `Box<_>`. + let two = + build(syn::parse_quote!(Box>>)).expect("nested boxes are bridgeable"); + let tc: String = two.split_whitespace().collect(); + assert!( + tc.contains("letv:Option=(*(*v));"), + "two layers, two dereferences:\n{two}" + ); + + // `Cow` is erased by the model and CANNOT be moved through, so the crossing + // must not resolve. The diagnosis names the type. + let cow = build(syn::parse_quote!(Cow<'static, Option>)) + .expect_err("a Cow payload cannot be moved out, so it must not resolve"); + assert!( + cow.contains("could not be resolved") && cow.contains("Cow"), + "the refusal names the unsupported representation: {cow}" + ); +} + +/// A wrapper cannot be bridged where the converter does not produce the spelled +/// type at all — the **borrow** shapes — so those refuse rather than resolve. +/// +/// `&T` and `Option<&T>` are served by handing back the inner type's own +/// converter (or an `OwnedObject`) and letting the call site add `&` / +/// `.as_deref()`. There is no value in hand to unwrap a representation from, so +/// `Box<&T>` would pass an owned `T` where `Box<&T>` is expected, and +/// `Box>` would decode the handle as `*mut &T`. Both used to resolve +/// (#272 review). +/// +/// The canonical spellings are asserted alongside, because a guard that also +/// refused those would be worse than no guard. +#[test] +fn a_wrapped_borrow_has_nothing_to_bridge_and_refuses() { + let loc = myflat_loc(); + let build = |param_ty: syn::Type| -> Result { + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZThing { + pub v: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_take(t: #param_ty) -> i64 { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZThing)) + .fun(crate::fun!(z_take)), + ); + let dir = unique_test_dir("jnigen_wrapped_borrow"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + match jni.build_with(registry) { + Ok(g) => Ok(std::fs::read_to_string( + g.write_rust(dir.join("g.rs")).expect("write_rust"), + ) + .expect("read rust")), + Err(e) => Err(format!("{e}")), + } + }; + + // The canonical borrows still resolve, and still adapt at the call site. + let borrowed = build(syn::parse_quote!(&ZThing)).expect("a plain borrow resolves"); + assert!( + borrowed.contains("myflat::z_take(&t)"), + "the call site adds the borrow:\n{borrowed}" + ); + let opt = build(syn::parse_quote!(Option<&ZThing>)).expect("an optional borrow resolves"); + assert!( + opt.contains("myflat::z_take(t.as_deref())"), + "the call site derefs the OwnedObject:\n{opt}" + ); + + // Wrapped, they have nothing to bridge and must not resolve. + for spelling in [ + syn::parse_quote!(Box<&ZThing>), + syn::parse_quote!(Box>), + ] { + let err = build(spelling).expect_err("a wrapped borrow must not resolve"); + assert!( + err.contains("could not be resolved"), + "the refusal names the type: {err}" + ); + } +} diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 401b855b..a6ef210a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -23,7 +23,14 @@ fn generated_converter_attr() -> syn::Attribute { unused_mut, unused_variables, unused_braces, + // A representation-agnostic converter says the same thing for every + // spelling, so the plain spelling gets the degenerate form of it: a + // reflexive `.into()`, a deref that is a no-op, parens around a value + // that needed none. Suppressing per-shape would mean asking which + // spelling this is, which is the guessing #270 removed. + unused_parens, dead_code, + clippy::useless_conversion, clippy::needless_question_mark, clippy::let_and_return, clippy::nonminimal_bool, @@ -612,13 +619,147 @@ pub(crate) fn build_handle_destructor_items( named.into_iter().map(|(_, item)| item).collect() } +/// Which built-in wrapper a converter is being built for — **the model's +/// answer, not a guess from the spelling**. +/// +/// This used to be a `&syn::Type` wildcard pattern (`Option<_>`, `& mut _`) +/// rebuilt from the type's tokens and compared as a *string*. That made the +/// dispatch depend on how Rust happened to spell the type: `Box>` +/// reconstructed as `Box<_>`, matched no pattern, and got no converter at all +/// (#270) — even though the model classifies it `Optional` and says so. +/// +/// So the shape comes from [`TypeKind`](crate::api::core::flat::TypeKind) and +/// the spelling comes from `origin.syntax`, which is the same split the rest of +/// the pipeline follows: classify off `kind`, spell off `syntax`. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) enum WrapperShape { + /// `Ref` — a borrow of its inner. + Borrow { mutable: bool }, + /// `Optional` whose inner is a `Ref` — the deep handle-borrow form, tried + /// before [`Self::Optional`]. + OptionRef { mutable: bool }, + /// `Sequence` — a run of its element. + Sequence, + /// `Optional` — its inner, or absent. + Optional, +} + +/// What generated Rust can do with one wrapper the model +/// [erases](crate::api::core::flat::TRANSPARENT_WRAPPERS). +/// +/// Erasure and reconstruction are different questions, and only the first is the +/// model's. `Box` *is* `T` to every destination language — but undoing it in +/// Rust is `*b`, undoing a `Cow` is `into_owned()`, and undoing an `Rc` is not +/// possible at all. There is no trait spanning those, so the operations live +/// here, one row per wrapper, instead of as a special case per converter. +/// +/// **Adding a wrapper is adding a row.** Put its name in +/// `TRANSPARENT_WRAPPERS` (the model decides what it erases) and a row here +/// (the adapter decides what it can rebuild); `every_erased_wrapper_has_ops` +/// fails if the two disagree, so a wrapper cannot become transparent without +/// this file having an answer for it. +struct WrapperOps { + /// Its last path segment, as `TRANSPARENT_WRAPPERS` spells it. + name: &'static str, + /// Move the inner value **out**. `None` when the representation does not + /// permit it — a `Cow` payload cannot be moved through `Deref` (`E0507`), + /// and neither can an `Rc`'s. + read: Option TokenStream>, + /// Build it **from** the inner value. `None` when not supported. + build: Option TokenStream>, +} + +/// The operations table. One row per wrapper the model erases. +const WRAPPER_OPS: &[WrapperOps] = &[ + WrapperOps { + name: "Box", + // `*b` moves out of a box, and `Box::new` puts it back. + read: Some(|e| quote!((*#e))), + build: Some(|e| quote!(::std::boxed::Box::new(#e))), + }, + WrapperOps { + name: "Cow", + // Reading would be `into_owned()`, which needs `B: ToOwned` — not + // implied by anything the model knows about the payload. Refused until + // something needs it; that is one row, not a redesign. + read: None, + build: None, + }, +]; + +fn wrapper_ops(name: &str) -> Option<&'static WrapperOps> { + WRAPPER_OPS.iter().find(|w| w.name == name) +} + +/// The chain of wrappers standing between a **spelling** and the canonical +/// shape its `kind` names, outermost first — empty when the source already +/// wrote the canonical form. +/// +/// `None` means the spelling is not a wrapping of the canonical one at all, so +/// no converter should claim it. +fn bridge_layers(spelling: &syn::Type, canonical: &syn::Type) -> Option> { + if spelling.to_token_stream().to_string() == canonical.to_token_stream().to_string() { + return Some(Vec::new()); + } + let (name, inner) = crate::api::core::flat::peel_transparent(spelling)?; + let ops = wrapper_ops(name)?; + let mut rest = bridge_layers(&inner, canonical)?; + rest.insert(0, ops); + Some(rest) +} + +/// Read the converter's `v` as the canonical shape, undoing each layer +/// outside-in. `None` when any layer cannot be read through — the crossing then +/// stays **unresolved**, naming the type, rather than resolving and emitting +/// Rust the consumer cannot build (#270 review). +fn read_as_canonical(produced: &syn::Type, canonical: &syn::Type) -> Option { + let layers = bridge_layers(produced, canonical)?; + let mut e = quote!(v); + for w in layers { + e = (w.read?)(e); + } + Some(e) +} + +/// Build the spelling from a canonical value — the input-side peer, applying +/// each layer inside-out. +fn build_from_canonical( + produced: &syn::Type, + canonical: &syn::Type, + value: TokenStream, +) -> Option { + let layers = bridge_layers(produced, canonical)?; + let mut e = value; + for w in layers.into_iter().rev() { + e = (w.build?)(e); + } + Some(e) +} + +/// Whether the source wrote the canonical spelling itself — no wrapper to undo. +/// +/// Required by the converters that do **not** produce the spelled type by +/// construction: the borrow shapes hand back the inner type's own converter (or +/// an `OwnedObject`) and let the call site add `&` / `.as_deref()`. There is no +/// value in hand to wrap or unwrap, so a wrapped spelling cannot be served +/// here at all and must not resolve. +fn is_canonical_spelling(produced: &syn::Type, canonical: &syn::Type) -> bool { + bridge_layers(produced, canonical).is_some_and(|l| l.is_empty()) +} + /// Per-shape **input** wrapper converter builders (`&`/`Option<&>`/`Vec`/ -/// `Option`). Each returns `Some(ConverterImpl)` only for the wildcard pattern -/// it claims; [`JniGenBuilder::input_wrapper_shape`] chains them in priority order. -/// Because [`pat_match`] is an exact match, the patterns are disjoint — except -/// the two `Option<_>` sub-cases (direct-handle-by-value vs general), which -/// share a pattern and so live together in [`JniGenBuilder::input_option`] to keep -/// their original fall-through. +/// `Option`). Each returns `Some(ConverterImpl)` only for the [`WrapperShape`] +/// it claims; [`Declarations::input_wrapper_shape`] chains them in priority +/// order. The shapes are disjoint — except the two `Optional` sub-cases +/// (direct-handle-by-value vs general), which share one and so live together in +/// [`Declarations::input_option`] to keep their original fall-through. +/// +/// Each takes `produced`: the Rust type the converter's function **yields**. +/// Normally that is the crossing's own spelling, so a `Box>` crossing +/// produces a `Box>` rather than silently declaring `Option` and +/// mismatching its call site. The one deliberate exception is a `&[T]` +/// parameter, which decodes to an owned `Vec` the call site borrows — see +/// [`Declarations::select_input_type`]. impl Declarations { /// `& _` / `& mut _` borrow: share T's resolved converter — `&T`'s entry /// points at the same `ItemFn` (the fn returns owned `T`; the call site in @@ -626,19 +767,29 @@ impl Declarations { /// wildcard-substitution machinery marks T required transitively from `&T`. fn input_borrow( &self, - pat: &syn::Type, + shape: WrapperShape, + produced: &syn::Type, t1: &syn::Type, registry: &impl Conversions, ) -> Option> { - if !(pat_match(pat, "& _") || pat_match(pat, "& mut _")) { + let WrapperShape::Borrow { mutable } = shape else { return None; - } - let inner = registry.input_entry(t1)?; - let outer_ty: syn::Type = if pat_match(pat, "& mut _") { + }; + // This converter does NOT produce the spelled type: it hands back the + // inner type's own entry, and the call site adds the `&`. So there is no + // value in hand to unwrap a representation from, and a wrapped spelling + // — `Box<&T>` — must not resolve here (it would pass an owned `T` where + // `Box<&T>` is expected). + let canonical: syn::Type = if mutable { syn::parse_quote!(&mut #t1) } else { syn::parse_quote!(&#t1) }; + if !is_canonical_spelling(produced, &canonical) { + return None; + } + let inner = registry.input_entry(t1)?; + let outer_ty = produced.clone(); // `&T` / `&mut T` are Kotlin-side no-ops — inherit the inner // type's name, unless the user pinned an explicit override // on the outer form itself (rare but legal). @@ -672,11 +823,23 @@ impl Declarations { /// over `&T` and the general handler takes it. fn input_option_ref( &self, - pat: &syn::Type, + shape: WrapperShape, + produced: &syn::Type, t1: &syn::Type, registry: &impl Conversions, ) -> Option> { - if !(pat_match(pat, "Option < & _ >") || pat_match(pat, "Option < & mut _ >")) { + let WrapperShape::OptionRef { mutable } = shape else { + return None; + }; + // Produces `Option>`, which the call site adapts with + // `.as_deref()` — again not the spelled type, so a wrapped spelling has + // nothing to bridge and must not resolve. See `input_borrow`. + let canonical: syn::Type = if mutable { + syn::parse_quote!(Option<&mut #t1>) + } else { + syn::parse_quote!(Option<&#t1>) + }; + if !is_canonical_spelling(produced, &canonical) { return None; } let inner = registry.input_entry(t1)?; @@ -684,14 +847,9 @@ impl Declarations { // Non-opaque: let the general `Option<_>` handler take it. return None; } - let is_mut = pat_match(pat, "Option < & mut _ >"); let inner_wire = inner.destination.clone(); let inner_conv = inner.function.sig.ident.clone(); - let outer_ty: syn::Type = if is_mut { - syn::parse_quote!(Option<&mut #t1>) - } else { - syn::parse_quote!(Option<&#t1>) - }; + let outer_ty = produced.clone(); let name = input_name(&outer_ty, &inner_wire); let gen_allow = generated_converter_attr(); let function: syn::ItemFn = syn::parse_quote!( @@ -733,11 +891,12 @@ impl Declarations { /// collect into a `Vec`. (`Vec` is special-cased at rank-0.) fn input_vec( &self, - pat: &syn::Type, + shape: WrapperShape, + produced: &syn::Type, t1: &syn::Type, registry: &impl Conversions, ) -> Option> { - if !pat_match(pat, "Vec < _ >") { + if shape != WrapperShape::Sequence { return None; } let inner = registry.input_entry(t1)?; @@ -753,7 +912,10 @@ impl Declarations { inner, quote::quote!(&__elem_wire), ); - let outer_ty: syn::Type = syn::parse_quote!(Vec<#t1>); + let outer_ty = produced.clone(); + let canonical: syn::Type = syn::parse_quote!(Vec<#t1>); + // Bridgeable first — see `box_layers_to`. + let build = build_from_canonical(produced, &canonical, quote::quote!(__out))?; let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); let body: syn::Expr = syn::parse_quote!({ let __list = jni::objects::JList::from_env(env, v) @@ -768,7 +930,7 @@ impl Declarations { let __elem: #t1 = #inner_conv; __out.push(__elem); } - __out + #build }); let inner_kotlin = inner.metadata.kotlin_name.clone()?; let kotlin_name = self.override_kotlin_name( @@ -797,15 +959,18 @@ impl Declarations { /// the original sequential fall-through. fn input_option( &self, - pat: &syn::Type, + shape: WrapperShape, + produced: &syn::Type, t1: &syn::Type, registry: &impl Conversions, ) -> Option> { - if pat_match(pat, "Option < _ >") { + if shape == WrapperShape::Optional { let inner = registry.input_entry(t1)?; if inner.metadata.is_direct_handle() { let inner_wire = inner.destination.clone(); - let outer_ty: syn::Type = syn::parse_quote!(Option<#t1>); + let outer_ty = produced.clone(); + let canonical: syn::Type = syn::parse_quote!(Option<#t1>); + let build = build_from_canonical(produced, &canonical, quote::quote!(__v))?; let name = input_name(&outer_ty, &inner_wire); let gen_allow = generated_converter_attr(); let function: syn::ItemFn = syn::parse_quote!( @@ -813,9 +978,9 @@ impl Declarations { pub(crate) unsafe fn #name<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &#inner_wire, - ) -> ::core::result::Result, __JniErr> { + ) -> ::core::result::Result<#outer_ty, __JniErr> { Ok({ - if *v == 0 { + let __v: ::core::option::Option<#t1> = if *v == 0 { None } else if (*v & 1) == 1 { // Tagged (closed) handle raced past the Kotlin @@ -828,7 +993,8 @@ impl Declarations { ); } else { Some(*std::boxed::Box::from_raw(*v as *mut #t1)) - } + }; + #build }) } ); @@ -856,9 +1022,17 @@ impl Declarations { } // Non-opaque inner: fall through to the general Option handler. } - if pat_match(pat, "Option < _ >") { - let outer_ty: syn::Type = syn::parse_quote!(Option<#t1>); - let (wire, body, niches) = option_input(t1, registry)?; + if shape == WrapperShape::Optional { + let outer_ty = produced.clone(); + let canonical: syn::Type = syn::parse_quote!(Option<#t1>); + let build = build_from_canonical(produced, &canonical, quote::quote!(__v))?; + let (wire, inner_body, niches) = option_input(t1, registry)?; + // `option_input` yields the canonical `Option`; the converter + // yields the spelling. + let body: syn::Expr = syn::parse_quote!({ + let __v: ::core::option::Option<#t1> = #inner_body; + #build + }); // Inherit the inner's name; user pins on `Option` win. // The nullability marker (`?`) is added by the use site. let inherited = registry @@ -1014,7 +1188,7 @@ impl Declarations { let args: Vec = args.iter().map(|a| a.origin.syntax.clone()).collect(); self.dispatch_fn_input(&args, built) }), - Direction::Output => self.select_output_type(&ty, built), + Direction::Output => self.select_output_type(&reading, built), } } @@ -1588,9 +1762,13 @@ impl Declarations { /// empty. pub(crate) fn input_terminal( &self, - ty: &syn::Type, + reading: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + // Classify off `kind`, spell off `syntax`: the arms below that ask what + // a type IS use `reading`, and everything that has to name it in + // generated Rust uses this. + let ty = &reading.origin.syntax; // Structured-config overrides first (opaque handles, then user- // registered rank-0 wrappers, then built-ins). let key = TypeKey::from_type(ty); @@ -1673,12 +1851,17 @@ impl Declarations { metadata: self.framework_meta(kotlin_name), }); } - // `Box`: a heap string carried as an opaque-pointer struct field - // (e.g. an FFI-safe `#[repr(C)]` struct's `Option>`). Decode - // the `JString` to an owned `String` and box it; surfaces as Kotlin - // `String` (and `Option>` composes to `String?` via the - // `Option<_>` wrapper). Dual of the `Box` output arm. - if TypeKey::from_type(ty).as_str() == "Box < String >" { + // Any OWNED string, however Rust spells it — `String`, `Box`, + // `Cow<'_, str>`. The model classifies each of them `Str`; the spelling is + // the source's business, and `.into()` constructs it from the decoded + // `String`. This used to be one hardcoded `TypeKey == "Box < String >"` + // arm, which is what a spelling-keyed converter table costs: one + // hand-written case per representation anyone happened to write (#270). + // + // `str` is handled above, separately and deliberately: it is unsized, + // so its converter yields an owned `String` the call site borrows — + // a different contract, not a different spelling. + if matches!(reading.kind, crate::api::core::flat::TypeKind::Str) { let wire: syn::Type = syn::parse_quote!(jni::objects::JString); let body: syn::Expr = syn::parse_quote!({ let s = env.get_string(v).map_err(|e| { @@ -1687,9 +1870,10 @@ impl Declarations { e )) })?; - ::std::boxed::Box::new(::std::string::String::from(s)) + // The canonical value, then the spelling. + ::std::string::String::from(s).into() }); - let rust_ty: syn::Type = syn::parse_quote!(::std::boxed::Box<::std::string::String>); + let rust_ty = ty.clone(); let kotlin_name = self.override_kotlin_name(ty, Some(kt::KtType::string())); let niches = default_niches_for_wire(&wire); return Some(ConverterImpl { @@ -1784,17 +1968,18 @@ impl Declarations { /// handlers. pub(crate) fn input_wrapper_shape( &self, - pat: &syn::Type, + shape: WrapperShape, + produced: &syn::Type, t1: &syn::Type, registry: &impl Conversions, ) -> Option> { - // Disjoint wildcard patterns (see the `impl JniGenBuilder` block above), tried - // in priority order. The borrow/option-ref/vec patterns are exact and - // mutually exclusive; the two `Option<_>` sub-cases share a method. - self.input_borrow(pat, t1, registry) - .or_else(|| self.input_option_ref(pat, t1, registry)) - .or_else(|| self.input_vec(pat, t1, registry)) - .or_else(|| self.input_option(pat, t1, registry)) + // Disjoint shapes (see [`WrapperShape`]), tried in priority order. The + // borrow/option-ref/vec shapes are mutually exclusive; the two + // `Optional` sub-cases share a method. + self.input_borrow(shape, produced, t1, registry) + .or_else(|| self.input_option_ref(shape, produced, t1, registry)) + .or_else(|| self.input_vec(shape, produced, t1, registry)) + .or_else(|| self.input_option(shape, produced, t1, registry)) } // ── Output converters ──────────────────────────────────────────── @@ -1804,9 +1989,11 @@ impl Declarations { /// `str`, `Cow<[u8]>`, unit, primitive, struct) — `subs` empty. pub(crate) fn output_terminal( &self, - ty: &syn::Type, + reading: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + // Classify off `kind`, spell off `syntax` — see `input_terminal`. + let ty = &reading.origin.syntax; // Structured-config overrides first (opaque handles, then built-ins). let key = TypeKey::from_type(ty); if let Some(cfg) = self.types.get(&key) { @@ -1867,19 +2054,23 @@ impl Declarations { if TypeKey::from_type(ty).as_str() == "str" { return Some(self.str_ref_output()); } - // `Box`: read the heap string through the box and encode it as a - // `JString`; surfaces as Kotlin `String` (and `Option>` → - // `String?` via the `Option<_>` wrapper). Dual of the `Box` - // input arm — together they let an opaque-pointer `String` struct field - // map to a plain Kotlin `String`. - if TypeKey::from_type(ty).as_str() == "Box < String >" { + // An owned string in any representation the model erases — `Box`, + // `Cow<'_, str>`. It classifies each of them `Str`, and the body was + // already representation-agnostic: `v.as_str()` reaches through any of + // them by `Deref`. Only the *dispatch* was spelling-keyed, as one + // hardcoded `TypeKey == "Box < String >"` arm (#270). + // + // Plain `String` keeps its own earlier arm in `primitive_output`, whose + // body this matches exactly; this one is reached for the wrapped + // spellings that arm's key cannot name. + if matches!(reading.kind, crate::api::core::flat::TypeKind::Str) { let wire: syn::Type = syn::parse_quote!(jni::objects::JString); let body: syn::Expr = syn::parse_quote!({ env.new_string(v.as_str()).map_err(|e| { <__JniErr as ::core::convert::From>::from(format!("encode_str: {}", e)) })? }); - let rust_ty: syn::Type = syn::parse_quote!(::std::boxed::Box<::std::string::String>); + let rust_ty = ty.clone(); let kotlin_name = self.override_kotlin_name(ty, Some(kt::KtType::string())); let niches = default_niches_for_wire(&wire); return Some(ConverterImpl { @@ -1904,7 +2095,7 @@ impl Declarations { // Wire is `()`. Body just returns `v`. No Kotlin name — Unit // returns are dropped from emitted signatures, so metadata stays // empty. - if pat_match(ty, "()") { + if matches!(reading.kind, crate::api::core::flat::TypeKind::Unit) { let wire: syn::Type = syn::parse_quote!(()); let body: syn::Expr = syn::parse_quote!(v); return Some(ConverterImpl { @@ -1964,7 +2155,8 @@ impl Declarations { /// `Option<&Handle>` resolves via the shallow `Option<_>`. pub(crate) fn output_wrapper_shape( &self, - pat: &syn::Type, + shape: WrapperShape, + produced: &syn::Type, t1: &syn::Type, registry: &impl Conversions, ) -> Option> { @@ -1977,7 +2169,7 @@ impl Declarations { // with a `.clone()`; `Option<&T>` then composes through the `Option` // arm below (it looks up this `&T` entry as its inner). Matched // structurally so the lifetime variant `&'static _` is covered too. - if let syn::Type::Reference(r) = pat { + if let syn::Type::Reference(r) = produced { if r.mutability.is_none() && self .types @@ -2006,7 +2198,7 @@ impl Declarations { // expansion). The single copy into the JVM is `&str → jstring` (no // intermediate owned `String`). The unsized `str` sub resolves via the // rank-0 arm to the same fn (see [`Self::str_ref_output`]). - if let syn::Type::Reference(r) = pat { + if let syn::Type::Reference(r) = produced { if r.mutability.is_none() && TypeKey::from_type(t1).as_str() == "str" { return Some(self.str_ref_output()); } @@ -2014,9 +2206,17 @@ impl Declarations { // `Result` is peeled by the selector, off the model's // `TypeKind::Fallible`. Bindings declare the `Err` type via // `.throwable()`. - if pat_match(pat, "Option < _ >") { - let outer_ty: syn::Type = syn::parse_quote!(Option<#t1>); - let (wire, body, niches) = option_output(t1, registry)?; + if shape == WrapperShape::Optional { + let outer_ty = produced.clone(); + let canonical: syn::Type = syn::parse_quote!(Option<#t1>); + // Bridgeable first: an unsupported representation must not resolve + // and then emit code the consumer cannot compile. + let read = read_as_canonical(produced, &canonical)?; + let (wire, inner_body, niches) = option_output(t1, registry)?; + let body: syn::Expr = syn::parse_quote!({ + let v: #canonical = #read; + #inner_body + }); let inherited = registry .output_entry(t1) .and_then(|e| e.metadata.kotlin_name.clone()); @@ -2059,7 +2259,7 @@ impl Declarations { // `Vec` (output side): encode as a `java.util.ArrayList`. // Symmetric to the input handler. `Vec` is special-cased at // rank-0 (primitive_output → JByteArray) so rank-1 never sees it. - if pat_match(pat, "Vec < _ >") { + if shape == WrapperShape::Sequence { let inner = registry.output_entry(t1)?; // `Vec` output is delivered by the Kotlin-side leaf // fold (`apply_leaf_vec_folds` → typed-handle wrap), so this @@ -2075,9 +2275,12 @@ impl Declarations { inner, quote::quote!(__elem), ); - let outer_ty: syn::Type = syn::parse_quote!(Vec<#t1>); + let outer_ty = produced.clone(); + let canonical: syn::Type = syn::parse_quote!(Vec<#t1>); + let read = read_as_canonical(produced, &canonical)?; let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); let body: syn::Expr = syn::parse_quote!({ + let v: #canonical = #read; let __list_obj = env .new_object("java/util/ArrayList", "()V", &[]) .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: new ArrayList: {}", e)))?; @@ -2365,3 +2568,51 @@ impl Declarations { .collect() } } + +#[cfg(test)] +mod wrapper_ops_tests { + use super::*; + + /// Every wrapper the model erases has a row here. + /// + /// The two lists answer different questions — the model's is "what do I + /// erase", this file's is "what can I rebuild" — and they are allowed to + /// disagree about *capability* (`Cow` is erased and cannot be read through). + /// They are not allowed to disagree about *membership*: a wrapper that + /// becomes transparent without a row here would be silently unbridgeable + /// everywhere, which looks exactly like a type the binding got wrong. + /// + /// So adding `Rc` is: one entry in `TRANSPARENT_WRAPPERS`, one row in + /// `WRAPPER_OPS` (`read: None` — an `Rc`'s payload cannot be moved out — + /// and `build: Some(Rc::new)`). This test is what says so out loud instead + /// of leaving the second step to be discovered. + #[test] + fn every_erased_wrapper_has_ops() { + let missing: Vec<&str> = crate::api::core::flat::TRANSPARENT_WRAPPERS + .iter() + .copied() + .filter(|w| wrapper_ops(w).is_none()) + .collect(); + assert!( + missing.is_empty(), + "the model erases {missing:?}, and this adapter has no `WrapperOps` row for them — \ + add one (`read`/`build` may be `None` when the representation does not allow it, \ + which refuses the shape instead of mis-generating it)" + ); + } + + /// …and nothing here claims a wrapper the model does not erase, which would + /// be an operation that can never run. + #[test] + fn no_ops_for_a_wrapper_the_model_keeps() { + let stray: Vec<&str> = WRAPPER_OPS + .iter() + .map(|w| w.name) + .filter(|n| !crate::api::core::flat::TRANSPARENT_WRAPPERS.contains(n)) + .collect(); + assert!( + stray.is_empty(), + "`WRAPPER_OPS` rows for non-erased {stray:?}" + ); + } +} From aad98feaa8090f79c6dd537981c347e6c53abbc8 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sat, 1 Aug 2026 23:11:55 +0200 Subject: [PATCH 26/52] Ask the model whether a type is optional, not the spelling (#274) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #273. Kotlin nullability was decided by `is_option_type`, which is `path_tail_is(ty, "Option")`. The model ERASES transparent wrappers — `Box` is `T`, and so is `Cow<'_, T>` — so `Box>` classifies `Optional` and that check answered `false`. Proven against the base commit: a wrapped parameter rendered `note: String` where the identical-meaning plain one rendered `note: String?`. Not cosmetic — Kotlin rejects `null` at the call site, so the absent case became unexpressible. `Conversions` gains `is_optional` / `optional_inner` / `sequence_elem` / `is_optional_borrow` as default methods over `reading`, the way `input_entry` sits over `conversion`. The rule has ONE home, so a new transparent wrapper is a change to `TRANSPARENT_WRAPPERS` and nothing else. Two of the eight migrated sites are not `?` decisions and are worth naming: `render.rs` picks a handle's LOCK MODE and `emit/callback.rs` picks a jvalue union member — a deadlock and an ABI mismatch respectively. `struct_plan::classify_field` peeled the same field seven times by name, beside the nullability decision it would then contradict; `builder.rs`'s niche loop re-keyed each peeled spelling. `is_option_ref` is deleted. Then the accessors were made mostly unnecessary rather than merely correct: the struct/data-class chain CARRIES the element. `build_struct_plan`, `classify_field`, both `flatten_struct_*`, `synth_value_struct_leaves`, `struct_output_body` and `render_data_class_source` take `&flat::Struct` / `&flat::TypeRef`; the sum walk zips `flat::Variant::alternatives`. Holding a `TypeRef` is proof the model classified the type, so asking about an unregistered one is now a COMPILE error, not a runtime miss answered `false`. Two entry points were already fetching the element and discarding it with `.map(|st| &st.origin.syntax)` — the #267 pattern — and two runtime guards (`Fields::Named` + an "unnamed field" panic) became unrepresentable, because a tuple struct is an `Extern` in the model, never a `Struct`. A census of the remaining spelling helpers under `api/lang/jnigen` keeps a new one from appearing silently. It walks tokens and resolves `use … as …` first: the sibling adapter aliases these exact helpers today (`cbindgen/mod.rs`: `is_option_type as is_option`), so an alias was the established idiom and would have slipped past. Verified by adding a call, a module, and an aliased call — each moves a number. Verified: 545 lib tests, `--all --all-features` 14/14, clippy, fmt. The regen diff is the evidence: `covertest.kt` is NOT in it — wrapping a parameter changes nothing the JVM can see — and the element refactor is byte-identical. covertest 48/48 on the JVM. Boundary ledger 130 -> 129. Follow-up: #275 (core's plan leaves carry `syn::Type`, which is what would let the accessors be deleted outright). --- .../io/prebindgen/covertest/model.kt | 10 +- .../src/generated_bindings.rs | 26 +- examples/perftest-flat/src/ext.rs | 14 +- prebindgen/src/api/core/flat/boundary.ledger | 4 +- prebindgen/src/api/core/registry/view.rs | 48 ++++ prebindgen/src/api/core/types_util.rs | 8 +- prebindgen/src/api/lang/jnigen/jni/builder.rs | 18 +- .../src/api/lang/jnigen/jni/emit/callback.rs | 2 +- .../api/lang/jnigen/jni/emit/flat_input.rs | 4 +- .../api/lang/jnigen/jni/emit/struct_out.rs | 30 +-- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 15 +- prebindgen/src/api/lang/jnigen/jni/fold.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/iface.rs | 2 +- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 14 +- prebindgen/src/api/lang/jnigen/jni/mod.rs | 4 +- prebindgen/src/api/lang/jnigen/jni/render.rs | 33 +-- .../src/api/lang/jnigen/jni/struct_plan.rs | 72 ++--- .../api/lang/jnigen/jni/tests/value_form.rs | 74 ++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 16 +- prebindgen/src/api/lang/jnigen/mod.rs | 248 ++++++++++++++++++ 20 files changed, 529 insertions(+), 115 deletions(-) diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt index faf9da4e..96b6d299 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt @@ -1596,6 +1596,14 @@ public fun ledgerEach(n: Long, sink: LedgerCallback, onError: JniErrorHandler): String? { val __bcap = JniErrorHandlerCapture.acquire() @@ -1606,7 +1614,7 @@ public fun boxedNoteEcho(note: String?, onError: JniErrorHandler): Stri /** * The same crossing with nothing wrapped — the control the wrapped form must - * match, since the model says the two returns are the same type. + * match, since the model says the two signatures are the same type. */ public fun plainNoteEcho(note: String?, onError: JniErrorHandler): String? { val __bcap = JniErrorHandlerCapture.acquire() diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index 1987cebe..c1a730b9 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -5233,6 +5233,30 @@ pub(crate) unsafe fn JShortArray_to_i16_2_098f4ad5<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn JString_to_Box_Option_String_caeff346<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JString<'v>, +) -> ::core::result::Result>, __JniErr> { + Ok({ + let __v: ::core::option::Option = { + if v.is_null() { None } else { Some(JString_to_String_c7f3ca43(env, v)?) } + }; + ::std::boxed::Box::new(__v) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn JString_to_Box_String_027f6250<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JString<'v>, @@ -12694,7 +12718,7 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedNoteEcho<'a static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; - let note = match JString_to_Option_String_56d5e304(&mut env, ¬e) { + let note = match JString_to_Box_Option_String_caeff346(&mut env, ¬e) { ::core::result::Result::Ok(__v) => __v, ::core::result::Result::Err(__e) => { signal_binding_error( diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index ca6b0383..a34bb57e 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -1598,13 +1598,21 @@ pub fn ledger_archived(l: &Ledger) -> Option { /// A `Cow` payload is the other half and cannot appear in a compiled fixture: /// it must be REFUSED, which only /// `a_transparent_wrapper_is_bridged_only_where_it_can_be` can assert. +/// +/// The **parameter** is wrapped too, and that half is #273: nullability was +/// decided by asking the spelling whether its last path segment read `Option`, +/// so this rendered `note: String` while `plain_note_echo` rendered +/// `note: String?`. A non-null Kotlin parameter for an optional value is a +/// wrong contract rather than a cosmetic one — Kotlin rejects `null` at the +/// call site, so the absent case becomes unexpressible. The two externs must +/// come out **identical**. #[prebindgen] -pub fn boxed_note_echo(note: Option) -> Box>> { - Box::new(Box::new(note)) +pub fn boxed_note_echo(note: Box>) -> Box>> { + Box::new(note) } /// The same crossing with nothing wrapped — the control the wrapped form must -/// match, since the model says the two returns are the same type. +/// match, since the model says the two signatures are the same type. #[prebindgen] pub fn plain_note_echo(note: Option) -> Option { note diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index fcd49138..391cb46f 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -45,7 +45,7 @@ # gaps are listed here rather than implied away. 2 api/core/registry/scan.rs -10 api/core/types_util.rs +9 api/core/types_util.rs 8 api/lang/cbindgen/builder.rs 1 api/lang/cbindgen/convert.rs 5 api/lang/cbindgen/emit.rs @@ -71,4 +71,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 130 +# total: 129 diff --git a/prebindgen/src/api/core/registry/view.rs b/prebindgen/src/api/core/registry/view.rs index a6f13b2d..1fcc06eb 100644 --- a/prebindgen/src/api/core/registry/view.rs +++ b/prebindgen/src/api/core/registry/view.rs @@ -37,6 +37,54 @@ pub trait Conversions { /// out. fn reading(&self, ty: &syn::Type) -> Option; + /// Whether `ty` crosses as **optional** — the model's answer, not the + /// spelling's. + /// + /// The question every consumer actually means, and the one they could not + /// ask: they reached for `is_option_type`, which is `path_tail_is(ty, + /// "Option")`. But the model **erases** transparent wrappers — `Box` *is* + /// `T`, and so is `Cow<'_, T>` — so `Box>` is `Optional` and + /// that check answers `false`. Kotlin then lost the `?`, which is not a + /// cosmetic slip: a non-null parameter for an optional value makes the + /// absent case unexpressible (#273). + /// + /// Here rather than at each consumer so the rule has **one** home. A new + /// transparent wrapper — an `Rc`, say — is then a change to + /// [`TRANSPARENT_WRAPPERS`](crate::api::core::flat::TRANSPARENT_WRAPPERS) + /// and nothing else; every site asking this question follows automatically. + fn is_optional(&self, ty: &syn::Type) -> bool { + self.reading(ty) + .is_some_and(|r| r.optional_inner().is_some()) + } + + /// What an optional wraps, **spelled as generated Rust must spell it**. + /// + /// Classify off `kind`, spell off `syntax`: the decision that this *is* an + /// optional comes from the model, and what comes back is the inner's own + /// spelling — which is what a converter signature or a `quote!` needs. + fn optional_inner(&self, ty: &syn::Type) -> Option { + self.reading(ty) + .and_then(|r| r.optional_inner().map(|i| i.origin.syntax.clone())) + } + + /// The element of a run of values (`Vec`, `[T]`, `Cow<'_, [T]>`), spelled + /// as generated Rust must spell it. The sequence peer of + /// [`Self::optional_inner`]. + fn sequence_elem(&self, ty: &syn::Type) -> Option { + self.reading(ty) + .and_then(|r| r.sequence_elem().map(|e| e.origin.syntax.clone())) + } + + /// Whether `ty` is a borrow of an optional (`Option<&T>`) — the shape a + /// handle parameter locks differently. Reads both layers off the model, so + /// a wrapped spelling answers the same as the bare one. + fn is_optional_borrow(&self, ty: &syn::Type) -> bool { + self.reading(ty).is_some_and(|r| { + r.optional_inner() + .is_some_and(|i| i.borrow_target().is_some()) + }) + } + /// The conversion for `ty` in `dir`, if there is one. fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry>; diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index 326d1cf7..9e31b0c5 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -113,10 +113,10 @@ pub fn first_type_arg(ty: &syn::Type) -> Option { }) } -/// True when `ty` is `Option<&T>` / `Option<&mut T>`. -pub fn is_option_ref(ty: &syn::Type) -> bool { - option_inner_type(ty).is_some_and(|inner| matches!(inner, syn::Type::Reference(_))) -} +// `is_option_ref` lived here — `option_inner_type(ty)` then a `Type::Reference` +// match — and decided how a handle parameter locks. Both halves read the +// spelling, so an optional borrow behind an erased wrapper answered `false`. +// `Conversions::is_optional_borrow` asks the model instead (#273). /// The bare ident of a plain path type (`ZThing` → `ZThing`); `None` for /// references, generics, or multi-shape types. diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index a91b755c..03c2fb24 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -1748,16 +1748,20 @@ impl Declarations { .crossing_keys(direction) .iter() .map(|candidate| { - let mut ty = candidate.to_type(); + // How many `Option` layers this crossing puts over `key` — the + // model's count, so a wrapped spelling contributes the same + // demand a bare one does. Walking the reading also drops the + // re-lookup the old loop did: it peeled a spelling and re-keyed + // each result, where the layers are already right here (#273). + let Some(mut reading) = registry.reading(&candidate.to_type()) else { + return 0; + }; let mut depth = 0; - while crate::api::core::types_util::is_option_type(&ty) { - let Some(inner) = option_inner_type(&ty) else { - return 0; - }; - ty = inner; + while let Some(inner) = reading.optional_inner().cloned() { + reading = inner; depth += 1; } - if TypeKey::from_type(&ty) == *key { + if reading.key() == *key { depth } else { 0 diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs index 293458dd..63d8e02c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs @@ -263,7 +263,7 @@ pub(crate) fn callback_input( .projection .as_ref() .is_none_or(|p| p.kind == ProjectionKind::Unsigned64) - && !is_option_type(arg_ty) + && !registry.is_optional(arg_ty) && matches!(jni_field_access(&arg_wire), Some((_, _, false))); if arg_is_prim { let letter = jni_field_access(&arg_wire).unwrap().1; diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index bc41ce08..6192b831 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -10,7 +10,7 @@ pub(crate) fn struct_input_body( registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { let struct_name = s.ident.to_string(); - let struct_module = struct_module_path(ext, registry, s); + let struct_module = struct_module_path(ext, registry, &s.ident); let struct_ident = &s.ident; let syn::Fields::Named(named) = &s.fields else { @@ -1507,7 +1507,7 @@ fn build_flat_struct_node( } stack.pop(); Ok(FlatStructNode { - struct_module: struct_module_path(ext, registry, st), + struct_module: struct_module_path(ext, registry, &st.ident), struct_ident: st.ident.clone(), binding: format_ident!("__flat_{native_prefix}"), optional, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index c8a329e0..af877a40 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -83,7 +83,7 @@ pub(crate) fn primitive_default_for_descriptor(sig: &str) -> TokenStream { pub(crate) fn synth_value_struct_leaves( ext: &Declarations, registry: &impl Conversions, - s: &syn::ItemStruct, + s: &crate::api::core::flat::Struct, path_prefix: &[crate::api::core::unfold::PathStep], name_prefix: &str, depth: usize, @@ -92,13 +92,11 @@ pub(crate) fn synth_value_struct_leaves( if depth > 16 { return None; } - let syn::Fields::Named(named) = &s.fields else { - return None; - }; + // Named by construction — a tuple struct is an `Extern`, not a `Struct`. let mut leaves: Vec = Vec::new(); - for field in &named.named { - let fname = field.ident.as_ref()?.clone(); - let effective_ty = field.ty.clone(); + for field in &s.fields { + let fname = field.name.as_ref()?.clone(); + let effective_ty = field.ty.origin.syntax.clone(); let camel = mangle_kotlin_ident(&kt_snake_to_camel(&fname.to_string())); let leaf_name = if name_prefix.is_empty() { camel @@ -127,7 +125,7 @@ pub(crate) fn synth_value_struct_leaves( // converter for it, failing the resolve with the sum named rather // than the unsupported position. TypeKind::Handle | TypeKind::Enum | TypeKind::Sum => return None, - TypeKind::DataStruct { st, cfg: Some(_) } => Some(st.origin.syntax.clone()), + TypeKind::DataStruct { st, cfg: Some(_) } => Some(st.clone()), _ => None, }; if let Some(child) = nested { @@ -173,7 +171,7 @@ pub(crate) fn synth_value_struct_leaves( pub(crate) fn flatten_struct_encode( ext: &Declarations, registry: &impl Conversions, - s: &syn::ItemStruct, + s: &crate::api::core::flat::Struct, access: &TokenStream, prefix: &str, depth: usize, @@ -588,15 +586,15 @@ fn encode_field( pub(crate) fn struct_output_body( ext: &Declarations, - s: &syn::ItemStruct, + s: &crate::api::core::flat::Struct, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { - let struct_name = s.ident.to_string(); + let struct_name = s.name.to_string(); // Prefer the registered Kotlin FQN (`io.zenoh.jni.JniSample`) so the // mangle closure flows through; fall back to the bare struct ident // qualified with the package when no `data_class` / // `ptr_class` declaration exists for this Rust type. - let struct_ident = &s.ident; + let struct_ident = &s.name; let struct_ty: syn::Type = syn::parse_quote!(#struct_ident); let registered_fqn = ext .types @@ -651,11 +649,13 @@ pub(crate) fn struct_output_body( pub(crate) fn struct_module_path( ext: &Declarations, registry: &impl Conversions, - s: &syn::ItemStruct, + name: &syn::Ident, ) -> syn::Path { // The module the struct is reachable under from the generated file: its - // origin crate (multi-source registries) or the default module. - ext.fn_module(registry, &s.ident) + // origin crate (multi-source registries) or the default module. Takes the + // NAME, which is all it needs — so it serves a caller holding the element + // and one still holding the item. + ext.fn_module(registry, name) } // ────────────────────────────────────────────────────────────────────── diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index 834b8e48..b93f5d7a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -80,8 +80,14 @@ pub(crate) struct PlanLeaf { /// `JNINative` extern declares for pass-through leaves (for projections /// this is the erased wire name, not the typed surface). pub kt_meta: Option, - /// Raw `is_option_type(ty)` — each site applies its own nullability rule - /// (handles stay non-null `Long` on the extern but `T?` on the surface). + /// Whether the leaf crosses as optional, **per the model** — so a wrapped + /// spelling (`Box>`) answers exactly as the bare one does. Each + /// site applies its own nullability rule on top (handles stay non-null + /// `Long` on the extern but `T?` on the surface). + /// + /// This used to be `is_option_type(ty)`, which asks the *spelling* whether + /// its last path segment reads `Option` — and the model erases `Box` and + /// `Cow`, so an optional behind one lost its `?` (#273). pub optional: bool, /// `true` when the (probed-through `&`/`Option`) type is an /// `enum_class` enum: surface keeps the typed enum, the extern declares @@ -523,7 +529,7 @@ fn classify_leaf( expanded: bool, source_param: &syn::Ident, ) -> Result { - let optional = is_option_type(ty); + let optional = registry.is_optional(ty); let as_enum_value = ext.is_kotlin_enum(&enum_probe_type(ty)); let kt_name = kt_param_name(&ident.to_string()); @@ -576,7 +582,8 @@ fn classify_leaf( }, Some(ProjectionKind::Unsigned64) => InputKind::Unsigned64 { niche: entry.metadata.projection.as_ref().and_then(|p| { - is_option_type(ty) + registry + .is_optional(ty) .then(|| p.niche_sentinels.first().cloned()) .flatten() }), diff --git a/prebindgen/src/api/lang/jnigen/jni/fold.rs b/prebindgen/src/api/lang/jnigen/jni/fold.rs index 02ea52f7..9299a017 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fold.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fold.rs @@ -171,7 +171,7 @@ pub(crate) fn is_kotlin_primitive_ty(t: &kt::KtType) -> bool { pub(crate) fn flatten_struct_factory( ext: &Declarations, registry: &Registry, - s: &syn::ItemStruct, + s: &crate::api::core::flat::Struct, prefix: &str, class_name: &str, imports: &mut BTreeSet, diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index ef23539e..f79c5e29 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -1219,7 +1219,7 @@ pub(crate) fn callback_iface_spec( leaf_tys.push(LeafDesc::Whole { name: whole_value_name(t, i), ty: t.clone(), - nullable: is_option_type(t), + nullable: registry.is_optional(t), owned_handle, }); groups.push(GroupDesc { diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 76858832..891acb1e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -860,7 +860,7 @@ impl Declarations { // field — the Kotlin type must match that slot. Read from the same // entry the type came from. let primitive_wire = crate::api::lang::jnigen::jni::is_jni_primitive(&out.destination); - if is_option_type(&field.ty) && !primitive_wire { + if registry.is_optional(&field.ty) && !primitive_wire { ty.nullable() } else { ty @@ -897,11 +897,7 @@ impl Declarations { }) else { continue; }; - let Some(item_struct) = registry - .flat() - .struct_type(&ident) - .map(|st| &st.origin.syntax) - else { + let Some(item_struct) = registry.flat().struct_type(&ident) else { continue; }; @@ -909,8 +905,8 @@ impl Declarations { Some((p, c)) => (p.to_string(), c.to_string()), None => (String::new(), kotlin_fqn.clone()), }; - if item_struct.ident != class_name { - aliases.push((item_struct.ident.to_string(), class_name.clone())); + if item_struct.name != class_name { + aliases.push((item_struct.name.to_string(), class_name.clone())); } let mut class = build_data_class(self, &class_name, item_struct, registry); // The data class is self-contained (property/factory types + @@ -1517,7 +1513,7 @@ impl Declarations { name: &str, imports: &mut BTreeSet, ) -> String { - let optional = is_option_type(&leaf.out_ty); + let optional = registry.is_optional(&leaf.out_ty); let arg = if param.raw.is_nullable() && !optional { format!("{name}!!") } else { diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index 3dc48ba5..afd63a09 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -53,9 +53,7 @@ pub(crate) use crate::api::{ niches::{NicheSlot, Niches}, prebindgen::{ConverterImpl, Prebindgen, Stage}, registry::{extract_fn_trait_args, Direction, Registry, TypeKey}, - types_util::{ - bare_path_ident, is_option_ref, is_option_type, option_inner_type, vec_inner_type, - }, + types_util::{bare_path_ident, option_inner_type, vec_inner_type}, }, gen::kotlin::WriteKotlinError, lang::jnigen::{ diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index 970d2bd5..a8e8f50c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -69,18 +69,12 @@ pub(crate) fn build_enum_class(class_name: &str, item_enum: &syn::ItemEnum) -> k pub(crate) fn build_data_class( ext: &Declarations, class_name: &str, - item_struct: &syn::ItemStruct, + item_struct: &crate::api::core::flat::Struct, registry: &Registry, ) -> kt::KtClass { - let fields_named = match &item_struct.fields { - syn::Fields::Named(n) => &n.named, - _ => { - panic!( - "render_data_class_source: struct `{}` must use named fields to map onto Kotlin data class properties", - item_struct.ident - ) - } - }; + // A tuple struct is an `Extern` in the model, never a `Struct`, so every + // field here is named by construction. + let fields_named = &item_struct.fields; // The class declaration is derived from the SAME plan the `fromParts` // factory and the Rust encoder walk. Deriving it separately — a third @@ -92,7 +86,7 @@ pub(crate) fn build_data_class( field needs a resolved OUTPUT converter (that direction declares the slot the \ encoder fills) AND the Kotlin metadata that converter carries — a `kotlin_name`, \ or a registered class for a projection leaf", - item_struct.ident + item_struct.name ) }); @@ -105,14 +99,14 @@ pub(crate) fn build_data_class( let mut destructible_fields: Vec<(String, crate::api::lang::jnigen::jni::FoldStrategy)> = Vec::new(); for (field, pf) in fields_named.iter().zip(&plan.fields) { - let field_ident = field.ident.as_ref().unwrap_or_else(|| { + let field_ident = field.name.as_ref().unwrap_or_else(|| { panic!( "render_data_class_source: struct `{}` has an unnamed field in named-fields context", - item_struct.ident + item_struct.name ) }); let kotlin_field_name = mangle_kotlin_ident(&kt_snake_to_camel(&field_ident.to_string())); - let owner = format!("{}.{}", item_struct.ident, field_ident); + let owner = format!("{}.{}", item_struct.name, field_ident); // The declaration reads ONE direction — output — because that is the // direction that declares the `fromParts` slots the encoder fills, and @@ -129,7 +123,7 @@ pub(crate) fn build_data_class( // disagreed. Reject it at the declaration instead. if !matches!(pf.kind, PlanFieldKind::Projection { .. }) { if let Some(proj) = registry - .input_entry(&field.ty) + .input_entry(&field.ty.origin.syntax) .and_then(|e| e.metadata.projection.clone()) { panic!( @@ -172,7 +166,8 @@ pub(crate) fn build_data_class( }); let mut class = kt::KtClass::new(kt::ClassKind::Data, class_name).vis(kt::Vis::Public); - if let Some(doc) = crate::api::lang::jnigen::util::doc_string(&item_struct.attrs) { + if let Some(doc) = crate::api::lang::jnigen::util::doc_string(&item_struct.origin.syntax.attrs) + { class = class.kdoc(doc); } for p in ctor_params { @@ -1210,9 +1205,9 @@ fn classify_params( // Handle → Borrow/Consume by Rust syntactic shape (locked); // `Option<&T>` / by-value `Option` mark the param nullable // and the wrapper body branches on null before lock selection. - if is_option_ref(arg_ty) { + if registry.is_optional_borrow(arg_ty) { ParamMode::BorrowNullable - } else if is_option_type(arg_ty) { + } else if registry.is_optional(arg_ty) { // by-value `Option` opaque → nullable consume ParamMode::ConsumeNullable } else if matches!(arg_ty, syn::Type::Reference(_)) { @@ -2116,7 +2111,7 @@ pub(crate) fn whole_value_name(ty: &syn::Type, i: usize) -> String { /// Fall-back Kotlin type derived directly from the JNI wire type. /// Returns the **non-nullable** Kotlin base name — the use site adds /// a `?` suffix when the entry's Rust type is `Option<…>` (via -/// [`is_option_type`]), so this helper must not double up. +/// the model), so this helper must not double up. pub(crate) fn kotlin_for_wire(wire: &syn::Type) -> Option { if let Some(p) = JniPrim::from_wire(wire) { return Some(kt::KtType::cls(p.kotlin_type())); diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 15557e89..8823c16e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -185,21 +185,20 @@ pub(crate) struct SumPlanField { pub(crate) fn build_struct_plan( ext: &Declarations, registry: &impl Conversions, - s: &syn::ItemStruct, + s: &crate::api::core::flat::Struct, depth: usize, ) -> Option { assert!( depth <= 16, "struct fromParts plan: recursion too deep at struct `{}` (cyclic data_class?)", - s.ident + s.name ); - let syn::Fields::Named(named) = &s.fields else { - return None; - }; let mut fields: Vec = Vec::new(); - for field in &named.named { - let fname = field.ident.as_ref()?.clone(); - let owner = format!("{}.{}", s.ident, fname); + for field in &s.fields { + // A tuple struct is an `Extern` in the model, never a `Struct`, so a + // nameless field cannot reach here. + let fname = field.name.as_ref()?.clone(); + let owner = format!("{}.{}", s.name, fname); let kind = classify_field(ext, registry, &field.ty, &owner, depth)?; fields.push(PlanField { fname, kind }); } @@ -217,11 +216,16 @@ pub(crate) fn build_struct_plan( pub(crate) fn classify_field( ext: &Declarations, registry: &impl Conversions, - ty: &syn::Type, + reading: &crate::api::core::flat::TypeRef, owner: &str, depth: usize, ) -> Option { - let effective_ty = ty.clone(); + // The **reading**, not a spelling. Every layer question below is answered + // from `kind` and cannot fail: holding a `TypeRef` is proof the model + // classified this type. Taking a `syn::Type` meant asking the registry per + // question, and a type it had never seen answered "no layer" rather than + // saying so — which is the missing `?` of #273 waiting to happen again. + let effective_ty = reading.origin.syntax.clone(); // A sum is classified FIRST, because it is the one kind with no converter // of its own: it crosses as a tag plus one leaf group per variant, never @@ -233,27 +237,28 @@ pub(crate) fn classify_field( // answers about a bare ident, so it reports `Vec` as `Other` and // a rejection guarded on the unpeeled type could never fire. Peeling first // is what makes the `Vec` error reachable at all. - let bare = option_inner_type(&effective_ty).unwrap_or_else(|| effective_ty.clone()); - let core = vec_inner_type(&bare).unwrap_or_else(|| bare.clone()); + // Every layer question below is the MODEL's, asked once: a field spelled + // `Box>` is `Optional` and must classify, nest and render exactly + // as `Option` does. Peeling by path segment answered "not optional" for + // it, and the seven peels in this function would then disagree with each + // other about the same field (#273). + let optional_inner = reading.optional_inner(); + let bare_ref = optional_inner.unwrap_or(reading); + let bare = bare_ref.origin.syntax.clone(); + let seq_elem = bare_ref.sequence_elem(); + let core = seq_elem.map_or_else(|| bare.clone(), |e| e.origin.syntax.clone()); if matches!(ext.type_kind(registry, &core), TypeKind::Sum) { // A `Vec` of tag-gated groups has variable arity, exactly like a `Vec` // of nested data classes — the flattened bridge is fixed-layout by // construction. - if vec_inner_type(&bare).is_some() { + if seq_elem.is_some() { panic!( "fromParts bridge: `Vec<{}>` sealed-class field (`{owner}`) is not supported \ (variable arity)", core.to_token_stream(), ); } - return sum_plan_kind( - ext, - registry, - &bare, - owner, - option_inner_type(&effective_ty).is_some(), - depth, - ); + return sum_plan_kind(ext, registry, &bare, owner, optional_inner.is_some(), depth); } let field_entry = registry.output_entry(&effective_ty)?; @@ -277,7 +282,7 @@ pub(crate) fn classify_field( return Some(PlanFieldKind::Enum { conv, kotlin }); } // `Option` leaf. - if let Some(inner) = option_inner_type(&effective_ty) { + if let Some(inner) = optional_inner.map(|i| i.origin.syntax.clone()) { if ext.is_kotlin_enum(&inner) { let kotlin = registry .output_entry(&inner)? @@ -300,9 +305,9 @@ pub(crate) fn classify_field( let child_fqn = cfg .and_then(|c| c.name_spec.as_ref()) .map(|s| ext.fqn_of(s)); - let plan = build_struct_plan(ext, registry, &st.origin.syntax, depth + 1)?; + let plan = build_struct_plan(ext, registry, st, depth + 1)?; return Some(PlanFieldKind::Nested { - optional: option_inner_type(&effective_ty).is_some(), + optional: optional_inner.is_some(), child_fqn, plan, }); @@ -317,8 +322,8 @@ pub(crate) fn classify_field( None => { // Object-shaped wire with no fixed descriptor; the JVM slot // must be the field's actual declared type (Option-stripped). - let slot_ty = - option_inner_type(&effective_ty).unwrap_or_else(|| effective_ty.clone()); + let slot_ty = optional_inner + .map_or_else(|| effective_ty.clone(), |i| i.origin.syntax.clone()); let descriptor = registry .output_entry(&slot_ty) .and_then(|e| jni_field_access(&e.destination)) @@ -353,7 +358,7 @@ pub(crate) fn classify_field( (LeafForm::Object, descriptor) } }; - let nullable = is_option_type(&effective_ty) && !is_jni_primitive(&wire); + let nullable = optional_inner.is_some() && !is_jni_primitive(&wire); Some(PlanFieldKind::Leaf { conv, wire: Box::new(wire), @@ -492,6 +497,13 @@ fn sum_plan_kind( let item_enum = registry.flat().enum_item(&ident).unwrap_or_else(|| { panic!("fromParts bridge: sealed-class field `{owner}` has no indexed enum `{ident}`") }); + // The sum as the MODEL holds it: its alternatives' payloads are `TypeRef`s + // already, so classifying one asks nothing and cannot be asked about a type + // the model never saw. + let Some(crate::api::core::flat::Type::Variant(sum)) = registry.flat().declared_type(&ident) + else { + panic!("fromParts bridge: sealed-class field `{owner}`: `{ident}` is not a sum") + }; let key = TypeKey::from_ident(&ident); let cfg = ext .types @@ -508,16 +520,16 @@ fn sum_plan_kind( let spec = SumSpec::from_item_enum(item_enum); let mut variants: Vec = Vec::new(); - for (v, item_variant) in spec.variants.iter().zip(&item_enum.variants) { + for (v, alt) in spec.variants.iter().zip(&sum.alternatives) { let kotlin_name = ext.sum_variant_class_name(sum_cfg, &v.ident); let mut fields: Vec = Vec::new(); - for (f, item_field) in v.fields.iter().zip(item_variant.fields.iter()) { + for (f, alt_field) in v.fields.iter().zip(alt.fields.iter()) { let prop = sum_field_prop_name(f); let slot = sum_slot_fragment(&kotlin_name, &prop); let owner = format!("{ident}::{}.{prop}", v.ident); // `?` — a payload whose converter has not resolved yet defers the // whole plan to the next iteration, it does not fail the build. - let kind = classify_field(ext, registry, &item_field.ty, &owner, depth + 1)?; + let kind = classify_field(ext, registry, &alt_field.ty, &owner, depth + 1)?; fields.push(SumPlanField { member: f.member.clone(), slot, diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index c358f13e..23d37c2a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -2658,3 +2658,77 @@ fn a_wrapped_borrow_has_nothing_to_bridge_and_refuses() { ); } } + +/// Nullability is the **model's** answer, so an optional behind an erased +/// wrapper renders exactly as the bare one does — everywhere, not only in the +/// positions a compiled fixture reaches. +/// +/// `is_option_type` asked the spelling whether its last path segment read +/// `Option`. The model erases `Box` and `Cow`, so `Box>` answered +/// "not optional" and Kotlin lost its `?`. That is a wrong contract rather than +/// a cosmetic slip: a non-null parameter for an optional value makes the absent +/// case unexpressible (#273). +/// +/// covertest's `boxed_note_echo` covers the parameter and return positions and +/// compiles them; this covers the **data-class field** and **callback** ones, +/// which it does not reach. +#[test] +fn nullability_ignores_how_rust_spells_the_optional() { + let loc = myflat_loc(); + let build = |field_ty: syn::Type| -> String { + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZRec { + pub note: #field_ty, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_rec_emit(cb: impl Fn(ZRec) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::data_class!(ZRec)) + .fun(crate::fun!(z_rec_emit)), + ); + let dir = unique_test_dir("jnigen_nullability"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = jni.build_with(registry).expect("resolve"); + let _ = gen.write_rust(dir.join("g.rs")).expect("write_rust"); + gen.write_kotlin(&dir.join("kotlin")) + .expect("write_kotlin") + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n") + }; + + let plain = build(syn::parse_quote!(Option)); + let boxed = build(syn::parse_quote!(Box>)); + + for (label, kotlin) in [("Option", &plain), ("Box>", &boxed)] { + assert!( + kotlin.contains("note: String?"), + "{label}: an optional field is nullable in Kotlin:\n{kotlin}" + ); + } + // The wrapper is a Rust spelling and nothing else: the Kotlin surface of + // the two is identical, character for character. + assert_eq!( + plain, boxed, + "a transparent wrapper must not change the Kotlin surface" + ); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index a6ef210a..aa3b93e7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -1277,7 +1277,7 @@ impl Declarations { ) -> Vec { let mut out = Vec::new(); for (ident, item_struct) in registry.flat().types().filter_map(|t| match t { - crate::api::core::flat::Type::Struct(s) => Some((&s.name, &s.origin.syntax)), + crate::api::core::flat::Type::Struct(s) => Some((&s.name, s)), _ => None, }) { let source: syn::Type = syn::parse_quote!(#ident); @@ -1931,12 +1931,8 @@ impl Declarations { }); } } - if let Some(s) = registry - .flat() - .struct_type(&name) - .map(|st| &st.origin.syntax) - { - let (wire, body) = struct_input_body(self, s, registry)?; + if let Some(s) = registry.flat().struct_type(&name) { + let (wire, body) = struct_input_body(self, &s.origin.syntax, registry)?; let niches = default_niches_for_wire(&wire); // Auto-generated struct: the value-context Kotlin name is // whatever the user pinned via `data_class`. If @@ -2125,11 +2121,7 @@ impl Declarations { }); } if let Some(name) = bare_path_ident(ty) { - if let Some(s) = registry - .flat() - .struct_type(&name) - .map(|st| &st.origin.syntax) - { + if let Some(s) = registry.flat().struct_type(&name) { let (wire, body) = struct_output_body(self, s, registry)?; let niches = default_niches_for_wire(&wire); let kotlin_name = self diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index 66266379..dfb51745 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -52,3 +52,251 @@ pub use jni::{ // unchanged (`KotlinFile` aliases the model's `KtFile`). pub use crate::api::gen::kotlin::KtFile as KotlinFile; pub use crate::api::gen::kotlin::WriteKotlinError; + +#[cfg(test)] +mod spelling_census { + //! A committed census of every place this adapter asks a **spelling** what + //! a type's layers are, instead of asking the model. + //! + //! `is_option_type` is `path_tail_is(ty, "Option")`, and + //! `option_inner_type`/`vec_inner_type` peel by last path segment. The model + //! **erases** transparent wrappers — `Box` *is* `T`, and so is + //! `Cow<'_, T>` — so every one of these answers "no layer" for a type the + //! model says is `Optional` or `Sequence`. + //! + //! That is how #273 happened: Kotlin nullability was decided this way, so a + //! `Box>` **parameter** rendered non-null while the + //! identical-meaning `Option` rendered `String?` — and a non-null + //! parameter for an optional value makes the absent case unexpressible. + //! `Conversions::{is_optional, optional_inner, sequence_elem, + //! is_optional_borrow}` ask the model, and every site with a registry in + //! scope should use those. + //! + //! ## What this is for + //! + //! The counts go **down**. A file at zero has been migrated and must not + //! regress; a file above zero is remaining work, and #229's L4 "layer + //! questions" is where it is tracked. Either way a NEW call cannot appear + //! without moving a number, which is what stops the next site from reaching + //! for the spelling because it was the easiest thing in scope. + //! + //! It does not say the remaining calls are wrong *today* — some may be + //! legitimate spelling questions, the way `decoded_vec_satisfies` and + //! `is_unsized_spelling` are. It says each one is a decision someone made, + //! and moving the number is what puts it in front of review. + //! + //! ## Why it walks tokens + //! + //! A text scan is the wrong instrument for anything with more than one + //! spelling: `option_inner_type(..)`, + //! `types_util::option_inner_type(..)` and a `use`-aliased call are the same + //! call. #271's first guard matched text and missed a bare `Some(..)` that + //! turned out to be a live bug, so this counts **call expressions by callee + //! name**, whatever path qualifies them — and resolves `use … as …` first, + //! because an alias rewrites the call site and would otherwise be the same + //! blind spot one form over (see [`tracked_names`]). + + use proc_macro2::{Delimiter, TokenTree}; + + /// The helpers that read a spelling where the model has the answer. + const SPELLING_HELPERS: &[&str] = &[ + "is_option_type", + "is_option_ref", + "option_inner_type", + "vec_inner_type", + "peel_ref_option_vec", + ]; + + /// `(file, call count)` — every `.rs` under `api/lang/jnigen`, checked + /// against the directory tree so a new module cannot sit outside the census. + const CENSUS: &[(&str, usize)] = &[ + // The L4 "layer questions" remainder — #229. Not migrated here because + // it is a separate consumer and bundling it would make one review of + // both impossible. + ("jni/emit/flat_input.rs", 20), + ("jni/emit/struct_out.rs", 2), + ("jni/emit/vec_build.rs", 1), + ("jni/emit/wrapper.rs", 2), + ("jni/fold.rs", 1), + ("jni/iface.rs", 2), + ("jni/kotlin_emit.rs", 1), + ("jni/trait_impl.rs", 4), + // Down from 2: the nullability decisions now ask the model. The one + // left probes for an enum through its layers. + ("jni/fn_plan.rs", 1), + ]; + + /// Count `name(` call expressions, ignoring how the path is qualified. + /// Collect the local names a file can call a tracked helper by: the helper's + /// own name, plus anything a `use … as …` renamed it to. + /// + /// Without this the census counts the wrong thing. Qualifying a call leaves + /// the tracked ident in place (`types_util::option_inner_type(..)` still + /// ends in it), but **aliasing rewrites the call site**: after + /// `use …::option_inner_type as peel_optional;` the call reads + /// `peel_optional(ty)` and nothing in `SPELLING_HELPERS` matches it. + /// + /// That is not hypothetical — the sibling adapter does exactly this today + /// (`api/lang/cbindgen/mod.rs`: `is_option_type as is_option`, called at a + /// dozen sites), so anyone writing jnigen code in that style would have + /// walked straight through this guard. + /// + /// Found in tokens rather than by parsing `use` items, for the reason the + /// module docs give: a token walk sees macro bodies and nested `use` groups + /// alike. The shape is ` as `, wherever it appears. + fn tracked_names(ts: proc_macro2::TokenStream, out: &mut Vec) { + let toks: Vec = ts.into_iter().collect(); + for (i, t) in toks.iter().enumerate() { + if let TokenTree::Ident(id) = t { + let renamed = matches!(toks.get(i + 1), Some(TokenTree::Ident(kw)) if kw == "as"); + if renamed && SPELLING_HELPERS.iter().any(|h| id == h) { + if let Some(TokenTree::Ident(alias)) = toks.get(i + 2) { + out.push(alias.to_string()); + } + } + } + if let TokenTree::Group(g) = t { + tracked_names(g.stream(), out); + } + } + } + + /// Count calls to any name in `tracked`, ignoring how the path qualifies it. + fn count(ts: proc_macro2::TokenStream, tracked: &[String], n: &mut usize) { + let toks: Vec = ts.into_iter().collect(); + for (i, t) in toks.iter().enumerate() { + if let TokenTree::Ident(id) = t { + let called = matches!( + toks.get(i + 1), + Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis + ); + if called && tracked.iter().any(|h| id == h) { + *n += 1; + } + } + if let TokenTree::Group(g) = t { + count(g.stream(), tracked, n); + } + } + } + + fn rs_files(dir: &std::path::Path, root: &std::path::Path, out: &mut Vec) { + for e in std::fs::read_dir(dir).expect("jnigen dir") { + let p = e.expect("dir entry").path(); + if p.is_dir() { + rs_files(&p, root, out); + } else if p.extension().is_some_and(|x| x == "rs") { + out.push( + p.strip_prefix(root) + .expect("under jnigen") + .to_string_lossy() + .replace('\\', "/"), + ); + } + } + } + + /// The census counts a helper **however the file names it** — including a + /// `use … as …` rename, which rewrites the call site. + /// + /// The reviewer's case (#274), and it was not hypothetical: the sibling + /// adapter aliases these exact helpers today + /// (`cbindgen/mod.rs`: `is_option_type as is_option`), so the idiom is + /// established and would have slipped a new jnigen call past a guard whose + /// whole claim is that it cannot happen. + /// + /// A guard is only worth its docs if it has been seen to fail, so this + /// asserts the aliased form counts, the qualified form counts, and the two + /// things that must NOT count still do not. + #[test] + fn the_census_sees_through_a_use_alias() { + let census = |src: &str| -> usize { + let ts: proc_macro2::TokenStream = src.parse().expect("tokenize"); + let mut tracked: Vec = + SPELLING_HELPERS.iter().map(|h| (*h).to_string()).collect(); + tracked_names(ts.clone(), &mut tracked); + let mut n = 0usize; + count(ts, &tracked, &mut n); + n + }; + + // Bare, qualified, and aliased are one call. + assert_eq!(census("fn f(t: &T) { option_inner_type(t); }"), 1, "bare"); + assert_eq!( + census("fn f(t: &T) { crate::api::core::types_util::option_inner_type(t); }"), + 1, + "qualified" + ); + assert_eq!( + census( + "use crate::api::core::types_util::option_inner_type as peel_optional;\n\ + fn f(t: &T) { peel_optional(t); }" + ), + 1, + "aliased — the case that used to pass silently" + ); + // An alias inside a nested `use` group, the shape cbindgen actually writes. + assert_eq!( + census( + "use crate::api::core::types_util::{first_type_arg, is_option_type as is_option};\n\ + fn f(t: &T) { is_option(t); }" + ), + 1, + "aliased inside a use group" + ); + + // Naming a helper without calling it is not a call; an unrelated alias + // is not tracked. + assert_eq!(census("use x::option_inner_type;"), 0, "import alone"); + assert_eq!( + census("use x::something_else as option_probe;\nfn f(t: &T) { option_probe(t); }"), + 0, + "an alias of an untracked fn stays untracked" + ); + } + + #[test] + fn spelling_helper_calls_are_accounted_for() { + let root = + std::path::Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/src/api/lang/jnigen")); + let mut files = Vec::new(); + rs_files(root, root, &mut files); + files.sort(); + + let mut drift: Vec = Vec::new(); + for f in &files { + // Tests may exercise the helpers directly. + if f.contains("tests") { + continue; + } + let src = std::fs::read_to_string(root.join(f)).expect("read source"); + let ts: proc_macro2::TokenStream = + src.parse().unwrap_or_else(|e| panic!("tokenize {f}: {e}")); + // The helpers' own names, plus whatever this file renamed them to. + let mut tracked: Vec = + SPELLING_HELPERS.iter().map(|h| (*h).to_string()).collect(); + tracked_names(ts.clone(), &mut tracked); + let mut found = 0usize; + count(ts, &tracked, &mut found); + let expected = CENSUS + .iter() + .find(|(name, _)| name == f) + .map(|(_, n)| *n) + .unwrap_or(0); + if found != expected { + drift.push(format!(" {f}: {expected} -> {found}")); + } + } + assert!( + drift.is_empty(), + "SPELLING-CENSUS DRIFT:\n{}\n\n\ + These helpers read a type's layers off its SPELLING, which the model \ + erases wrappers from — see this module's docs. A count going DOWN is \ + the goal: drop the row (or lower it) in the same commit. A count going \ + UP needs a reason in review: prefer `Conversions::{{is_optional, \ + optional_inner, sequence_elem, is_optional_borrow}}`, which ask the \ + model, wherever a registry is in scope.", + drift.join("\n"), + ); + } +} From 040dfa07102c8a1e540b4ef904d903f44f83e46e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 04:43:11 +0200 Subject: [PATCH 27/52] The `Prebindgen` trait hands over elements, not syn items (#276) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First of two for #275. The `Conversions` spelling accessors exist only because adapters hold `syn::Type`/`syn::ItemFn` where they mean "a type the model classified" — and a type with no cell answers `false`, "not optional", rather than saying it never entered the pipeline. That is the #266 shape, and its symptom is the missing `?` of #273. #274 proved the fix on the struct chain; this does it for functions, constants and the trait itself. `on_function`/`on_struct`/`on_enum`/`on_const` take `flat::Function` / `Struct` / `Enum` / `Constant`. An adapter handed an element cannot ask what a type means and be told "no reading". What generated Rust must SPELL is unchanged, on `origin.syntax`. `on_enum` split into `on_variant` + `on_enum`: the model separates the two shapes deliberately and there is no single element to pass. They still SORT together — emission order is one sequence — only dispatch differs. A public-API break with zero external cost: all six implementors are in-crate, no example implements the trait, and the doc examples only call the built-in builders. `write.rs` was already holding the elements and reaching into `.origin.syntax` to satisfy the signatures — and so were eight other sites, each spelled `.map(|f| &f.origin.syntax)`. They now stop discarding what they fetched. jnigen's fn chain follows. The param walk reads `f.params` — a name and a `TypeRef` each — instead of destructuring `FnArg`/`Pat`, so a position that yields no type is no longer representable. The return reads `f.ret`, already normalized by the model. `synthetic_getter` builds a `flat::Function` for the one function no source wrote: a declared const needs no lookup (`flat::Constant.ty` is a `TypeRef`), while an expression constant names its type in a build script, so that path looks it up and panics on a miss. `PlanLeaf` carries the reading, which lets `render`'s handle lock-mode decisions read layers off the leaf. Accessors 4 -> 1: `is_optional_borrow` deleted with its last caller, and `optional_inner`/`sequence_elem` deleted as never-used — added speculatively in #274. The last one (`is_optional`, 4 callers) needs `UnfoldLeaf::out_ty` and `FoldLeaf::ty` to carry readings, which is #275's second half. The one lookup this PR adds is at that exact boundary, commented as such. Goldens BYTE-IDENTICAL — the bar for a refactor this size. 545 lib tests, `--all --all-features` 14/14, clippy, fmt. covertest 48/48 on the JVM. Both ledgers unmoved. --- prebindgen/src/api/core/prebindgen.rs | 51 +++++++-- prebindgen/src/api/core/registry/tests.rs | 47 ++++++-- prebindgen/src/api/core/registry/view.rs | 28 ----- prebindgen/src/api/core/write.rs | 22 ++-- prebindgen/src/api/core/write/tests.rs | 55 +++++++-- .../src/api/lang/cbindgen/trait_impl.rs | 24 +++- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 67 ++++++++--- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 108 +++++++++++------- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 29 ++--- .../src/api/lang/jnigen/jni/overloads.rs | 42 +++---- prebindgen/src/api/lang/jnigen/jni/render.rs | 80 +++++++------ prebindgen/src/api/lang/jnigen/jni/report.rs | 6 +- prebindgen/src/api/lang/jnigen/jni/symbols.rs | 12 +- .../api/lang/jnigen/jni/tests/callbacks.rs | 7 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 40 +++++-- 15 files changed, 381 insertions(+), 237 deletions(-) diff --git a/prebindgen/src/api/core/prebindgen.rs b/prebindgen/src/api/core/prebindgen.rs index 6853ad79..64d88e88 100644 --- a/prebindgen/src/api/core/prebindgen.rs +++ b/prebindgen/src/api/core/prebindgen.rs @@ -228,17 +228,50 @@ pub trait Prebindgen { } // ── Item methods ─────────────────────────────────────────────── + // + // Each takes the **element**, not the `syn` item it was parsed from. + // + // The element is the model's own node: its types are `TypeRef`s, already + // classified. An adapter handed one therefore cannot ask what a type means + // and be told "no reading" — the question a `&syn::ItemFn` forced it to ask + // the registry, and which answered wrongly for a type that never entered + // the pipeline (#275). What generated Rust must *spell* is still exactly + // available, on `origin.syntax`: classify off `kind`, spell off `syntax`. /// Wrap a `#[prebindgen]` fn into the destination-language wrapper /// (e.g. JNI `extern "C"` fn). - fn on_function(&self, f: &syn::ItemFn, registry: &Registry) -> TokenStream; + fn on_function( + &self, + f: &crate::api::core::flat::Function, + registry: &Registry, + ) -> TokenStream; /// Per-struct emission. Typically empty for languages that get /// everything they need from auto-generated converters. - fn on_struct(&self, s: &syn::ItemStruct, registry: &Registry) -> TokenStream; + fn on_struct( + &self, + s: &crate::api::core::flat::Struct, + registry: &Registry, + ) -> TokenStream; - /// Per-enum emission. - fn on_enum(&self, e: &syn::ItemEnum, registry: &Registry) -> TokenStream; + /// Per-sum emission — an `enum` whose alternatives carry payloads. + /// + /// Separate from [`Self::on_enum`] because the model separates them: the + /// two are numbered differently and consumed as different constructs. An + /// adapter with nothing to say about one shape returns an empty stream, as + /// both in-tree adapters do for both. + fn on_variant( + &self, + v: &crate::api::core::flat::Variant, + registry: &Registry, + ) -> TokenStream; + + /// Per-enum emission — the fieldless shape, a named set of integers. + fn on_enum( + &self, + e: &crate::api::core::flat::Enum, + registry: &Registry, + ) -> TokenStream; /// Per-const emission. Default: a named const re-emits as a path-alias /// (see [`const_path_alias`]) when [`Self::source_module`] is available — @@ -249,11 +282,15 @@ pub trait Prebindgen { /// A const reaching here is always named: prebindgen's own injected feature /// checks are [`Guard`](crate::api::core::flat::Guard)s, not consts, so this /// never has to recognise one. - fn on_const(&self, c: &syn::ItemConst, _registry: &Registry) -> TokenStream { + fn on_const( + &self, + c: &crate::api::core::flat::Constant, + _registry: &Registry, + ) -> TokenStream { use quote::ToTokens; match self.source_module() { - Some(m) => const_path_alias(c, m), - None => c.to_token_stream(), + Some(m) => const_path_alias(&c.origin.syntax, m), + None => c.origin.syntax.to_token_stream(), } } } diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 4dbf494d..d19e0aad 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -94,13 +94,28 @@ impl StubExt { impl Prebindgen for StubExt { type Metadata = (); - fn on_function(&self, _f: &syn::ItemFn, _registry: &Registry<()>) -> TokenStream { + fn on_function( + &self, + _f: &crate::api::core::flat::Function, + _registry: &Registry<()>, + ) -> TokenStream { TokenStream::new() } - fn on_struct(&self, _s: &syn::ItemStruct, _registry: &Registry<()>) -> TokenStream { + fn on_struct( + &self, + _s: &crate::api::core::flat::Struct, + _registry: &Registry<()>, + ) -> TokenStream { TokenStream::new() } - fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry<()>) -> TokenStream { + fn on_variant( + &self, + _v: &crate::api::core::flat::Variant, + _registry: &Registry<()>, + ) -> TokenStream { + TokenStream::new() + } + fn on_enum(&self, _e: &crate::api::core::flat::Enum, _registry: &Registry<()>) -> TokenStream { TokenStream::new() } } @@ -398,13 +413,20 @@ fn resolve_surfaces_adapter_invariant_errors() { fn validate(&self, _binding: &Building<'_, ()>) -> Result<(), String> { Err("member fun `f` has no receiver".to_string()) } - fn on_function(&self, f: &syn::ItemFn, r: &Registry<()>) -> TokenStream { + fn on_function( + &self, + f: &crate::api::core::flat::Function, + r: &Registry<()>, + ) -> TokenStream { self.0.on_function(f, r) } - fn on_struct(&self, s: &syn::ItemStruct, r: &Registry<()>) -> TokenStream { + fn on_struct(&self, s: &crate::api::core::flat::Struct, r: &Registry<()>) -> TokenStream { self.0.on_struct(s, r) } - fn on_enum(&self, e: &syn::ItemEnum, r: &Registry<()>) -> TokenStream { + fn on_variant(&self, v: &crate::api::core::flat::Variant, r: &Registry<()>) -> TokenStream { + self.0.on_variant(v, r) + } + fn on_enum(&self, e: &crate::api::core::flat::Enum, r: &Registry<()>) -> TokenStream { self.0.on_enum(e, r) } } @@ -1355,13 +1377,20 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { } impl Prebindgen for AnyConverterExt { type Metadata = (); - fn on_function(&self, f: &syn::ItemFn, r: &Registry<()>) -> TokenStream { + fn on_function( + &self, + f: &crate::api::core::flat::Function, + r: &Registry<()>, + ) -> TokenStream { self.0.on_function(f, r) } - fn on_struct(&self, st: &syn::ItemStruct, r: &Registry<()>) -> TokenStream { + fn on_struct(&self, st: &crate::api::core::flat::Struct, r: &Registry<()>) -> TokenStream { self.0.on_struct(st, r) } - fn on_enum(&self, e: &syn::ItemEnum, r: &Registry<()>) -> TokenStream { + fn on_variant(&self, v: &crate::api::core::flat::Variant, r: &Registry<()>) -> TokenStream { + self.0.on_variant(v, r) + } + fn on_enum(&self, e: &crate::api::core::flat::Enum, r: &Registry<()>) -> TokenStream { self.0.on_enum(e, r) } } diff --git a/prebindgen/src/api/core/registry/view.rs b/prebindgen/src/api/core/registry/view.rs index 1fcc06eb..90cea6d5 100644 --- a/prebindgen/src/api/core/registry/view.rs +++ b/prebindgen/src/api/core/registry/view.rs @@ -57,34 +57,6 @@ pub trait Conversions { .is_some_and(|r| r.optional_inner().is_some()) } - /// What an optional wraps, **spelled as generated Rust must spell it**. - /// - /// Classify off `kind`, spell off `syntax`: the decision that this *is* an - /// optional comes from the model, and what comes back is the inner's own - /// spelling — which is what a converter signature or a `quote!` needs. - fn optional_inner(&self, ty: &syn::Type) -> Option { - self.reading(ty) - .and_then(|r| r.optional_inner().map(|i| i.origin.syntax.clone())) - } - - /// The element of a run of values (`Vec`, `[T]`, `Cow<'_, [T]>`), spelled - /// as generated Rust must spell it. The sequence peer of - /// [`Self::optional_inner`]. - fn sequence_elem(&self, ty: &syn::Type) -> Option { - self.reading(ty) - .and_then(|r| r.sequence_elem().map(|e| e.origin.syntax.clone())) - } - - /// Whether `ty` is a borrow of an optional (`Option<&T>`) — the shape a - /// handle parameter locks differently. Reads both layers off the model, so - /// a wrapped spelling answers the same as the bare one. - fn is_optional_borrow(&self, ty: &syn::Type) -> bool { - self.reading(ty).is_some_and(|r| { - r.optional_inner() - .is_some_and(|i| i.borrow_target().is_some()) - }) - } - /// The conversion for `ty` in `dir`, if there is one. fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry>; diff --git a/prebindgen/src/api/core/write.rs b/prebindgen/src/api/core/write.rs index 1c49ccfa..f252cd0d 100644 --- a/prebindgen/src/api/core/write.rs +++ b/prebindgen/src/api/core/write.rs @@ -85,7 +85,7 @@ pub fn write_rust, E: Prebindgen>( let flat = registry.flat(); items.extend(parse_items_from_tokens( "on_function", - sorted_by_name(flat.functions().map(|f| (&f.name, &f.origin.syntax))) + sorted_by_name(flat.functions().map(|f| (&f.name, f))) .into_iter() .filter(|(ident, _)| declared_fns.contains(*ident)) .map(|(_, item)| ext.on_function(item, registry)), @@ -93,7 +93,7 @@ pub fn write_rust, E: Prebindgen>( items.extend(parse_items_from_tokens( "on_struct", sorted_by_name(flat.types().filter_map(|t| match t { - crate::api::core::flat::Type::Struct(s) => Some((&s.name, &s.origin.syntax)), + crate::api::core::flat::Type::Struct(s) => Some((&s.name, s)), _ => None, })) .into_iter() @@ -101,18 +101,24 @@ pub fn write_rust, E: Prebindgen>( .map(|(_, item)| ext.on_struct(item, registry)), )?); // Both enum shapes emit through `on_enum` and sort together: they were one - // map here before they were two elements, and an adapter re-emitting the - // item does not branch on the distinction. + // map here before they were two elements. They still SORT together — the + // emission order is one sequence — but they dispatch to their own methods + // now, because handing an adapter a `Type` it has to re-match is worse than + // handing it the element the model already decided on. items.extend(parse_items_from_tokens( "on_enum", sorted_by_name(flat.types().filter_map(|t| match t { - crate::api::core::flat::Type::Variant(v) => Some((&v.name, &v.origin.syntax)), - crate::api::core::flat::Type::Enum(e) => Some((&e.name, &e.origin.syntax)), + crate::api::core::flat::Type::Variant(v) => Some((&v.name, t)), + crate::api::core::flat::Type::Enum(e) => Some((&e.name, t)), _ => None, })) .into_iter() .filter(|(ident, _)| declared_types.contains(&TypeKey::from_ident(ident))) - .map(|(_, item)| ext.on_enum(item, registry)), + .map(|(_, t)| match t { + crate::api::core::flat::Type::Variant(v) => ext.on_variant(v, registry), + crate::api::core::flat::Type::Enum(e) => ext.on_enum(e, registry), + _ => unreachable!("filtered to the two enum shapes above"), + }), )?); // Consts: an adapter WITH a const declaration mechanism // (`declared_consts() == Some(set)`) emits declared consts only, @@ -122,7 +128,7 @@ pub fn write_rust, E: Prebindgen>( let declared_consts = &declared.consts; items.extend(parse_items_from_tokens( "on_const", - sorted_by_name(flat.constants().map(|c| (&c.name, &c.origin.syntax))) + sorted_by_name(flat.constants().map(|c| (&c.name, c))) .into_iter() .filter(|(ident, _)| { declared_consts diff --git a/prebindgen/src/api/core/write/tests.rs b/prebindgen/src/api/core/write/tests.rs index e080222b..371b9568 100644 --- a/prebindgen/src/api/core/write/tests.rs +++ b/prebindgen/src/api/core/write/tests.rs @@ -26,16 +26,36 @@ impl IdentityExt { impl Prebindgen for IdentityExt { type Metadata = (); - fn on_function(&self, f: &syn::ItemFn, _registry: &Registry) -> TokenStream { - f.to_token_stream() + fn on_function( + &self, + f: &crate::api::core::flat::Function, + _registry: &Registry, + ) -> TokenStream { + f.origin.syntax.to_token_stream() } - fn on_struct(&self, s: &syn::ItemStruct, _registry: &Registry) -> TokenStream { - s.to_token_stream() + fn on_struct( + &self, + s: &crate::api::core::flat::Struct, + _registry: &Registry, + ) -> TokenStream { + s.origin.syntax.to_token_stream() } - fn on_enum(&self, e: &syn::ItemEnum, _registry: &Registry) -> TokenStream { - e.to_token_stream() + fn on_variant( + &self, + v: &crate::api::core::flat::Variant, + _registry: &Registry, + ) -> TokenStream { + v.origin.syntax.to_token_stream() + } + + fn on_enum( + &self, + e: &crate::api::core::flat::Enum, + _registry: &Registry, + ) -> TokenStream { + e.origin.syntax.to_token_stream() } } @@ -217,14 +237,25 @@ fn guards_emit_ungated_and_in_stream_order() { impl Prebindgen for ConstGatingExt { type Metadata = (); - fn on_function(&self, f: &syn::ItemFn, _r: &Registry<()>) -> TokenStream { - f.to_token_stream() + fn on_function( + &self, + f: &crate::api::core::flat::Function, + _r: &Registry<()>, + ) -> TokenStream { + f.origin.syntax.to_token_stream() + } + fn on_struct(&self, s: &crate::api::core::flat::Struct, _r: &Registry<()>) -> TokenStream { + s.origin.syntax.to_token_stream() } - fn on_struct(&self, s: &syn::ItemStruct, _r: &Registry<()>) -> TokenStream { - s.to_token_stream() + fn on_variant( + &self, + v: &crate::api::core::flat::Variant, + _r: &Registry<()>, + ) -> TokenStream { + v.origin.syntax.to_token_stream() } - fn on_enum(&self, e: &syn::ItemEnum, _r: &Registry<()>) -> TokenStream { - e.to_token_stream() + fn on_enum(&self, e: &crate::api::core::flat::Enum, _r: &Registry<()>) -> TokenStream { + e.origin.syntax.to_token_stream() } } diff --git a/prebindgen/src/api/lang/cbindgen/trait_impl.rs b/prebindgen/src/api/lang/cbindgen/trait_impl.rs index 7cef477b..e8e3b914 100644 --- a/prebindgen/src/api/lang/cbindgen/trait_impl.rs +++ b/prebindgen/src/api/lang/cbindgen/trait_impl.rs @@ -1724,17 +1724,33 @@ impl Prebindgen for CbindgenBuilder { // ── Item emission ────────────────────────────────────────────────── - fn on_function(&self, f: &syn::ItemFn, registry: &Registry<()>) -> TokenStream { - self.emit_function_wrapper(f, registry) + fn on_function( + &self, + f: &crate::api::core::flat::Function, + registry: &Registry<()>, + ) -> TokenStream { + self.emit_function_wrapper(&f.origin.syntax, registry) } - fn on_struct(&self, _s: &syn::ItemStruct, _registry: &Registry<()>) -> TokenStream { + fn on_struct( + &self, + _s: &crate::api::core::flat::Struct, + _registry: &Registry<()>, + ) -> TokenStream { // The `#[repr(C)]` mirror + converters come from prerequisites / // on_output_type; the original (non-FFI-safe) struct is dropped. TokenStream::new() } - fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry<()>) -> TokenStream { + fn on_variant( + &self, + _v: &crate::api::core::flat::Variant, + _registry: &Registry<()>, + ) -> TokenStream { + TokenStream::new() + } + + fn on_enum(&self, _e: &crate::api::core::flat::Enum, _registry: &Registry<()>) -> TokenStream { TokenStream::new() } } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 6aa94ead..541ccbfc 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -6,7 +6,7 @@ use crate::api::core::{registry::Conversions, types_util::result_ok_type}; pub(crate) fn emit_jni_function_wrapper( ext: &Declarations, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, registry: &Registry, ) -> TokenStream { emit_jni_function_wrapper_with_callee(ext, f, registry, None) @@ -18,14 +18,12 @@ pub(crate) fn emit_jni_function_wrapper( /// [`emit_jni_function_wrapper_with_callee`]) and the Kotlin `val` /// initializer (`render_const_val`) — derive the extern symbol from this one /// ident, so they stay in sync by construction. The body is never used. -pub(crate) fn const_getter_fn(c: &syn::ItemConst) -> syn::ItemFn { - let ident = format_ident!("const_get_{}", c.ident.to_string().to_lowercase()); - let ty = &c.ty; - syn::parse_quote! { - pub fn #ident() -> #ty { - unimplemented!() - } - } +pub(crate) fn const_getter_fn( + c: &crate::api::core::flat::Constant, +) -> crate::api::core::flat::Function { + let ident = format_ident!("const_get_{}", c.name.to_string().to_lowercase()); + // No lookup: a constant element carries its own `TypeRef`. + synthetic_getter(ident, c.ty.clone()) } /// A const whose (peeled) type is a declared opaque handle is rejected: a @@ -103,12 +101,53 @@ pub(crate) fn validate_constant_fn(ext: &Declarations, f: &syn::ItemFn) { /// `pub fn const_get_() -> ` — the same convention as /// const-backed getters, so both sides derive the extern symbol from the one /// val name. The body is never used. -pub(crate) fn const_expr_getter_fn(kotlin_name: &str, ty: &syn::Type) -> syn::ItemFn { +pub(crate) fn const_expr_getter_fn( + kotlin_name: &str, + ty: &syn::Type, + registry: &impl Conversions, +) -> crate::api::core::flat::Function { let ident = format_ident!("const_get_{}", kotlin_name.to_lowercase()); - syn::parse_quote! { - pub fn #ident() -> #ty { + // The one lookup this path needs: the type is named by a build script, so + // no element carries it. A miss means the declared type never entered the + // pipeline, which is a binding error worth naming rather than a `None` to + // absorb. + let ret = registry.reading(ty).unwrap_or_else(|| { + panic!( + "constant_expr `{kotlin_name}`: type `{}` is not a type this binding crosses — \ + declare it, or name one that is", + quote::ToTokens::to_token_stream(ty), + ) + }); + synthetic_getter(ident, ret) +} +/// A nullary getter as the MODEL would hold it — the one function no source +/// wrote. +/// +/// A declared constant crosses as a getter extern, and that getter goes through +/// exactly the same emitter a real function does. So it needs a +/// [`flat::Function`](crate::api::core::flat::Function), not a `syn::ItemFn`: +/// the emitter classifies off `kind` now, and a synthesized item would have no +/// reading to classify from. +/// +/// `ret` is supplied by the caller because the two callers get it from +/// different places — a declared const carries its own `TypeRef`, an +/// expression constant names a type in the build script — and only the second +/// has to look one up. +pub(crate) fn synthetic_getter( + ident: syn::Ident, + ret: crate::api::core::flat::TypeRef, +) -> crate::api::core::flat::Function { + let ret_syntax = &ret.origin.syntax; + let item: syn::ItemFn = syn::parse_quote! { + pub fn #ident() -> #ret_syntax { unimplemented!() } + }; + crate::api::core::flat::Function { + name: ident, + params: Vec::new(), + origin: ret.origin.with(item), + ret, } } @@ -132,11 +171,11 @@ pub(crate) fn validate_constant_expr(ext: &Declarations, kotlin_name: &str, ty: /// `::` — a path, not a call. pub(crate) fn emit_jni_function_wrapper_with_callee( ext: &Declarations, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, registry: &Registry, callee: Option, ) -> TokenStream { - let original_ident = &f.sig.ident; + let original_ident = &f.name; let mut wire_params: Vec = Vec::new(); // Each entry is a per-input decode statement. Fallible decodes are diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index b93f5d7a..4f7d152d 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -65,7 +65,10 @@ pub(crate) enum ParamForm { /// One classified effective parameter (a source param, or one expansion leaf). pub(crate) struct PlanLeaf { - pub ty: syn::Type, + /// The leaf's **reading** — classification and spelling in one value, so + /// the two cannot disagree and no consumer has to look the type up. Spell + /// with `reading.origin.syntax`. + pub reading: crate::api::core::flat::TypeRef, /// Kotlin parameter name (`kt_param_name(ident)`: camelCase + /// hard-keyword escaping) — shared by the wrapper signature and the /// `external fun` declaration. @@ -313,8 +316,7 @@ pub(crate) fn validate_bindings( if !declared.contains(ident) { continue; } - let item_fn = &f.origin.syntax; - match ext.fn_plan(registry, item_fn) { + match ext.fn_plan(registry, f) { Ok(plan) => record_symbol(&plan.native_symbol, ident.to_string(), &mut errors), Err(e) => errors.push(e.message(ident)), } @@ -331,10 +333,10 @@ pub(crate) fn validate_bindings( if !declared_consts.contains(ident) { continue; } - let getter = const_getter_fn(&c.origin.syntax); + let getter = const_getter_fn(c); match ext.fn_plan(registry, &getter) { Ok(plan) => record_symbol(&plan.native_symbol, ident.to_string(), &mut errors), - Err(e) => errors.push(e.message(&getter.sig.ident)), + Err(e) => errors.push(e.message(&getter.name)), } } } @@ -348,10 +350,10 @@ pub(crate) fn validate_bindings( .collect(); expr_decls.sort_by(|a, b| a.kotlin_name.cmp(&b.kotlin_name)); for decl in expr_decls { - let getter = const_expr_getter_fn(&decl.kotlin_name, &decl.ty); + let getter = const_expr_getter_fn(&decl.kotlin_name, &decl.ty, registry); match ext.fn_plan(registry, &getter) { Ok(plan) => record_symbol(&plan.native_symbol, decl.kotlin_name.clone(), &mut errors), - Err(e) => errors.push(e.message(&getter.sig.ident)), + Err(e) => errors.push(e.message(&getter.name)), } } @@ -393,15 +395,15 @@ impl Declarations { pub(crate) fn fn_plan( &self, registry: &Registry, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, ) -> Result, PlanError> { - if let Some(hit) = self.fn_plans.borrow().get(&f.sig.ident).cloned() { + if let Some(hit) = self.fn_plans.borrow().get(&f.name).cloned() { return Ok(hit); } let plan = std::rc::Rc::new(JniFunctionPlan::build(self, registry, f)?); self.fn_plans .borrow_mut() - .insert(f.sig.ident.clone(), plan.clone()); + .insert(f.name.clone(), plan.clone()); Ok(plan) } } @@ -413,40 +415,56 @@ impl JniFunctionPlan { pub fn build( ext: &Declarations, registry: &Registry, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, ) -> Result { - let jni_method = ext.mangle_jni_method(&kt_snake_to_camel(&f.sig.ident.to_string())); + let jni_method = ext.mangle_jni_method(&kt_snake_to_camel(&f.name.to_string())); let native_symbol = ext.native_method_symbol(&jni_method); - let onerror_iface = onerror_iface_spec(ext, registry, &f.sig.ident); + let onerror_iface = onerror_iface_spec(ext, registry, &f.name); // Output first: the Rust emitter historically resolved the output // before the inputs, so an unresolved-output failure takes precedence // over an unresolved-input one. let output = build_output(ext, registry, f)?; let mut params = Vec::new(); - for input in &f.sig.inputs { - let syn::FnArg::Typed(pt) = input else { - continue; - }; - let syn::Pat::Ident(pid) = &*pt.pat else { - continue; - }; - let ident = pid.ident.clone(); - let ty = (*pt.ty).clone(); + // The element's parameters: each already a name and a `TypeRef`, so + // there is no `FnArg`/`Pat` destructuring and no position that could + // fail to yield a type. + for param in &f.params { + let ident = param.name.clone(); + let ty = param.ty.origin.syntax.clone(); let form = if let Some(plan) = registry .expansion_plans() - .get(&(f.sig.ident.clone(), ident.clone())) + .get(&(f.name.clone(), ident.clone())) { let mut leaves = Vec::new(); for leaf in &plan.leaves { + // The ONE lookup left on this path: `FoldLeaf::ty` is a + // `syn::Type` in core, so the reading has to be fetched + // rather than carried. #275's second half removes it by + // making the plan leaves carry `TypeRef`; until then a miss + // means the leaf's type never entered the pipeline, which + // is worth naming rather than absorbing. + let leaf_reading = registry.reading(&leaf.ty).unwrap_or_else(|| { + panic!( + "fold leaf `{}` of `{}`: type `{}` never entered the pipeline", + leaf.name, + f.name, + quote::ToTokens::to_token_stream(&leaf.ty), + ) + }); leaves.push(classify_leaf( - ext, registry, &leaf.name, &leaf.ty, /*expanded=*/ true, &ident, + ext, + registry, + &leaf.name, + &leaf_reading, + /*expanded=*/ true, + &ident, )?); } ParamForm::Expanded(leaves) } else { ParamForm::Single(Box::new(classify_leaf( - ext, registry, &ident, &ty, /*expanded=*/ false, &ident, + ext, registry, &ident, ¶m.ty, /*expanded=*/ false, &ident, )?)) }; params.push(PlanParam { ident, ty, form }); @@ -474,7 +492,11 @@ impl JniFunctionPlan { }) } - fn jvm_parameter_slots(&self, registry: &Registry, f: &syn::ItemFn) -> usize { + fn jvm_parameter_slots( + &self, + registry: &Registry, + f: &crate::api::core::flat::Function, + ) -> usize { // `JNINative` is a Kotlin object, so its external methods are instance // methods and the JVM counts the implicit receiver as one unit. let mut slots = 1usize; @@ -489,7 +511,7 @@ impl JniFunctionPlan { InputKind::Handle { .. } | InputKind::VecBuild { .. } => 2, InputKind::Callback { .. } => 1, InputKind::Unsigned64 { .. } | InputKind::Plain => registry - .input_entry(&leaf.ty) + .input_entry(&leaf.reading.origin.syntax) .and_then(|entry| JniPrim::from_wire(&entry.destination)) .map_or(1, |prim| match prim { JniPrim::Long | JniPrim::Double => 2, @@ -503,7 +525,7 @@ impl JniFunctionPlan { FnOutputPlan::Value(_) => 0, }; slots += 1; // binding-error sink - if registry.error_plans().contains_key(&f.sig.ident) { + if registry.error_plans().contains_key(&f.name) { slots += 1; } slots @@ -525,11 +547,14 @@ fn classify_leaf( ext: &Declarations, registry: &Registry, ident: &syn::Ident, - ty: &syn::Type, + reading: &crate::api::core::flat::TypeRef, expanded: bool, source_param: &syn::Ident, ) -> Result { - let optional = registry.is_optional(ty); + // The reading, so the layer questions cannot miss. What generated Rust must + // spell is `origin.syntax`, unchanged. + let ty = &reading.origin.syntax; + let optional = reading.optional_inner().is_some(); let as_enum_value = ext.is_kotlin_enum(&enum_probe_type(ty)); let kt_name = kt_param_name(&ident.to_string()); @@ -538,7 +563,7 @@ fn classify_leaf( if let Some(args) = extract_fn_trait_args(ty) { let iface = ext.iface_spec(registry, &SpecKey::callback(&args)); return Ok(PlanLeaf { - ty: ty.clone(), + reading: reading.clone(), kt_name, kt_public: None, kt_meta: registry @@ -582,8 +607,9 @@ fn classify_leaf( }, Some(ProjectionKind::Unsigned64) => InputKind::Unsigned64 { niche: entry.metadata.projection.as_ref().and_then(|p| { - registry - .is_optional(ty) + reading + .optional_inner() + .is_some() .then(|| p.niche_sentinels.first().cloned()) .flatten() }), @@ -601,7 +627,7 @@ fn classify_leaf( }; Ok(PlanLeaf { - ty: ty.clone(), + reading: reading.clone(), kt_name, kt_public, kt_meta, @@ -619,13 +645,13 @@ fn classify_leaf( fn build_output( ext: &Declarations, registry: &Registry, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, ) -> Result { use crate::api::core::{ types_util::result_ok_type, unfold::{Delivery, UnfoldShape}, }; - let ident = &f.sig.ident; + let ident = &f.name; let unfold_plan = registry.unfold_plans().get(ident); // Callback delivery: the return is decomposed to a foreign builder/fold @@ -668,10 +694,9 @@ fn build_output( // `Return` delivery, the `Result` Ok type when an error plan peels, else // the function's own return. let is_convert = unfold_plan.is_some(); - let return_ty: syn::Type = match &f.sig.output { - syn::ReturnType::Default => syn::parse_quote!(()), - syn::ReturnType::Type(_, ty) => (**ty).clone(), - }; + // The element normalizes an elided return and a written `-> ()` to one + // `Unit` reading, so there is no `ReturnType` match here. + let return_ty: syn::Type = f.ret.origin.syntax.clone(); let error_plan = registry.error_plans().get(ident); let ok_ty = error_plan.and_then(|_| result_ok_type(&return_ty)); let target_ty = match unfold_plan { @@ -695,7 +720,8 @@ fn build_output( let ret_decl: syn::ReturnType = if is_convert { syn::parse_quote!(-> #target_ty) } else { - f.sig.output.clone() + let ret = &f.ret.origin.syntax; + syn::parse_quote!(-> #ret) }; let (surface, canonical) = ReturnSurface::classify(ext, registry, &ret_decl); let is_enum = ext.is_kotlin_enum(&canonical); diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 891acb1e..c6a5773b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -925,11 +925,7 @@ impl Declarations { imports.insert(format!("{}.{}", self.package, self.jni_native_class_name())); } for m in members.iter().filter(|m| m.kind == MemberKind::Method) { - if let Some(item_fn) = registry - .flat() - .function(&m.rust_ident) - .map(|func| &func.origin.syntax) - { + if let Some(item_fn) = registry.flat().function(&m.rust_ident) { if let Some(f) = crate::api::lang::jnigen::jni::render_wrapper_fn( self, item_fn, @@ -959,11 +955,7 @@ impl Declarations { .map(|c| *c) .unwrap_or_else(|| KtClass::companion_object().vis(Vis::Public)); for m in ctors { - if let Some(item_fn) = registry - .flat() - .function(&m.rust_ident) - .map(|func| &func.origin.syntax) - { + if let Some(item_fn) = registry.flat().function(&m.rust_ident) { if let Some(f) = crate::api::lang::jnigen::jni::render_wrapper_fn( self, item_fn, @@ -1604,7 +1596,6 @@ impl Declarations { entry.rust_ident, ) }); - let item_fn = &item_fn.origin.syntax; let kotlin_name = self.effective_function_name(subpackage, entry); if let Some(f) = render_wrapper_fn(self, item_fn, registry, Some(&kotlin_name), None) { // #52: idiomatic typed overloads for `.split_on_param` @@ -1621,7 +1612,6 @@ impl Declarations { let item_const = registry .flat() .constant(&entry.rust_ident) - .map(|konst| &konst.origin.syntax) .unwrap_or_else(|| { panic!( "write_jni_package: const `{}` registered via .constant(...) is \ @@ -1630,7 +1620,7 @@ impl Declarations { entry.rust_ident, ) }); - reject_handle_const(self, item_const); + reject_handle_const(self, &item_const.origin.syntax); if let Some((helper, prop)) = render_const_val( self, &package, @@ -1658,8 +1648,7 @@ impl Declarations { entry.rust_ident, ) }); - let item_fn = &item_fn.origin.syntax; - validate_constant_fn(self, item_fn); + validate_constant_fn(self, &item_fn.origin.syntax); if let Some((helper, prop)) = render_constant_fn_val( self, &package, @@ -1717,7 +1706,7 @@ impl Declarations { if !declared.contains(&f.name) { continue; } - if let Some(fun) = render_extern_decl(self, &f.origin.syntax, registry) { + if let Some(fun) = render_extern_decl(self, f, registry) { externs.push(fun); } } @@ -1733,11 +1722,7 @@ impl Declarations { .collect(); const_idents.sort_by_key(|i| i.to_string()); for ident in const_idents { - let Some(item_const) = registry - .flat() - .constant(&ident) - .map(|konst| &konst.origin.syntax) - else { + let Some(item_const) = registry.flat().constant(&ident) else { continue; // missing decl already warned by the scan }; let getter = crate::api::lang::jnigen::jni::const_getter_fn(item_const); @@ -1755,7 +1740,7 @@ impl Declarations { .collect(); expr_decls.sort_by(|a, b| a.kotlin_name.cmp(&b.kotlin_name)); for decl in expr_decls { - let getter = const_expr_getter_fn(&decl.kotlin_name, &decl.ty); + let getter = const_expr_getter_fn(&decl.kotlin_name, &decl.ty, registry); if let Some(fun) = render_extern_decl(self, &getter, registry) { externs.push(fun); } diff --git a/prebindgen/src/api/lang/jnigen/jni/overloads.rs b/prebindgen/src/api/lang/jnigen/jni/overloads.rs index 72d9c020..bb168b10 100644 --- a/prebindgen/src/api/lang/jnigen/jni/overloads.rs +++ b/prebindgen/src/api/lang/jnigen/jni/overloads.rs @@ -318,7 +318,7 @@ fn find_block(params: &[kt::KtParam], leaf_names: &[String]) -> Option { /// hard errors — the user explicitly asked to split it). fn resolve_split<'a>( registry: &'a Registry, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, sel_fun: &kt::KtFun, param_name: &str, multi: bool, @@ -326,25 +326,25 @@ fn resolve_split<'a>( let param = syn::Ident::new(param_name, Span::call_site()); let plan = registry .expansion_plans() - .get(&(f.sig.ident.clone(), param.clone())) + .get(&(f.name.clone(), param.clone())) .unwrap_or_else(|| { panic!( "fun!({}).split_on_param(\"{param_name}\"): `{param_name}` is not an expandable \ parameter (it has no `expand_param!` variants)", - f.sig.ident + f.name ) }); assert!( plan.selector.is_some(), "fun!({}).split_on_param(\"{param_name}\"): `{param_name}` has a single variant — there \ is nothing to split (it already flattens to one signature)", - f.sig.ident + f.name ); assert!( plan_in_scope(plan), "fun!({}).split_on_param(\"{param_name}\"): `{param_name}` has a recursively-built arm — \ it cannot be overloaded; keep the selector form", - f.sig.ident + f.name ); let leaf_names: Vec = plan .leaves @@ -356,7 +356,7 @@ fn resolve_split<'a>( panic!( "fun!({}).split_on_param(\"{param_name}\"): could not locate the parameter's leaf \ block in the generated wrapper", - f.sig.ident + f.name ) }); let block = &sel_fun.params[start..start + len]; @@ -376,7 +376,7 @@ fn resolve_split<'a>( panic!( "fun!({}).split_on_param(\"{param_name}\"): an arm has a non-flat input; \ it cannot be overloaded", - f.sig.ident + f.name ) }); (vi, typed) @@ -387,7 +387,7 @@ fn resolve_split<'a>( "fun!({}).split_on_param(\"{param_name}\"): `{param_name}` is an `Option<_>` parameter \ and none of its arms is a single leaf — its overload has no clean nullable type; keep \ the selector form", - f.sig.ident + f.name ); Split { param, @@ -406,7 +406,7 @@ fn resolve_split<'a>( /// error) if the product has two combinations with the same JVM signature. pub(crate) fn render_param_overloads( ext: &Declarations, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, registry: &Registry, sel_fun: &kt::KtFun, ) -> Vec { @@ -415,33 +415,25 @@ pub(crate) fn render_param_overloads( let want: std::collections::HashSet<&str> = ext .fn_split_params .iter() - .filter(|(func, _)| func == &f.sig.ident) + .filter(|(func, _)| func == &f.name) .map(|(_, p)| p.as_str()) .collect(); if want.is_empty() { return Vec::new(); } - f.sig - .inputs + f.params .iter() - .filter_map(|a| match a { - syn::FnArg::Typed(pt) => match &*pt.pat { - syn::Pat::Ident(pid) if want.contains(pid.ident.to_string().as_str()) => { - Some(pid.ident.to_string()) - } - _ => None, - }, - _ => None, - }) + .filter(|p| want.contains(p.name.to_string().as_str())) + .map(|p| p.name.to_string()) .collect() }; // Any requested name that didn't match a real parameter is a typo — surface // it rather than silently dropping. for (func, p) in &ext.fn_split_params { - if func == &f.sig.ident && !requested.iter().any(|r| r == p) { + if func == &f.name && !requested.iter().any(|r| r == p) { panic!( "fun!({}).split_on_param(\"{p}\"): no parameter named `{p}` on this function", - f.sig.ident + f.name ); } } @@ -477,7 +469,7 @@ pub(crate) fn render_param_overloads( "fun!({}): split_on_param product is ambiguous — combinations {} and {} both \ surface as `({})`; add .no_split() intent is not enough here, disambiguate \ the constructors or drop one .split_on_param", - f.sig.ident, + f.name, combo_label(&splits, &combos[i]), combo_label(&splits, &combos[j]), sigs[i] @@ -534,7 +526,7 @@ pub(crate) fn render_param_overloads( seen.insert(p.name.clone()), "fun!({}): split overload has a duplicate parameter name `{}` — rename the \ constructor parameter", - f.sig.ident, + f.name, p.name ); } diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index a8e8f50c..8503ab1e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -305,11 +305,7 @@ pub(crate) fn build_typed_handle( .param(kt::KtParam::new("ptr", kt::KtType::long())), ); for m in members.iter().filter(|m| m.kind == MemberKind::Constructor) { - if let Some(item_fn) = registry - .flat() - .function(&m.rust_ident) - .map(|func| &func.origin.syntax) - { + if let Some(item_fn) = registry.flat().function(&m.rust_ident) { if let Some(f) = render_wrapper_fn( ext, item_fn, @@ -418,11 +414,7 @@ pub(crate) fn build_typed_handle( // (receiver bound to `this`), delegating to the same centralized // `JNINative` extern as a free wrapper would. for m in members.iter().filter(|m| m.kind == MemberKind::Method) { - if let Some(item_fn) = registry - .flat() - .function(&m.rust_ident) - .map(|func| &func.origin.syntax) - { + if let Some(item_fn) = registry.flat().function(&m.rust_ident) { if let Some(f) = render_wrapper_fn( ext, item_fn, @@ -460,7 +452,7 @@ pub(crate) fn is_iterable_fold(shape: &crate::api::core::unfold::UnfoldShape) -> /// resolved. Full-FQN types throughout — no derivation-time shortening. pub(crate) fn render_extern_decl( ext: &Declarations, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, registry: &Registry, ) -> Option { // The name and wire params come straight off the lowered plan — the @@ -545,7 +537,7 @@ pub(crate) fn render_extern_decl( // erased to `Any` (JObject) on the wire; the wrapper passes a capture for // each. A domain plan ⇒ `error_plans` has this fn. params.push(kt::KtParam::new("errorSink", kt::KtType::any())); - if registry.error_plans().contains_key(&f.sig.ident) { + if registry.error_plans().contains_key(&f.name) { params.push(kt::KtParam::new("domainSink", kt::KtType::any())); } @@ -748,7 +740,7 @@ pub(crate) struct WrapperSurface { /// (`build_native_call` / `render_body` / KDoc / opaque-lock collection). pub(crate) fn build_wrapper_surface( ext: &Declarations, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, registry: &Registry, kotlin_name_override: Option<&str>, receiver_key: Option<&TypeKey>, @@ -761,7 +753,7 @@ pub(crate) fn build_wrapper_surface( // to hit the one extern that the Rust extern actually emits. let kt_name = match kotlin_name_override { Some(n) => n.to_string(), - None => kt_snake_to_camel(&f.sig.ident.to_string()), + None => kt_snake_to_camel(&f.name.to_string()), }; let jni_call = fplan.jni_method.clone(); let (params, receiver_idx) = @@ -827,7 +819,7 @@ pub(crate) fn build_wrapper_surface( pub(crate) fn render_wrapper_fn( ext: &Declarations, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, registry: &Registry, kotlin_name_override: Option<&str>, receiver_key: Option<&TypeKey>, @@ -885,24 +877,24 @@ pub(crate) fn render_wrapper_fn( pub(crate) fn render_const_val( ext: &Declarations, package: &str, - c: &syn::ItemConst, + c: &crate::api::core::flat::Constant, registry: &Registry, imports: &mut BTreeSet, kotlin_name_override: Option<&str>, ) -> Option<(kt::KtFun, kt::KtProperty)> { let getter = const_getter_fn(c); - let default = kt_snake_to_camel(&getter.sig.ident.to_string()); + let default = kt_snake_to_camel(&getter.name.to_string()); let helper_name = ext.mangle_fun(package, &default); let helper = render_wrapper_fn(ext, &getter, registry, Some(&helper_name), None)?; let val_name = kotlin_name_override .map(str::to_string) - .unwrap_or_else(|| c.ident.to_string()); + .unwrap_or_else(|| c.name.to_string()); let framework_line = format!( "Mirrors the Rust `#[prebindgen]` const `{}` (read lazily, once, through \ the generated JNI getter on first use).", - c.ident + c.name ); - let kdoc = crate::api::lang::jnigen::util::doc_string(&c.attrs) + let kdoc = crate::api::lang::jnigen::util::doc_string(&c.origin.syntax.attrs) .map(|d| format!("{d}\n\n{framework_line}")) .unwrap_or(framework_line); render_val_over_helper(ext, registry, helper, val_name, kdoc, imports) @@ -916,23 +908,23 @@ pub(crate) fn render_const_val( pub(crate) fn render_constant_fn_val( ext: &Declarations, package: &str, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, registry: &Registry, imports: &mut BTreeSet, kotlin_name_override: Option<&str>, ) -> Option<(kt::KtFun, kt::KtProperty)> { - let default = kt_snake_to_camel(&f.sig.ident.to_string()); + let default = kt_snake_to_camel(&f.name.to_string()); let helper_name = ext.mangle_fun(package, &default); let helper = render_wrapper_fn(ext, f, registry, Some(&helper_name), None)?; let val_name = kotlin_name_override .map(str::to_string) - .unwrap_or_else(|| f.sig.ident.to_string()); + .unwrap_or_else(|| f.name.to_string()); let framework_line = format!( "Mirrors the Rust `#[prebindgen]` fn `{}()` (evaluated lazily, once, \ through the generated JNI wrapper on first use).", - f.sig.ident + f.name ); - let kdoc = crate::api::lang::jnigen::util::doc_string(&f.attrs) + let kdoc = crate::api::lang::jnigen::util::doc_string(&f.origin.syntax.attrs) .map(|d| format!("{d}\n\n{framework_line}")) .unwrap_or(framework_line); render_val_over_helper(ext, registry, helper, val_name, kdoc, imports) @@ -950,8 +942,8 @@ pub(crate) fn render_const_expr_val( registry: &Registry, imports: &mut BTreeSet, ) -> Option<(kt::KtFun, kt::KtProperty)> { - let getter = const_expr_getter_fn(&decl.kotlin_name, &decl.ty); - let default = kt_snake_to_camel(&getter.sig.ident.to_string()); + let getter = const_expr_getter_fn(&decl.kotlin_name, &decl.ty, registry); + let default = kt_snake_to_camel(&getter.name.to_string()); let helper_name = ext.mangle_fun(package, &default); let helper = render_wrapper_fn(ext, &getter, registry, Some(&helper_name), None)?; let expr = decl.expr.to_token_stream(); @@ -1077,7 +1069,7 @@ fn classify_params( let mut params: Vec = Vec::new(); for leaf in fplan.leaves() { let mut name = leaf.kt_name.clone(); - let arg_ty = &leaf.ty; + let arg_ty = &leaf.reading.origin.syntax; // Instance-method receiver: the first parameter whose peeled Rust type // is the owning class binds to `this` (so `this_ptr`/`this.ptr`/lock or @@ -1205,9 +1197,15 @@ fn classify_params( // Handle → Borrow/Consume by Rust syntactic shape (locked); // `Option<&T>` / by-value `Option` mark the param nullable // and the wrapper body branches on null before lock selection. - if registry.is_optional_borrow(arg_ty) { + // Both read off the leaf's own reading — no lookup, and a + // wrapped spelling answers as the bare one does. + if leaf + .reading + .optional_inner() + .is_some_and(|i| i.borrow_target().is_some()) + { ParamMode::BorrowNullable - } else if registry.is_optional(arg_ty) { + } else if leaf.reading.optional_inner().is_some() { // by-value `Option` opaque → nullable consume ParamMode::ConsumeNullable } else if matches!(arg_ty, syn::Type::Reference(_)) { @@ -1258,12 +1256,12 @@ fn classify_params( /// unchanged). fn classify_output( ext: &Declarations, - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, fplan: &JniFunctionPlan, registry: &Registry, imports: &mut BTreeSet, ) -> Option { - let unfold = registry.unfold_plans().get(&f.sig.ident); + let unfold = registry.unfold_plans().get(&f.name); // `builder_param` is the trailing **lambda** param (build / fold) as a // `(name, function-type)` pair. For the `Iterable` shape, the non-lambda // accumulator (`acc: A`) goes in `builder_lead` — it must precede @@ -1607,7 +1605,7 @@ fn collect_opaques(params: &[Param]) -> Vec { /// separate SAM param; the wrapper passes a per-thread capture to the extern, /// then after the native call redispatches to whichever channel fired. fn error_sink_parts( - f: &syn::ItemFn, + f: &crate::api::core::flat::Function, fplan: &JniFunctionPlan, registry: &Registry, imports: &mut BTreeSet, @@ -1624,7 +1622,7 @@ fn error_sink_parts( let domain = if let Some(domain_spec) = &ifaces.domain { let error_plan = registry .error_plans() - .get(&f.sig.ident) + .get(&f.name) .expect("domain handler ⇒ error plan"); // Per ze leaf: (raw capture Kotlin type, raw→typed wrap). The CAPTURE // is the raw twin (what the native side calls); the user's handler is @@ -2223,8 +2221,11 @@ pub(crate) fn kt_param_name(rust_ident: &str) -> String { /// documenting the REAL prototype after all expansions — one note per /// position a plan reshaped, phrased for the caller. `None` for an /// undocumented, unshaped fn. -fn wrapper_kdoc(f: &syn::ItemFn, registry: &Registry) -> Option { - let prose = crate::api::lang::jnigen::util::doc_string(&f.attrs); +fn wrapper_kdoc( + f: &crate::api::core::flat::Function, + registry: &Registry, +) -> Option { + let prose = crate::api::lang::jnigen::util::doc_string(&f.origin.syntax.attrs); let notes = shape_notes(f, registry); match (prose, notes) { (Some(p), Some(n)) => Some(format!("{p}\n\n{n}")), @@ -2239,8 +2240,11 @@ fn wrapper_kdoc(f: &syn::ItemFn, registry: &Registry) -> Option) -> Option { - let fn_ident = &f.sig.ident; +fn shape_notes( + f: &crate::api::core::flat::Function, + registry: &Registry, +) -> Option { + let fn_ident = &f.name; let mut notes: Vec = Vec::new(); let mut plans: Vec<(&syn::Ident, &crate::api::core::expand::FoldPlan)> = registry diff --git a/prebindgen/src/api/lang/jnigen/jni/report.rs b/prebindgen/src/api/lang/jnigen/jni/report.rs index aff72a82..5e472df7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/report.rs +++ b/prebindgen/src/api/lang/jnigen/jni/report.rs @@ -189,11 +189,7 @@ impl super::JniGen { ) { let ext = self.declarations(); let registry = self.registry(); - let Some(item_fn) = registry - .flat() - .function(&rust_ident) - .map(|func| &func.origin.syntax) - else { + let Some(item_fn) = registry.flat().function(&rust_ident) else { return; }; let Some(f) = render_wrapper_fn(ext, item_fn, registry, kotlin_name, receiver_key) else { diff --git a/prebindgen/src/api/lang/jnigen/jni/symbols.rs b/prebindgen/src/api/lang/jnigen/jni/symbols.rs index fdce8523..44354f79 100644 --- a/prebindgen/src/api/lang/jnigen/jni/symbols.rs +++ b/prebindgen/src/api/lang/jnigen/jni/symbols.rs @@ -172,11 +172,7 @@ pub(crate) fn validate_symbols(ext: &Declarations, registry: &Registry) -> TokenStream { + fn on_function( + &self, + f: &crate::api::core::flat::Function, + registry: &Registry, + ) -> TokenStream { emit_jni_function_wrapper(self, f, registry) } - fn on_struct(&self, _s: &syn::ItemStruct, _registry: &Registry) -> TokenStream { + fn on_struct( + &self, + _s: &crate::api::core::flat::Struct, + _registry: &Registry, + ) -> TokenStream { // Struct converter bodies are emitted by the resolver via // input_terminal / output_terminal below; no separate // per-struct item is needed. TokenStream::new() } - fn on_enum(&self, _e: &syn::ItemEnum, _registry: &Registry) -> TokenStream { + fn on_variant( + &self, + _v: &crate::api::core::flat::Variant, + _registry: &Registry, + ) -> TokenStream { + TokenStream::new() + } + + fn on_enum( + &self, + _e: &crate::api::core::flat::Enum, + _registry: &Registry, + ) -> TokenStream { TokenStream::new() } @@ -1736,14 +1756,18 @@ impl Prebindgen for Declarations { /// extern. The getter reuses the whole function-wrapper pipeline (so the /// const's type flows through the ordinary output-converter machinery); /// only the callee expression differs — a path to the const, not a call. - fn on_const(&self, c: &syn::ItemConst, registry: &Registry) -> TokenStream { - reject_handle_const(self, c); + fn on_const( + &self, + c: &crate::api::core::flat::Constant, + registry: &Registry, + ) -> TokenStream { + reject_handle_const(self, &c.origin.syntax); let getter = const_getter_fn(c); - let const_ident = &c.ident; + let const_ident = &c.name; let source_module = self.fn_module(registry, const_ident); let callee: syn::Expr = syn::parse_quote!(#source_module::#const_ident); let wrapper = emit_jni_function_wrapper_with_callee(self, &getter, registry, Some(callee)); - let alias = crate::api::core::const_path_alias(c, &source_module); + let alias = crate::api::core::const_path_alias(&c.origin.syntax, &source_module); quote! { #alias #wrapper From 01c9e5ddd515f6c40195adaeb81babce5788e054 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 04:48:31 +0200 Subject: [PATCH 28/52] docs: the item methods take elements (L5), and classify.rs is closed (#277) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two completion facts the design record was missing. L5 gains a ticked item for #276: `on_function`/`on_struct`/`on_enum`/`on_const` taking elements. It was the widest part of the public `syn` surface and the one that decided what adapters could KNOW — an adapter handed a `flat::Function` cannot ask what a parameter means and be told "no reading". Recorded as landing out of stage order, and why: it was the last thing keeping L4's spelling accessors alive, not a decision to start L5. L4's `classify.rs` item is ticked. Its one named leak (`DataStruct { st: &syn::ItemStruct }`) closed with #267, which NEEDED it — a field record cannot carry its own reading while the walk is handed a `syn::ItemStruct`. The issue body recorded that at the time; the doc did not. The seed counts in L4's file list are left alone: they are a measured baseline with provenance, not live figures, and the live ones are the ledger's. --- docs/language-integration.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 97dacb6d..3725cf7a 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -443,8 +443,11 @@ The long pole — 97 sites, down from 106 because #248 took `jni/builder` from 1 - [ ] `emit/names` (17), `jni/trait_impl` (11), `emit/wrapper` (11), `emit/flat_input` (10), `render` (8), `selector` (7), `iface` (5), `jni/builder` (4), and the rest -- [ ] `classify.rs` — a whole classifier with **zero** watched sites, so the - ledger cannot see it: it must be migrated on its own merit +- [x] `classify.rs` — a whole classifier with **zero** watched sites, so the + ledger cannot see it: it must be migrated on its own merit. Its one leak + (`DataStruct { st: &syn::ItemStruct }`) closed with #267, which needed it: + a field record cannot carry its own reading while the walk is handed a + `syn::ItemStruct` - [ ] `prim_array_of` reads `ArrayExtent` instead of re-matching `Type::Array` - [ ] Generated Rust and Kotlin byte-identical @@ -456,6 +459,14 @@ growing back. - [x] `Registry`'s public item maps stop being the adapter-facing contract — done early by L1.5, which deleted them outright; relates to [#92](https://github.com/milyin/prebindgen/issues/92) +- [x] `Prebindgen::on_function` / `on_struct` / `on_enum` / `on_const` take + **elements**, not `syn` items — the item methods were the widest part of + the public `syn` surface, and the one that decided what adapters could + know. An adapter handed a `flat::Function` cannot ask what a parameter + means and be told "no reading"; a `&syn::ItemFn` gave no such guarantee. + `on_enum` split into `on_variant` + `on_enum` along the model's own + distinction. Done as #275's first half rather than waiting for this stage, + because it was the last thing keeping the spelling accessors alive - [ ] `Prebindgen::post_process_item(&mut syn::Item)` — the hook that let qualification live in an adapter in the first place - [ ] `ConverterImpl::function` / `TypeEntry::function` as `syn::ItemFn`; From 88c38ea3bd6314310cbfad3194eed51c1622d89a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 10:37:05 +0200 Subject: [PATCH 29/52] Core's plan leaves carry the reading (#278) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second of three for #275. `UnfoldLeaf::out_ty` and `FoldLeaf::ty` were `syn::Type`, so an adapter consuming a decomposition had to hand the spelling back to the registry to learn what it meant — the round trip #263 removed from `api/core`, surviving in the plans. The reading was being DISCARDED at construction: `flatten` peels `TypeRef`s and stored `origin.syntax`. Most producers just stop discarding. Five genuinely compose a type no source wrote — a borrow, two `Option` layers, a presence flag, a selector — and `Flat::classify` is not available for them: it lowers SOURCE syntax, and `classify_has_no_caller_outside_the_registry` keeps it that way, correctly. So `flat` composes its own types: `TypeRef::{borrowed, optional, scalar, named}`. Each builds `kind` AND the matching `origin.syntax` in one place, so the classification and the spelling cannot disagree. `scalar`/`named` are PLACELESS — no file wrote a presence flag, and claiming a location would make a fabricated one indistinguishable from a real one, the same call `Flat::classify` already makes. Layered ones keep the inner's location. `a_composed_type_keys_as_its_spelling` pins what would otherwise break silently: a composed `&T` must key EXACTLY as `parse_quote!(&#t)`, or `require_output` registers a different cell and resolution changes. Checked before the migration, not after. `constructed_value_layers` returns the core reading it already held. `synth_sum_leaves` takes `flat::Variant`, so payload types are readings and the tag readback reads `TypeId` instead of taking a path apart. `opt()` — whose whole problem was building a spelling with no classification beside it — is deleted, as is the lookup #276 added in `fn_plan`. Two review catches, both real. `TypeId::name` stores a raw identifier as `"r#type"`, and `Ident::new` PANICS on that spelling rather than erroring — `TypeId::ident()` parses instead, in one place. The end-to-end test for it found a second, pre-existing instance in `emitted_source_type_names` that I had wrongly judged synthetic. And the first version of that test passed with the fix reverted, because it matched the raw name coming from the INPUT converter; it now drives the sum through the output encoder and was verified to fail at `sum_out.rs:198` without the fix. `is_optional` 4 -> 3 callers; the rest need the adapter-side reading sources, which is #275's last part. Goldens BYTE-IDENTICAL. 548 lib tests, `--all --all-features` 14/14, clippy, fmt. covertest 48/48 on the JVM. Both ledgers unmoved. --- prebindgen/src/api/core/expand.rs | 65 +++++----- prebindgen/src/api/core/expand/plan.rs | 16 ++- prebindgen/src/api/core/expand/tests.rs | 122 +++++++++++++++--- .../src/api/core/flat/tests/acceptance.rs | 94 ++++++++++++++ prebindgen/src/api/core/flat/ty.rs | 98 ++++++++++++++ prebindgen/src/api/core/registry/order.rs | 2 +- prebindgen/src/api/core/unfold.rs | 28 ++-- prebindgen/src/api/core/unfold/plan.rs | 17 ++- prebindgen/src/api/core/unfold/tests.rs | 109 +++++++++++++--- prebindgen/src/api/lang/jnigen/jni/builder.rs | 11 +- .../src/api/lang/jnigen/jni/emit/callback.rs | 4 +- .../src/api/lang/jnigen/jni/emit/delivery.rs | 24 ++-- .../api/lang/jnigen/jni/emit/struct_out.rs | 2 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 55 +++++--- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 24 +--- prebindgen/src/api/lang/jnigen/jni/iface.rs | 10 +- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 9 +- .../src/api/lang/jnigen/jni/tests/sealed.rs | 79 ++++++++++++ .../api/lang/jnigen/jni/tests/value_form.rs | 7 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 14 +- 21 files changed, 642 insertions(+), 150 deletions(-) diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index a5c9a7a3..b1a942fb 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -297,7 +297,7 @@ fn process_expand( // The boundary layers: `Option<&T>` → optional + by_ref, `Option` → // optional, `&T` → by_ref, and `target` is what is left under them. let (optional, by_ref, target) = constructed_value_layers(¶m_ty); - let target_key = TypeKey::from_type(&target); + let target_key = target.key(); let variants = resolve_constructor(exp, registry, &target_key, ed)?; let mut visited: HashSet = HashSet::new(); @@ -313,7 +313,7 @@ fn process_expand( )?; for leaf in &plan.leaves { - registry.require_input(&leaf.ty); + registry.require_input(&leaf.ty.origin.syntax); } registry .expansion_plans @@ -394,7 +394,7 @@ fn build_plan( ed: &ExpandDecl, optional: bool, by_ref: bool, - target: &syn::Type, + target: &crate::api::core::flat::TypeRef, variants: &[Variant], visited: &mut HashSet, ) -> Result { @@ -414,7 +414,7 @@ fn build_plan( if optional { let [Variant::Ctor(func)] = variants else { // Combined-selector dispatch under `Optional`. - visited.insert(TypeKey::from_type(target)); + visited.insert(target.key()); let prefix = param.to_string(); let (selector, fold_variants) = build_core( exp, @@ -427,9 +427,9 @@ fn build_plan( &mut leaves, visited, )?; - visited.remove(&TypeKey::from_type(target)); + visited.remove(&target.key()); return Ok(FoldPlan { - target: target.clone(), + target: target.origin.syntax.clone(), by_ref, shape: FoldShape::Optional((), Box::new(FoldShape::Base)), leaves, @@ -439,15 +439,15 @@ fn build_plan( }); }; let sig = ctor_signature(registry, func)?; - check_target(func, &sig.target, target)?; + check_target(func, &sig.target, &target.origin.syntax)?; if sig.params.len() == 1 { let (_pn, pty) = &sig.params[0]; leaves.push(FoldLeaf { name: param.clone(), - ty: opt(&pty.origin.syntax), + ty: pty.optional(), }); return Ok(FoldPlan { - target: target.clone(), + target: target.origin.syntax.clone(), by_ref, shape: FoldShape::Optional((), Box::new(FoldShape::Base)), leaves, @@ -464,7 +464,8 @@ fn build_plan( // Multi-arg: presence flag (leaf 0) + one flat leaf per ctor arg. leaves.push(FoldLeaf { name: ident(&format!("{}_present", param)), - ty: syn::parse_quote!(bool), + // A presence flag no source wrote — placeless by construction. + ty: crate::api::core::flat::TypeRef::scalar(crate::api::core::flat::ScalarKind::Bool), }); let prefix = param.to_string(); let mut inputs = Vec::new(); @@ -490,7 +491,7 @@ fn build_plan( inputs.push(arg); } return Ok(FoldPlan { - target: target.clone(), + target: target.origin.syntax.clone(), by_ref, shape: FoldShape::Optional((), Box::new(FoldShape::Base)), leaves, @@ -507,7 +508,7 @@ fn build_plan( // Non-optional: build the (possibly recursive) construct core. The target is // on the cycle chain so a constructor parameter of the same type is rejected. - visited.insert(TypeKey::from_type(target)); + visited.insert(target.key()); let prefix = param.to_string(); let (selector, fold_variants) = build_core( exp, @@ -520,9 +521,9 @@ fn build_plan( &mut leaves, visited, )?; - visited.remove(&TypeKey::from_type(target)); + visited.remove(&target.key()); Ok(FoldPlan { - target: target.clone(), + target: target.origin.syntax.clone(), by_ref, shape: FoldShape::Base, leaves, @@ -542,7 +543,7 @@ fn build_core( exp: &Expansions, registry: &Registry, ed: &ExpandDecl, - target: &syn::Type, + target: &crate::api::core::flat::TypeRef, variants: &[Variant], by_ref: bool, prefix: &str, @@ -552,7 +553,7 @@ fn build_core( if let [Variant::Ctor(func)] = variants { // Single constructor — no selector; args passed directly (not Option-wrapped). let sig = ctor_signature(registry, func)?; - check_target(func, &sig.target, target)?; + check_target(func, &sig.target, &target.origin.syntax)?; let np = sig.params.len(); let mut args = Vec::new(); for (pname, pty) in &sig.params { @@ -579,14 +580,15 @@ fn build_core( let sel_idx = leaves.len(); leaves.push(FoldLeaf { name: ident(&format!("{}_sel", prefix)), - ty: syn::parse_quote!(i32), + // The selector, likewise composed and placeless. + ty: crate::api::core::flat::TypeRef::scalar(crate::api::core::flat::ScalarKind::I32), }); let mut fold_variants: Vec = Vec::new(); for (vi, v) in variants.iter().enumerate() { match v { Variant::Ctor(func) => { let sig = ctor_signature(registry, func)?; - check_target(func, &sig.target, target)?; + check_target(func, &sig.target, &target.origin.syntax)?; let np = sig.params.len(); let mut args = Vec::new(); for (pi, (_pname, pty)) in sig.params.iter().enumerate() { @@ -612,9 +614,9 @@ fn build_core( Variant::Identity => { let idx = leaves.len(); let leaf_ty = if by_ref { - opt(&syn::parse_quote!(&#target)) + target.borrowed().optional() } else { - opt(target) + target.optional() }; leaves.push(FoldLeaf { name: ident(&format!("{}_{}", prefix, vi)), @@ -649,7 +651,7 @@ fn build_arg( ) -> Result { // The boundary layers down to the parameter's core type. let (popt, pby_ref, bare) = constructed_value_layers(pty); - let key = TypeKey::from_type(&bare); + let key = bare.key(); // A default constructor for the parameter's type ⇒ recursive nested build. let canon = exp .constructors @@ -687,7 +689,7 @@ fn build_arg( )?; visited.remove(&key); Ok(FoldArg::Build(Box::new(FoldBuild { - target: bare, + target: bare.origin.syntax.clone(), by_ref: pby_ref, selector, variants: vars, @@ -703,9 +705,9 @@ fn build_arg( leaves.push(FoldLeaf { name, ty: if dispatched && !passthrough { - opt(&pty.origin.syntax) + pty.optional() } else { - pty.origin.syntax.clone() + pty.clone() }, }); Ok(FoldArg::Leaf(idx, passthrough)) @@ -1089,17 +1091,22 @@ fn constructed_value(reading: &crate::api::core::flat::TypeRef) -> syn::Type { } /// [`constructed_value`], plus which of the two layers were there. -fn constructed_value_layers(reading: &crate::api::core::flat::TypeRef) -> (bool, bool, syn::Type) { +fn constructed_value_layers( + reading: &crate::api::core::flat::TypeRef, +) -> (bool, bool, crate::api::core::flat::TypeRef) { let optional = reading.optional_inner().is_some(); let after_opt = reading.optional_inner().unwrap_or(reading); let by_ref = after_opt.borrow_target().is_some(); let core = after_opt.borrow_target().unwrap_or(after_opt); - (optional, by_ref, core.origin.syntax.clone()) + // The core READING, not its spelling: the plan composes `Option<&T>` over + // it, and composing from a reading keeps the kind and the syntax paired. + (optional, by_ref, core.clone()) } -fn opt(ty: &syn::Type) -> syn::Type { - syn::parse_quote!(Option<#ty>) -} +// `opt` lived here — `parse_quote!(Option<#ty>)` — and built a spelling with no +// classification beside it, so every consumer had to hand it back to the +// registry to learn it was an optional. `TypeRef::optional` composes both at +// once (#275). #[cfg(test)] mod tests; diff --git a/prebindgen/src/api/core/expand/plan.rs b/prebindgen/src/api/core/expand/plan.rs index 49263d25..a424ad64 100644 --- a/prebindgen/src/api/core/expand/plan.rs +++ b/prebindgen/src/api/core/expand/plan.rs @@ -60,10 +60,18 @@ impl FoldPlan { pub struct FoldLeaf { /// Foreign-side parameter name. pub name: syn::Ident, - /// Rust type whose resolved **input** converter decodes this leaf. For a - /// single constructor these are the raw constructor parameter types; for a - /// combined one the selector (`i32`) and `Option`-wrapped variant inputs. - pub ty: syn::Type, + /// The **reading** of the type whose resolved input converter decodes this + /// leaf. For a single constructor these are the raw constructor parameter + /// types; for a combined one the selector (`i32`) and `Option`-wrapped + /// variant inputs. Spell it with `ty.origin.syntax`. + /// + /// A reading rather than a spelling for the reason [`UnfoldLeaf::out_ty`] + /// gives: a consumer asking what this leaf's type MEANS had to hand the + /// spelling back to the registry (#275). The leaves no source wrote — the + /// presence flag, the selector — are built by + /// [`TypeRef::scalar`](crate::api::core::flat::TypeRef::scalar), which + /// pairs the kind with its own spelling and is placeless by construction. + pub ty: crate::api::core::flat::TypeRef, } /// One dispatch arm of a [`FoldPlan`]. diff --git a/prebindgen/src/api/core/expand/tests.rs b/prebindgen/src/api/core/expand/tests.rs index 2fae7ac9..f4292514 100644 --- a/prebindgen/src/api/core/expand/tests.rs +++ b/prebindgen/src/api/core/expand/tests.rs @@ -1,6 +1,16 @@ use quote::ToTokens; use super::*; + +/// A reading for a fixture type — see the twin in `core/unfold/tests.rs`. +/// Plan leaves carry `TypeRef`s, and a fixture naming a type inline needs one. +fn tref(ty: syn::Type) -> crate::api::core::flat::TypeRef { + crate::api::core::flat::Flat::builder() + .build() + .expect("an empty model") + .classify(&ty) + .expect("a fixture type the language accepts") +} use crate::api::{core::registry::Registry, test_util::scanned_with as reg_with}; fn src_qualify(id: &syn::Ident) -> syn::Path { @@ -40,7 +50,15 @@ fn single_constructor_plan_and_fold() { assert_eq!(plan.selector, None); assert_eq!(plan.leaves.len(), 1); assert_eq!(plan.leaves[0].name.to_string(), "a"); - assert_eq!(plan.leaves[0].ty.to_token_stream().to_string(), "String"); + assert_eq!( + plan.leaves[0] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), + "String" + ); let locals = vec![ident("a")]; let folded = emit_fold(plan, &locals, &src_qualify); @@ -87,14 +105,32 @@ fn constructor_plan_and_fold() { assert_eq!(plan.selector, Some(0)); // selector + try_from(String) + identity(ZKeyExpr) = 3 leaves assert_eq!(plan.leaves.len(), 3); - assert_eq!(plan.leaves[0].ty.to_token_stream().to_string(), "i32"); assert_eq!( - plan.leaves[1].ty.to_token_stream().to_string(), + plan.leaves[0] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), + "i32" + ); + assert_eq!( + plan.leaves[1] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < String >" ); // `&ZKeyExpr` consumer ⇒ borrowed identity leaf (clone-preserving). assert_eq!( - plan.leaves[2].ty.to_token_stream().to_string(), + plan.leaves[2] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < & ZKeyExpr >" ); assert_eq!(plan.variants.len(), 2); @@ -104,7 +140,7 @@ fn constructor_plan_and_fold() { // Leaf types registered as required inputs (so the resolver builds // their converters). - assert!(reg.input_types[&TypeKey::from_type(&plan.leaves[1].ty)].root); + assert!(reg.input_types[&plan.leaves[1].ty.key()].root); let locals = vec![ident("sel"), ident("v0"), ident("vid")]; let folded = emit_fold(plan, &locals, &src_qualify); @@ -150,7 +186,12 @@ fn optional_byvalue_single_ctor() { assert_eq!(plan.leaves.len(), 1); // nullable leaf wrapping the ctor param assert_eq!( - plan.leaves[0].ty.to_token_stream().to_string(), + plan.leaves[0] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < Vec < u8 > >" ); @@ -201,7 +242,12 @@ fn optional_byref_single_ctor() { assert!(plan.produces_option()); assert!(plan.by_ref, "Option<&T> ⇒ by_ref"); assert_eq!( - plan.leaves[0].ty.to_token_stream().to_string(), + plan.leaves[0] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < String >" ); assert_eq!( @@ -254,12 +300,33 @@ fn optional_byref_multi_arg_ctor() { // leaf 0 = present:bool, leaf 1 = id:i32, leaf 2 = schema:Option assert_eq!(plan.leaves.len(), 3); assert_eq!(plan.leaves[0].name.to_string(), "encoding_present"); - assert_eq!(plan.leaves[0].ty.to_token_stream().to_string(), "bool"); + assert_eq!( + plan.leaves[0] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), + "bool" + ); assert_eq!(plan.leaves[1].name.to_string(), "encoding_id"); - assert_eq!(plan.leaves[1].ty.to_token_stream().to_string(), "i32"); + assert_eq!( + plan.leaves[1] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), + "i32" + ); assert_eq!(plan.leaves[2].name.to_string(), "encoding_schema"); assert_eq!( - plan.leaves[2].ty.to_token_stream().to_string(), + plan.leaves[2] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < String >" ); @@ -332,18 +399,41 @@ fn optional_combined_selector_encodes_absence() { // (passthrough), identity:Option<&ZEncoding>. assert_eq!(plan.leaves.len(), 4); assert_eq!(plan.leaves[0].name.to_string(), "encoding_sel"); - assert_eq!(plan.leaves[0].ty.to_token_stream().to_string(), "i32"); assert_eq!( - plan.leaves[1].ty.to_token_stream().to_string(), + plan.leaves[0] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), + "i32" + ); + assert_eq!( + plan.leaves[1] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < i32 >" ); assert_eq!( - plan.leaves[2].ty.to_token_stream().to_string(), + plan.leaves[2] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < String >", "already-Option ctor arg is NOT double-wrapped" ); assert_eq!( - plan.leaves[3].ty.to_token_stream().to_string(), + plan.leaves[3] + .ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < & ZEncoding >" ); assert!( @@ -386,7 +476,7 @@ fn iterable_emit_shape() { shape: FoldShape::Iterable(Box::new(FoldShape::Base)), leaves: vec![FoldLeaf { name: ident("kes"), - ty: syn::parse_quote!(Vec), + ty: tref(syn::parse_quote!(Vec)), }], selector: None, present: None, @@ -584,7 +674,7 @@ fn recursive_input_nests_param_constructors() { let leaf_tys: Vec = plan .leaves .iter() - .map(|l| l.ty.to_token_stream().to_string()) + .map(|l| l.ty.origin.syntax.to_token_stream().to_string()) .collect(); assert!( leaf_tys.iter().any(|t| t.contains("i32")), diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index b10a1aba..c9268ad6 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -1770,3 +1770,97 @@ fn the_layer_stack_stops_at_an_out_of_order_layer() { ("Optional(Iterable(Base))".into(), "& Sample".into(), 3) ); } + +/// A **composed** type keys exactly as the spelling it replaces, and classifies +/// as what it was built from. +/// +/// The decomposition plans compose types no source wrote — the borrow of a +/// value, a presence flag, a selector — and those types are registered as +/// crossings like any other. If a composed `&T` keyed differently from +/// `parse_quote!(&#t)`, it would register a *different cell* and resolution +/// would silently change. So the identity is pinned, not assumed. +/// +/// The pairing is the point: `kind` and `origin.syntax` are built together in +/// one place, so a consumer classifying off one and spelling off the other +/// cannot be handed a disagreement. +#[test] +fn a_composed_type_keys_as_its_spelling() { + use crate::api::core::{ + flat::{ScalarKind, TypeKind, TypeRef}, + registry::TypeKey, + }; + + let t = lower(quote::quote!(u64)).expect("in the language"); + + let borrowed = t.borrowed(); + assert_eq!(borrowed.key(), TypeKey::from_type(&syn::parse_quote!(&u64))); + assert!(matches!(borrowed.kind, TypeKind::Ref { .. })); + // The layer wraps the reading it was built from, so peeling gets it back. + assert_eq!(borrowed.borrow_target().expect("a borrow").key(), t.key()); + + let optional = t.optional(); + assert_eq!( + optional.key(), + TypeKey::from_type(&syn::parse_quote!(Option)) + ); + assert_eq!(optional.optional_inner().expect("optional").key(), t.key()); + + // A scalar the binding invented: spelled from its own kind, so the two + // cannot drift. + assert_eq!( + TypeRef::scalar(ScalarKind::Bool).key(), + TypeKey::from_type(&syn::parse_quote!(bool)) + ); + assert_eq!( + TypeRef::scalar(ScalarKind::I32).key(), + TypeKey::from_type(&syn::parse_quote!(i32)) + ); + assert_eq!( + TypeRef::named(&syn::parse_quote!(ZEnum)).key(), + TypeKey::from_type(&syn::parse_quote!(ZEnum)) + ); + + // Composed-from-nothing is PLACELESS: no file wrote it, and claiming a + // location would make a fabricated one indistinguishable from a real one. + assert!( + !TypeRef::scalar(ScalarKind::Bool) + .origin + .location + .has_position(), + "a scalar no source wrote carries no position" + ); + // A layered one keeps the inner's location — the borrow exists because of + // that value. + assert_eq!(&*borrowed.origin.location, &*t.origin.location); +} + +/// A **raw** identifier survives the round trip through `TypeId`. +/// +/// `TypeId::name` is a `String`, so an enum legitimately named `r#type` is +/// stored as `"r#type"` — and `Ident::new` *rejects* that spelling, panicking +/// rather than returning an error. A consumer rebuilding an ident from the name +/// therefore has to parse, and gets no warning until a source happens to use a +/// keyword. `TypeId::ident` is where that recovery lives (#278 review). +#[test] +fn a_raw_identifier_survives_typeid() { + use crate::api::core::flat::{TypeKind, TypeRef}; + + let raw: syn::Ident = syn::parse_quote!(r#type); + assert_eq!(raw.to_string(), "r#type", "the hash is part of the name"); + + let t = TypeRef::named(&raw); + let TypeKind::Named { id } = &t.kind else { + panic!("named") + }; + // Recovered, and it spells itself back the way it was written. + let back = id.ident().expect("a raw ident is still an ident"); + assert_eq!(back, raw); + assert_eq!(tokens(&t.origin.syntax), "r#type"); + + // A path-qualified name is not a single identifier — the same answer the + // old `bare_path_ident` gave. + let qualified = crate::api::core::flat::TypeId { + name: "foreign::Option".to_string(), + }; + assert!(qualified.ident().is_none()); +} diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 4d227651..e13a364e 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -140,6 +140,83 @@ impl TypeRef { } } + // ── Composition ─────────────────────────────────────────────── + // + // Building a type the SOURCE did not write, as opposed to reading one it + // did. The decomposition plans need this: a leaf may be the borrow of a + // value, a presence flag, or a selector — none of which any source spelled, + // and all of which have to carry a reading like everything else. + // + // Here rather than at the callers, and not via + // [`Flat::classify`](super::Flat::classify), because the two acts are + // different. `classify` lowers *source syntax* — it is the frontend reading + // what a crate wrote, and `classify_has_no_caller_outside_the_registry` + // keeps it that way. These compose a type from parts already understood, + // which needs no lowering at all: each builds `kind` **and** the matching + // `origin.syntax` in one place, so the classification and the spelling + // cannot disagree — the invariant every consumer of a `TypeRef` relies on. + + /// A borrow of this type — `&T` from `T`. + /// + /// Keeps this type's location: the borrow exists *because of* this value, + /// so a diagnostic about it should point where the value came from. + pub fn borrowed(&self) -> TypeRef { + let inner = &self.origin.syntax; + TypeRef { + kind: TypeKind::Ref { + mode: RefMode::Shared, + inner: Box::new(self.clone()), + }, + origin: self.origin.with(syn::parse_quote!(&#inner)), + } + } + + /// An optional of this type — `Option` from `T`. Location as + /// [`Self::borrowed`]. + pub fn optional(&self) -> TypeRef { + let inner = &self.origin.syntax; + TypeRef { + kind: TypeKind::Optional(Box::new(self.clone())), + origin: self.origin.with(syn::parse_quote!(Option<#inner>)), + } + } + + /// A scalar the binding invented — a presence flag, a selector. + /// + /// **Placeless**, and deliberately: no file wrote it, so claiming a location + /// would make a fabricated one indistinguishable from a real one. + /// [`Flat::classify`](super::Flat::classify) does exactly this for a + /// composed spelling, and `ensure_entry` gives adapter-authored cells the + /// same treatment — `has_position` already gates what a diagnostic prints. + pub fn scalar(kind: ScalarKind) -> TypeRef { + // The spelling comes from the kind, so the two cannot drift. + let ident = syn::Ident::new(kind.as_str(), proc_macro2::Span::call_site()); + TypeRef { + kind: TypeKind::Scalar(kind), + origin: Origin::new( + syn::parse_quote!(#ident), + std::rc::Rc::new(crate::SourceLocation::default()), + ), + } + } + + /// A nominal reference to a declared type, by name. Placeless for the same + /// reason as [`Self::scalar`] — this is the binding naming a type, not a + /// source mentioning one. + pub fn named(ident: &syn::Ident) -> TypeRef { + TypeRef { + kind: TypeKind::Named { + id: TypeId { + name: ident.to_string(), + }, + }, + origin: Origin::new( + syn::parse_quote!(#ident), + std::rc::Rc::new(crate::SourceLocation::default()), + ), + } + } + /// This type's identity as a table key. /// /// The canonical spelling is what a key *is* (#113), and reading it is @@ -364,9 +441,30 @@ pub struct TypeId { /// The path as written, minus any generic arguments — `Foo`, /// `foreign::Option`. Normalized, so a reducible std or source-module path /// has already collapsed to its final segment. + /// + /// A `String`, so a **raw** identifier is stored the way `Ident` prints + /// it — `r#type`, hashes and all. Recover it with [`Self::ident`] rather + /// than `Ident::new`, which rejects that spelling. pub name: String, } +impl TypeId { + /// This name as an identifier, **raw forms included**. + /// + /// `Ident::new("r#type", …)` *panics* — it takes a bare name, not a + /// spelling — so a consumer rebuilding an ident from [`Self::name`] has to + /// parse rather than construct. Here so that recovery is written once: the + /// caller that gets it wrong does not fail until a source happens to use a + /// keyword, which is exactly the kind of bug that ships. + /// + /// `None` for a name that is not a single identifier at all (a + /// path-qualified `foreign::Option`), which is the same answer + /// `bare_path_ident` gave for one. + pub fn ident(&self) -> Option { + syn::parse_str::(&self.name).ok() + } +} + /// The primitives the source language accepts. Mirrors the set every adapter /// already treats as directly representable. #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/prebindgen/src/api/core/registry/order.rs b/prebindgen/src/api/core/registry/order.rs index e020a8c2..c474c071 100644 --- a/prebindgen/src/api/core/registry/order.rs +++ b/prebindgen/src/api/core/registry/order.rs @@ -106,7 +106,7 @@ impl Registry { for arg in args { if let Some(plan) = self.callback_arg_plans.get(&TypeKey::from_type(&arg)) { for leaf in &plan.leaves { - out.push((Direction::Output, leaf.out_ty.clone())); + out.push((Direction::Output, leaf.out_ty.origin.syntax.clone())); } } } diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index 9bc6698a..a4273ebd 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -459,7 +459,7 @@ pub fn apply( continue; } for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty); + registry.require_output(&leaf.out_ty.origin.syntax); } registry.callback_arg_plans.insert(key, plan); } @@ -640,7 +640,7 @@ fn wire_fixed_returns( } } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty); + registry.require_output(&leaf.out_ty.origin.syntax); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -707,7 +707,7 @@ fn wire_fixed_callbacks( continue; } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty); + registry.require_output(&leaf.out_ty.origin.syntax); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -1036,7 +1036,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, element)?; let plan = build_plan(acc, registry, ed, by_ref, element, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty); + registry.require_output(&leaf.out_ty.origin.syntax); } plan } else { @@ -1082,7 +1082,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, source)?; let plan = build_plan(acc, registry, ed, by_ref, source, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty); + registry.require_output(&leaf.out_ty.origin.syntax); } plan }; @@ -1110,7 +1110,7 @@ fn process_decl( && plan.leaves.len() == 1 && !plan.leaves[0].nullable; let plan = if single_return { - let leaf_ty = plan.leaves[0].out_ty.clone(); + let leaf_ty = plan.leaves[0].out_ty.origin.syntax.clone(); let cv_ty: syn::Type = if matches!(plan.shape, UnfoldShape::Optional((), _)) { syn::parse_quote!(Option<#leaf_ty>) } else { @@ -1357,11 +1357,13 @@ fn flatten( // A plan field: the drop to spelling happens here, where the value // is stored for emission, and the borrowed form is composed rather // than looked up because no source wrote it. - let src = &source.origin.syntax; - let out_ty: syn::Type = if place_is_owned(hoists, path_prefix, by_ref) { - src.clone() + // The borrowed form is COMPOSED — no source wrote it — and the + // composition pairs the kind with its own spelling, so nothing + // downstream has to look either up. + let out_ty = if place_is_owned(hoists, path_prefix, by_ref) { + source.clone() } else { - syn::parse_quote!(&#src) + source.borrowed() }; leaves.push(UnfoldLeaf { name: if path_prefix.is_empty() { @@ -1554,7 +1556,7 @@ fn flatten( leaves.push(UnfoldLeaf { name: seg_name(&fr.name).join("__"), path: field_path, - out_ty: fr.ty.origin.syntax.clone(), + out_ty: fr.ty.clone(), identity: false, nullable, source: LeafSource::Field, @@ -1647,9 +1649,9 @@ fn flatten( // A plan field: the spelling is taken here, once, where the // leaf is stored for emission. let (out_ty, nullable, identity) = if cond_handle { - (core.origin.syntax.clone(), true, true) + (core.clone(), true, true) } else { - (ret.origin.syntax.clone(), nullable, false) + (ret.clone(), nullable, false) }; let mut path = path_prefix.to_vec(); path.push(PathStep::call(func.clone(), opt, !core_by_ref)); diff --git a/prebindgen/src/api/core/unfold/plan.rs b/prebindgen/src/api/core/unfold/plan.rs index 30a5c1e4..52ba7225 100644 --- a/prebindgen/src/api/core/unfold/plan.rs +++ b/prebindgen/src/api/core/unfold/plan.rs @@ -296,10 +296,19 @@ pub struct UnfoldLeaf { /// `[Call(f)]` = `f(&root)`; longer = nested records, M3). Steps of both /// kinds may mix — see [`PathStep`]. pub path: Vec, - /// Type whose resolved **output** converter encodes this leaf — a - /// reference type for accessors (`&str`, `&F`), `&Source` for the identity - /// leaf (so the borrowed-opaque clone converter / projection is reused). - pub out_ty: syn::Type, + /// The **reading** of the type whose resolved output converter encodes this + /// leaf — a reference type for accessors (`&str`, `&F`), `&Source` for the + /// identity leaf (so the borrowed-opaque clone converter / projection is + /// reused). Spell it with `out_ty.origin.syntax`. + /// + /// A reading rather than a spelling because a consumer asking what this + /// leaf's type *means* had to hand the spelling back to the registry and + /// hope for a cell — the round trip #263 removed from `api/core`, surviving + /// in the plans, and answering "no layer" for a type it had never seen + /// (#275). The composed ones (`&Source`) are built by + /// [`TypeRef::borrowed`](crate::api::core::flat::TypeRef::borrowed), which + /// pairs the kind with its own spelling. + pub out_ty: crate::api::core::flat::TypeRef, /// `true` for the move/clone-the-value handle leaf, emitted **last** (after /// every reference leaf's JVM conversion has ended its borrow). pub identity: bool, diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index 758c7495..f8ae249e 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -6,6 +6,22 @@ use crate::api::{ test_util::scanned_with as reg_with, }; +/// A reading for a fixture type, lowered by the model. +/// +/// Plan leaves carry `TypeRef`s, and these fixtures assert on plan STRUCTURE — +/// so they need a reading for a type they name inline. Lowering it through an +/// empty `Flat` gives the same classification the pipeline would, without +/// standing up a source crate for it. Legitimate here and nowhere else: the +/// `classify` guard exempts tests precisely because a fixture composing its own +/// input is not a consumer reasoning from `origin`. +fn tref(ty: syn::Type) -> crate::api::core::flat::TypeRef { + crate::api::core::flat::Flat::builder() + .build() + .expect("an empty model") + .classify(&ty) + .expect("a fixture type the language accepts") +} + /// A generous `.fun_accessor` set covering every function used as a /// deconstructor record across these tests (a superset is fine — `apply` /// only checks records are members). The `nested_record_*` tests that @@ -95,7 +111,15 @@ fn accessor_optional_primitive() { plan.leaves[0].path[0].ident().to_string(), "z_timestamp_ntp64" ); - assert_eq!(plan.leaves[0].out_ty.to_token_stream().to_string(), "i64"); + assert_eq!( + plan.leaves[0] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), + "i64" + ); assert!( reg.output_types[&TypeKey::from_type(&syn::parse_quote!(i64))].root, "the leaf type must be a root" @@ -151,7 +175,12 @@ fn accessor_plan_byref() { assert!(plan.leaves[0].identity); assert!(plan.leaves[0].path.is_empty()); assert_eq!( - plan.leaves[0].out_ty.to_token_stream().to_string(), + plan.leaves[0] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "& ZKeyExpr" ); // Accessor leaf: out_ty `&str`, path `[z_keyexpr_as_str]`. @@ -161,7 +190,15 @@ fn accessor_plan_byref() { plan.leaves[1].path[0].ident().to_string(), "z_keyexpr_as_str" ); - assert_eq!(plan.leaves[1].out_ty.to_token_stream().to_string(), "& str"); + assert_eq!( + plan.leaves[1] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), + "& str" + ); // Leaf out_tys registered as required outputs so the resolver builds // their converters. @@ -505,7 +542,12 @@ fn nested_accessor_flatten() { assert_eq!(path(&plan.leaves[2]), "z_sample_payload.z_zbytes_to_bytes"); assert_eq!(path(&plan.leaves[3]), "z_sample_kind"); assert_eq!( - plan.leaves[3].out_ty.to_token_stream().to_string(), + plan.leaves[3] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "SampleKind" ); assert_eq!( @@ -643,7 +685,12 @@ fn reply_product_double_option_flatten() { // Acc leaf keeping its full `Option<…>` return — not a nesting step. assert_eq!(path(&plan.leaves[0]), "z_reply_replier_zid"); assert_eq!( - plan.leaves[0].out_ty.to_token_stream().to_string(), + plan.leaves[0] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "Option < ZZenohId >" ); assert!(!plan.leaves[0].nullable && !plan.leaves[0].identity); @@ -809,7 +856,12 @@ fn iterable_decomposed_plan() { "z_zenoh_id_to_string" ); assert_eq!( - plan.leaves[0].out_ty.to_token_stream().to_string(), + plan.leaves[0] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "String" ); // Identity leaf: owned value (`ZZenohId`, not `&ZZenohId`) since the Vec @@ -817,7 +869,12 @@ fn iterable_decomposed_plan() { assert!(plan.leaves[1].identity); assert!(plan.leaves[1].path.is_empty()); assert_eq!( - plan.leaves[1].out_ty.to_token_stream().to_string(), + plan.leaves[1] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "ZZenohId" ); } @@ -1076,7 +1133,7 @@ fn value_struct_vec_is_fixed_iterable_fold() { let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { name: name.to_string(), path: vec![PathStep::field(ident(name), false)], - out_ty: ty, + out_ty: tref(ty), identity: false, nullable: false, source: LeafSource::Field, @@ -1129,7 +1186,7 @@ fn value_struct_slice_callback_is_fixed_iterable_fold() { let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { name: name.to_string(), path: vec![PathStep::field(ident(name), false)], - out_ty: ty, + out_ty: tref(ty), identity: false, nullable: false, source: LeafSource::Field, @@ -1221,7 +1278,12 @@ fn convert_error_decomposes_result_e() { assert_eq!(plan.delivery, Delivery::Callback); assert_eq!(plan.leaves.len(), 1); assert_eq!( - plan.leaves[0].out_ty.to_token_stream().to_string(), + plan.leaves[0] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "String" ); assert_eq!(plan.source.to_token_stream().to_string(), "ZError"); @@ -1341,7 +1403,12 @@ fn callback_arg_plan_derived() { "z_sample_key_expr" ); assert_eq!( - plan.leaves[0].out_ty.to_token_stream().to_string(), + plan.leaves[0] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "& ZKeyExpr" ); assert_eq!( @@ -1349,7 +1416,12 @@ fn callback_arg_plan_derived() { "z_keyexpr_as_str" ); assert_eq!( - plan.leaves[2].out_ty.to_token_stream().to_string(), + plan.leaves[2] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "SampleKind" ); // Leaf out_tys registered so the resolver builds their converters. @@ -1424,7 +1496,12 @@ fn callback_arg_borrowed_decomposed() { "z_sample_key_expr" ); assert_eq!( - plan.leaves[2].out_ty.to_token_stream().to_string(), + plan.leaves[2] + .out_ty + .origin + .syntax + .to_token_stream() + .to_string(), "SampleKind" ); } @@ -1671,7 +1748,7 @@ fn reading_sum_decon() -> SumDecon { let tag = UnfoldLeaf { name: "tag".to_string(), path: vec![], - out_ty: syn::parse_quote!(i32), + out_ty: tref(syn::parse_quote!(i32)), identity: false, nullable: false, source: LeafSource::SumTag, @@ -1680,7 +1757,7 @@ fn reading_sum_decon() -> SumDecon { let field = |name: &str, variant: &str, idx: u32, ty: syn::Type, group: i32| UnfoldLeaf { name: name.to_string(), path: vec![], - out_ty: ty, + out_ty: tref(ty), identity: false, nullable: false, source: LeafSource::VariantField { @@ -1854,7 +1931,7 @@ fn a_vec_of_optionals_installs_no_fixed_fold() { let leaf = |name: &str, ty: syn::Type| UnfoldLeaf { name: name.to_string(), path: vec![PathStep::field(ident(name), false)], - out_ty: ty, + out_ty: tref(ty), identity: false, nullable: false, source: LeafSource::Field, diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 03c2fb24..7c380c2f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -1056,10 +1056,13 @@ impl Declarations { let crate::api::core::flat::TypeKind::Named { id } = &probe.kind else { panic!("a sum type is a named type") }; - let item_enum = registry + let crate::api::core::flat::Type::Variant(sum) = registry .flat() - .enum_item(&id.name) - .expect("TypeKind::Sum implies an indexed enum"); + .declared_type(&id.name) + .expect("TypeKind::Sum implies an indexed enum") + else { + panic!("TypeKind::Sum implies a payload-carrying enum") + }; let sum_cfg = self.types[&probe.key()] .sum() .expect("TypeKind::Sum implies a sealed-class config"); @@ -1068,7 +1071,7 @@ impl Declarations { name, ty: field.ty.clone(), decon: FieldDecon::Leaves(crate::api::lang::jnigen::jni::synth_sum_leaves( - self, sum_cfg, item_enum, + self, sum_cfg, sum, )), }); continue; diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs index 63d8e02c..bf7eaf4f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs @@ -77,7 +77,7 @@ pub(crate) fn callback_input( // Every leaf converter must already be resolved (deferral safety). // A synthesized leaf (a sum's tag) has no converter to wait for. for leaf in plan.leaves.iter().filter(|l| l.has_converter()) { - registry.output_entry(&leaf.out_ty)?; + registry.output_entry(&leaf.out_ty.origin.syntax)?; } let spec = folder_iface_for_plan(ext, registry, plan)?; let holder_slash = @@ -170,7 +170,7 @@ pub(crate) fn callback_input( // would make the trampoline wait forever on an `i32` crossing the // binding may not have. for leaf in plan.leaves.iter().filter(|l| l.has_converter()) { - let e = registry.output_entry(&leaf.out_ty)?; + let e = registry.output_entry(&leaf.out_ty.origin.syntax)?; if leaf.identity && e.metadata.projection.is_none() { return None; } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index 7c181019..3e7a0cd7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -478,7 +478,7 @@ pub(crate) fn reach_leaf_flat( // `single_return` in `core/unfold.rs`. `is_plain_field` is what that rules // out, and it stays as the local statement of the same fact. let reached_is_ours = if leaf.identity { - !matches!(leaf.out_ty, syn::Type::Reference(_)) + !matches!(leaf.out_ty.origin.syntax, syn::Type::Reference(_)) } else { consuming }; @@ -979,12 +979,14 @@ pub(crate) fn encode_plan_leaves( }; let (value, by_ref, path, consuming) = rebase(leaf); let value = &value; - let out_entry = registry.output_entry(&leaf.out_ty).unwrap_or_else(|| { - panic!( - "jnigen unfold: leaf `{}` has no registered output converter", - TypeKey::from_type(&leaf.out_ty) - ) - }); + let out_entry = registry + .output_entry(&leaf.out_ty.origin.syntax) + .unwrap_or_else(|| { + panic!( + "jnigen unfold: leaf `{}` has no registered output converter", + TypeKey::from_type(&leaf.out_ty.origin.syntax) + ) + }); let conv_fail = fail(quote!(__e.to_string())); // The leaf's COMPLETE Rust -> wire chain: the rust-side stages a custom // `convert!` declaration inserts (`Duration -> u64`), then the @@ -1045,7 +1047,7 @@ pub(crate) fn encode_plan_leaves( panic!( "jnigen unfold: identity leaf `{}` has no projection — \ `.accessor_record_id()` requires a ptr_class type", - TypeKey::from_type(&leaf.out_ty) + TypeKey::from_type(&leaf.out_ty.origin.syntax) ) }); // The place this handle lives, when it is OURS to give away — the @@ -1061,7 +1063,9 @@ pub(crate) fn encode_plan_leaves( // `Option` through the nullable branch's `match`, which moves the // whole `Option` in rather than borrowing it. let owned_place: Option = - if !matches!(leaf.out_ty, syn::Type::Reference(_)) && steps_are_movable(&path) { + if !matches!(leaf.out_ty.origin.syntax, syn::Type::Reference(_)) + && steps_are_movable(&path) + { let segs: Vec<&syn::Ident> = path.iter().map(PathStep::ident).collect(); Some(quote!(#value #(.#segs)*)) } else { @@ -1372,7 +1376,7 @@ pub(crate) fn leaf_is_prim( if leaf.nullable { return false; } - leaf_ty_is_prim(registry, &leaf.out_ty) + leaf_ty_is_prim(registry, &leaf.out_ty.origin.syntax) } /// The wire half of [`leaf_is_prim`]: does a leaf of this type occupy a **raw diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index af877a40..f2b3f863 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -146,7 +146,7 @@ pub(crate) fn synth_value_struct_leaves( leaves.push(UnfoldLeaf { name: leaf_name, path, - out_ty: effective_ty, + out_ty: field.ty.clone(), identity: false, nullable: false, source: LeafSource::Field, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index e79bbb4e..59c07def 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -43,38 +43,45 @@ pub(crate) const SUM_TAG_LEAF: &str = "tag"; pub(crate) fn synth_sum_leaves( ext: &Declarations, sum_cfg: &SumConfig, - item_enum: &syn::ItemEnum, + sum: &crate::api::core::flat::Variant, ) -> Vec { use crate::api::core::{ types_util::SumSpec, unfold::{LeafSource, UnfoldLeaf}, }; - let spec = SumSpec::from_item_enum(item_enum); + // `SumSpec` still reads the item — it owns the leaf-NAMING convention, which + // is jnigen's own. The payload TYPES come from the element beside it, whose + // fields are already readings, so nothing here has to compose or look one up. + let spec = SumSpec::from_item_enum(&sum.origin.syntax); // The selector rides ahead of the groups it chooses between, and carries // **which sum** it selects over as its `out_ty` — that is how the emitter // finds the enum to `match` when the sum is a field rather than the whole // returned value. Nothing looks up a converter for it (`has_converter()` is // false for a `SumTag`): there is no value to convert, the emitter assigns // the tag literal per arm. Its wire is a `jint` by definition. - let enum_ident = &item_enum.ident; + let enum_ident = &sum.name; let mut leaves = vec![UnfoldLeaf { name: SUM_TAG_LEAF.to_string(), path: Vec::new(), - out_ty: syn::parse_quote!(#enum_ident), + // Composed: the tag names WHICH sum it selects over, and no source + // wrote that as a standalone type. Nothing resolves a converter for it + // (`has_converter()` is false), but the emitter reads it back to find + // the enum to `match`. + out_ty: crate::api::core::flat::TypeRef::named(enum_ident), identity: false, nullable: false, source: LeafSource::SumTag, group: None, }]; - for variant in &spec.variants { + for (variant, alt) in spec.variants.iter().zip(&sum.alternatives) { let kotlin_name = ext.sum_variant_class_name(sum_cfg, &variant.ident); - for field in &variant.fields { + for (field, alt_field) in variant.fields.iter().zip(&alt.fields) { let prop = sum_field_prop_name(field); leaves.push(UnfoldLeaf { name: sum_slot_fragment(&kotlin_name, &prop), path: Vec::new(), - out_ty: field.ty.clone(), + out_ty: alt_field.ty.clone(), identity: false, nullable: false, source: LeafSource::VariantField { @@ -122,7 +129,7 @@ pub(crate) fn leaf_slot( ("I", format_ident!("i")) } else { let wire = registry - .output_entry(&leaf.out_ty) + .output_entry(&leaf.out_ty.origin.syntax) .expect("leaf_is_prim implies a resolved output entry") .destination .clone(); @@ -178,10 +185,20 @@ pub(crate) fn encode_sum_group( .iter() .find(|l| l.source == LeafSource::SumTag) .expect("a sum segment carries its selector leaf"); - let ident = bare_path_ident(&tag_leaf.out_ty).unwrap_or_else(|| { + // The name off the reading — `TypeId` IS the name, so nothing takes a path + // apart to re-derive one. + let crate::api::core::flat::TypeKind::Named { id } = &tag_leaf.out_ty.kind else { panic!( - "jnigen sum unfold: selector type `{}` is not a path type", - TypeKey::from_type(&tag_leaf.out_ty) + "jnigen sum unfold: selector type `{}` is not a named type", + tag_leaf.out_ty.key() + ) + }; + // Raw-aware: a sum may legitimately be named `r#type`, and `Ident::new` + // rejects that spelling. + let ident = id.ident().unwrap_or_else(|| { + panic!( + "jnigen sum unfold: selector type `{}` is not a single identifier", + id.name ) }); let module = ext.fn_module(registry, &ident); @@ -328,13 +345,15 @@ fn encode_group_leaf( bind: &syn::Ident, fail: &dyn Fn(TokenStream) -> TokenStream, ) -> TokenStream { - let out_entry = registry.output_entry(&leaf.out_ty).unwrap_or_else(|| { - panic!( - "jnigen sum unfold: payload leaf `{}` (`{}`) has no registered output converter", - leaf.name, - TypeKey::from_type(&leaf.out_ty) - ) - }); + let out_entry = registry + .output_entry(&leaf.out_ty.origin.syntax) + .unwrap_or_else(|| { + panic!( + "jnigen sum unfold: payload leaf `{}` (`{}`) has no registered output converter", + leaf.name, + TypeKey::from_type(&leaf.out_ty.origin.syntax) + ) + }); let wire = out_entry.destination.clone(); let conv_fail = fail(quote!(__e.to_string())); let enc = format_ident!("__enc_{}", obj_ident); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 541ccbfc..34832ce9 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -754,7 +754,7 @@ pub(crate) fn emit_expanded_param( debug_assert_eq!(plan.leaves.len(), leaves.len()); for (leaf, classified) in plan.leaves.iter().zip(leaves) { - let leaf_ty = &leaf.ty; + let leaf_ty = &leaf.ty.origin.syntax; let lookup_entry = || { registry.input_entry(leaf_ty).unwrap_or_else(|| { panic!( diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index 4f7d152d..82d4b230 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -438,27 +438,11 @@ impl JniFunctionPlan { { let mut leaves = Vec::new(); for leaf in &plan.leaves { - // The ONE lookup left on this path: `FoldLeaf::ty` is a - // `syn::Type` in core, so the reading has to be fetched - // rather than carried. #275's second half removes it by - // making the plan leaves carry `TypeRef`; until then a miss - // means the leaf's type never entered the pipeline, which - // is worth naming rather than absorbing. - let leaf_reading = registry.reading(&leaf.ty).unwrap_or_else(|| { - panic!( - "fold leaf `{}` of `{}`: type `{}` never entered the pipeline", - leaf.name, - f.name, - quote::ToTokens::to_token_stream(&leaf.ty), - ) - }); + // The lookup that stood here is gone: the fold leaf carries + // its own reading now, so there is nothing to fetch and + // nothing that can miss. leaves.push(classify_leaf( - ext, - registry, - &leaf.name, - &leaf_reading, - /*expanded=*/ true, - &ident, + ext, registry, &leaf.name, &leaf.ty, /*expanded=*/ true, &ident, )?); } ParamForm::Expanded(leaves) diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index f79c5e29..4945eb24 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -720,12 +720,13 @@ fn plan_leaf_param( // here and re-asserted (`!!`) inside its own live arm — the same rule // `nullable_group_part` applies to the parent-inlined `fromParts`. Primitive // slots take their `0`/`false` default and stay unboxed. - let inert_nullable = leaf.group.is_some() && !leaf_ty_is_prim(registry, &leaf.out_ty); + let inert_nullable = + leaf.group.is_some() && !leaf_ty_is_prim(registry, &leaf.out_ty.origin.syntax); leaf_iface_param( ext, registry, name, - &leaf.out_ty, + &leaf.out_ty.origin.syntax, leaf.nullable || inert_nullable, true, ) @@ -1166,11 +1167,12 @@ pub(crate) fn callback_iface_spec( }; if leaf.source == LeafSource::SumTag { any_fixed = true; - let fqn = ext.kotlin_fqn(&TypeKey::from_type(&leaf.out_ty))?; + let fqn = + ext.kotlin_fqn(&TypeKey::from_type(&leaf.out_ty.origin.syntax))?; let (reassemble, imports) = fixed_reassembly( ext, registry, - &leaf.out_ty, + &leaf.out_ty.origin.syntax, &plan.leaves[k..seg], &fqn, ); diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index c6a5773b..5c72dc75 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -1505,7 +1505,9 @@ impl Declarations { name: &str, imports: &mut BTreeSet, ) -> String { - let optional = registry.is_optional(&leaf.out_ty); + // Off the leaf's own reading — no lookup, and a wrapped spelling + // answers as the bare one does. + let optional = leaf.out_ty.optional_inner().is_some(); let arg = if param.raw.is_nullable() && !optional { format!("{name}!!") } else { @@ -1515,8 +1517,9 @@ impl Declarations { // it `Int` and the wrap has to name the enum class itself — read off the // same output-converter metadata `factory_field` reads for an enum // struct field. - if self.is_kotlin_enum(&enum_probe_type(&leaf.out_ty)) { - let inner = option_inner_type(&leaf.out_ty).unwrap_or_else(|| leaf.out_ty.clone()); + if self.is_kotlin_enum(&enum_probe_type(&leaf.out_ty.origin.syntax)) { + let inner = option_inner_type(&leaf.out_ty.origin.syntax) + .unwrap_or_else(|| leaf.out_ty.origin.syntax.clone()); let name = registry .output_entry(&inner) .and_then(|e| e.metadata.kotlin_name.clone()) diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs index 7fc4e25b..aa1517bb 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs @@ -1623,3 +1623,82 @@ fn slice_of_sum_callback_arg_is_rejected_with_its_reason() { "…and point at the two shapes that do work: {err}" ); } + +/// A sum named with a **raw** identifier generates, rather than aborting — and +/// specifically through the OUTPUT encoder, which is the site that broke. +/// +/// `r#type` is a legal `#[prebindgen]` enum name, and `TypeId::name` stores it +/// as the string `"r#type"`. The sum encoder rebuilds an ident from that name +/// to spell the `match`'s path, and `Ident::new` *panics* on that spelling +/// rather than erroring, so the whole generation aborted (#278 review). +/// +/// The sum is a value-form FIELD here, not just a declared type: that is what +/// puts it through `encode_sum_group`. A first version of this test declared +/// the sum and a callback only — the raw name appeared in the generated file +/// via the *input* converter, the assertion passed, and reverting the fix left +/// it passing. A regression test that survives its own regression is worse than +/// none, so this one asserts the output-side `match` path. +#[test] +fn a_raw_named_sum_generates() { + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum r#type { + Missing, + Exact(i64), + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZThingStruct { + pub reading: r#type, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_thing_to_struct(t: &ZThing) -> ZThingStruct { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_emit(cb: impl Fn(ZThing) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZThing)) + .class(crate::sealed_class!(r#type)) + .fun(crate::fun!(z_emit)), + ) + .expand(crate::expand_return!(ZThing).fields(crate::fields!(z_thing_to_struct))); + + let dir = unique_test_dir("jnigen_raw_sum"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = jni.build_with(registry).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust")) + .expect("read rust"); + + // The output-side match, spelled with the raw name — this is what + // `encode_sum_group` emits, and what `Ident::new` could not produce. + assert!( + rust.contains("myflat::r#type::Missing") && rust.contains("myflat::r#type::Exact"), + "the sum encoder matches the raw-named enum by its real path:\n{rust}" + ); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index 23d37c2a..40ec0a32 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -238,7 +238,12 @@ fn deriving_matches_the_equivalent_hand_written_list() { .callback_arg_plans .values() .flat_map(|p| p.leaves.iter()) - .map(|l| (l.name.clone(), l.out_ty.to_token_stream().to_string())) + .map(|l| { + ( + l.name.clone(), + l.out_ty.origin.syntax.to_token_stream().to_string(), + ) + }) .collect() }; diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index d1ce39b4..55e81012 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -348,7 +348,13 @@ impl Declarations { // `#[prebindgen]` item; else the default module (a declared // type re-exported by the primary source, or a deliberately // unmarked type like a convert!-only newtype). - let ident = syn::Ident::new(&short, Span::call_site()); + // Parsed, not constructed: a short name is whatever the source + // wrote, and `Ident::new` PANICS on a raw one (`r#type`) + // rather than erroring. Pre-existing; found by the raw-name + // regression added for the sum encoder's twin of this bug. + let Ok(ident) = syn::parse_str::(&short) else { + return; + }; let module = registry .origin_module(&ident) .unwrap_or_else(|| self.default_module(registry)); @@ -1326,13 +1332,15 @@ impl Declarations { let Some(ident) = bare_path_ident(&source) else { continue; }; - let Some(item_enum) = registry.flat().enum_item(&ident) else { + let Some(crate::api::core::flat::Type::Variant(sum)) = + registry.flat().declared_type(&ident) + else { continue; }; out.push(crate::api::core::unfold::SumDecon { key: key.clone(), source, - leaves: crate::api::lang::jnigen::jni::synth_sum_leaves(self, sum_cfg, item_enum), + leaves: crate::api::lang::jnigen::jni::synth_sum_leaves(self, sum_cfg, sum), }); } out From 0727b6d0a4d70941add547880ac18f4c743b873f Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 11:40:58 +0200 Subject: [PATCH 30/52] The adapter's reading sources, and the last accessor goes (#279) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * The adapter's reading sources, and the last accessor goes Closes #275. `Conversions::is_optional` took a spelling and asked the registry for its reading; a type with no cell answered `false` — "not optional" — rather than saying it never entered the pipeline. That is #266's shape, and its symptom is #273's missing `?`. #274, #276 and #278 walked it back to 3 callers, all at places where the adapter OBTAINS a type rather than consumes one. This takes both sources and deletes the accessor, which is the issue's acceptance test. **Sum payload.** `write_sealed_classes` holds the `flat::Variant` beside the `SumSpec` — the split #278 drew in `synth_sum_leaves` — so `build_sealed_class` zips the alternatives in and `sum_payload_kt_type` takes the alternative's own `flat::Field`. `SumSpec` is untouched: it owns the leaf-NAMING convention, which is jnigen's own, and rebuilding it on the model would touch 8 production sites and 5 tests for no gain here. That surfaced a regression the existing tests caught: a fieldless enum declared `sealed_class!` used to panic with a diagnosis, and my first `else { continue }` made it a silent skip. It keeps the diagnosis — sourced from the model's own `Type::Variant`/`Type::Enum` distinction instead of re-deriving it with `enum_shape`, which is now unused here. **Callback args.** `convert_crossing` destructured `TypeKind::Callback { args }` — already `Vec` — and immediately discarded it with `.map(|a| a.origin.syntax.clone())`. That line is gone; `dispatch_fn_input` and `callback_input` take `&[TypeRef]`, and the borrow peel reads `borrow_target()` instead of matching `syn::Type::Reference`. `callback_iface_spec` needed a decision: it is reached only through the `SpecKey`-keyed memo, and `SpecKey` needs `Ord`, which a `TypeRef` cannot give (its `Origin` carries a `SourceLocation`, so two identical readings from different files would compare unequal). So `derive_iface_spec` resolves the key's `TypeKey`s back to readings — a LOOKUP, not a classification, `reading` having been a pure lookup since #267. That changes behaviour deliberately: a missing reading now DEFERS the spec (the "not yet derivable, retried" state `iface_spec` already documents) rather than answering "not optional" and rendering a non-null Kotlin param. The old answer was the #273 bug shape. `a_callback_identity_is_the_same_from_the_reading_or_the_syntax` pins the risk that has no other signal: a key built from `TypeRef::origin.syntax` must equal one built from `extract_fn_trait_args`, or the memo silently emits TWO interfaces for one callback. Written before the migration and verified to fail when the two disagree. Three stale doc references went with the accessor — they named `Conversions::{optional_inner, sequence_elem, is_optional_borrow}`, all deleted in #276. The worst was the spelling-census panic message, which handed a developer that list at the moment they tripped the guard; it now points at the `TypeRef` methods that exist. Boundary ledger 129 -> 127, `emit/callback.rs` off it entirely. Goldens BYTE-IDENTICAL — which is also what says the `SpecKey` identity did not split. 549 lib tests, `--all --all-features` 14/14, clippy, fmt. covertest 48/48 on the JVM. * jnigen: pin the Box<&T> callback-arg refusal with a regression test Review on #279 flagged that swapping `syn::Type::Reference` for `borrow_target()` in `callback_input`'s clone fallback accepts transparent wrappers, so `Box<&T>` would clone to `Box<&T>` against a `T` converter. Measured rather than assumed. The refusal that holds is one level out: `output_wrapper_shape`'s borrowed-opaque arm matches `syn::Type::Reference` on `produced` structurally, so `Box<&T>` (a `Type::Path`) gets no whole-value output converter — and a callback arg's is a required type. Neutralising that arm reproduces the reviewer's bug exactly: Box::new(move |__cb_arg0: Box<&myflat::ZThing>| { let __cb0_enc = ZThing_to_jlong_11822692(&mut env, __cb_arg0)?; `a_wrapped_borrow_callback_arg_declines` pins it end-to-end: plain `&T` resolves, `Box<&T>` refuses, and the test fails on that emitted clone when the arm is made spelling-blind. A local re-check inside `callback_input` was tried and dropped: it left the refusal text byte-identical on `Box<&String>` and `Box<&ZThing>`, and cost a `boundary.ledger` entry for a spelling classification that never fires — against #229's direction. The comment there now records the measurement and names the arm that carries the invariant. Verified: 550 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `cargo clean -p`, covertest-kotlin 48/48. --- prebindgen/src/api/core/flat/boundary.ledger | 5 +- prebindgen/src/api/core/registry/view.rs | 20 --- prebindgen/src/api/core/types_util.rs | 3 +- .../src/api/lang/jnigen/jni/emit/callback.rs | 63 ++++++-- prebindgen/src/api/lang/jnigen/jni/iface.rs | 45 ++++-- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 86 +++++----- .../api/lang/jnigen/jni/tests/callbacks.rs | 153 ++++++++++++++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 8 +- prebindgen/src/api/lang/jnigen/mod.rs | 16 +- 9 files changed, 293 insertions(+), 106 deletions(-) diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 391cb46f..83b31b1b 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -52,7 +52,6 @@ 5 api/lang/cbindgen/mod.rs 6 api/lang/cbindgen/trait_impl.rs 4 api/lang/jnigen/jni/builder.rs -1 api/lang/jnigen/jni/emit/callback.rs 4 api/lang/jnigen/jni/emit/convert.rs 2 api/lang/jnigen/jni/emit/delivery.rs 10 api/lang/jnigen/jni/emit/flat_input.rs @@ -60,7 +59,7 @@ 2 api/lang/jnigen/jni/emit/vec_build.rs 11 api/lang/jnigen/jni/emit/wrapper.rs 3 api/lang/jnigen/jni/fold.rs -5 api/lang/jnigen/jni/iface.rs +4 api/lang/jnigen/jni/iface.rs 2 api/lang/jnigen/jni/kotlin_emit.rs 2 api/lang/jnigen/jni/overloads.rs 1 api/lang/jnigen/jni/prim.rs @@ -71,4 +70,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 129 +# total: 127 diff --git a/prebindgen/src/api/core/registry/view.rs b/prebindgen/src/api/core/registry/view.rs index 90cea6d5..a6f13b2d 100644 --- a/prebindgen/src/api/core/registry/view.rs +++ b/prebindgen/src/api/core/registry/view.rs @@ -37,26 +37,6 @@ pub trait Conversions { /// out. fn reading(&self, ty: &syn::Type) -> Option; - /// Whether `ty` crosses as **optional** — the model's answer, not the - /// spelling's. - /// - /// The question every consumer actually means, and the one they could not - /// ask: they reached for `is_option_type`, which is `path_tail_is(ty, - /// "Option")`. But the model **erases** transparent wrappers — `Box` *is* - /// `T`, and so is `Cow<'_, T>` — so `Box>` is `Optional` and - /// that check answers `false`. Kotlin then lost the `?`, which is not a - /// cosmetic slip: a non-null parameter for an optional value makes the - /// absent case unexpressible (#273). - /// - /// Here rather than at each consumer so the rule has **one** home. A new - /// transparent wrapper — an `Rc`, say — is then a change to - /// [`TRANSPARENT_WRAPPERS`](crate::api::core::flat::TRANSPARENT_WRAPPERS) - /// and nothing else; every site asking this question follows automatically. - fn is_optional(&self, ty: &syn::Type) -> bool { - self.reading(ty) - .is_some_and(|r| r.optional_inner().is_some()) - } - /// The conversion for `ty` in `dir`, if there is one. fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry>; diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index 9e31b0c5..0429feba 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -116,7 +116,8 @@ pub fn first_type_arg(ty: &syn::Type) -> Option { // `is_option_ref` lived here — `option_inner_type(ty)` then a `Type::Reference` // match — and decided how a handle parameter locks. Both halves read the // spelling, so an optional borrow behind an erased wrapper answered `false`. -// `Conversions::is_optional_borrow` asks the model instead (#273). +// The reading answers it instead: `TypeRef::optional_inner().borrow_target()` +// (#273, #275). /// The bare ident of a plain path type (`ZThing` → `ZThing`); `None` for /// references, generics, or multi-shape types. diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs index bf7eaf4f..cae87a24 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs @@ -21,14 +21,14 @@ use crate::api::core::registry::Conversions; /// returned), so they are converted to `__JniErr` and logged via `tracing`. pub(crate) fn callback_input( ext: &Declarations, - args: &[syn::Type], + args: &[crate::api::core::flat::TypeRef], registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { // Human-readable tag for attach/log messages. let name = format!( "Fn({})", args.iter() - .map(|t| TypeKey::from_type(t).to_string()) + .map(|t| t.key().to_string()) .collect::>() .join(", ") ); @@ -46,7 +46,13 @@ pub(crate) fn callback_input( let arg_names: Vec = (0..args.len()) .map(|i| format_ident!("__cb_arg{}", i)) .collect(); - let arg_pat_ty: Vec = args.iter().map(|t| quote!(#t)).collect(); + let arg_pat_ty: Vec = args + .iter() + .map(|t| { + let t = &t.origin.syntax; + quote!(#t) + }) + .collect(); // Per-arg encode preludes binding the typed `run`'s args in declared // order (a decomposed arg contributes one arg per leaf). Each entry of @@ -71,7 +77,7 @@ pub(crate) fn callback_input( // user callback's `run(List)`. Reuses the OUTPUT fold's folder // interface + appender singleton, driven from the trampoline. if let Some(plan) = registry - .callback_arg_plan(&TypeKey::from_type(arg_ty)) + .callback_arg_plan(&arg_ty.key()) .filter(|p| super::render::is_iterable_fold(&p.shape)) { // Every leaf converter must already be resolved (deferral safety). @@ -162,7 +168,7 @@ pub(crate) fn callback_input( // Decomposed arg: deliver the leaves of its type-level canonical // output, exactly like a return delivery. - if let Some(plan) = registry.callback_arg_plan(&TypeKey::from_type(arg_ty)) { + if let Some(plan) = registry.callback_arg_plan(&arg_ty.key()) { // Deferral safety: every leaf converter (and identity-leaf // projection) must already be resolved — return None so the rank // resolver retries this converter later otherwise. A synthesized @@ -193,15 +199,37 @@ pub(crate) fn callback_input( // converter and clone the borrow (the callback only borrows the value). The // `data_class` converter composes the whole object via `fromParts`, so the // Kotlin `run(t: T)` receives a ready-made `T`. - let (cb_val, arg_entry) = match registry.output_entry(arg_ty) { + let (cb_val, arg_entry) = match registry.output_entry(&arg_ty.origin.syntax) { Some(e) => (quote!(#cb_arg), e), - None => match arg_ty { - syn::Type::Reference(r) => { - let core = (*r.elem).clone(); - (quote!((#cb_arg).clone()), registry.output_entry(&core)?) - } - _ => return None, - }, + // A borrow: the callback hands out a reference, and the value is + // cloned for the JVM. + // + // That this is a borrow is the model's answer (`borrow_target`) — + // no spelling is inspected here, which is the point of #229. + // + // `(#cb_arg).clone()` is nonetheless only well-typed for a + // DIRECTLY-spelled `&T`: `#cb_arg` carries the source's spelling, + // so a transparent wrapper — `Box<&T>`, `Ref` all the same — + // would clone to `Box<&T>`, which the `T` converter rejects. That + // case is refused before it reaches here: a callback arg's + // whole-value output converter is a *required* type, and + // `output_wrapper_shape`'s borrowed-opaque arm matches + // `syn::Type::Reference` on `produced` structurally, so `Box<&T>` + // (a `Type::Path`) never gets one. + // + // MEASURED, not assumed — `a_wrapped_borrow_callback_arg_declines` + // fails, on exactly this clone, if that arm is made + // spelling-blind. A local re-check here was tried (#279 review) + // and dropped: it changed no output on `Box<&String>` or + // `Box<&ZThing>`, and cost a `boundary.ledger` entry for a + // classification that never fires. + None => { + let core = arg_ty.borrow_target()?; + ( + quote!((#cb_arg).clone()), + registry.output_entry(&core.origin.syntax)?, + ) + } }; let arg_wire = arg_entry.destination.clone(); let enc_ident = format_ident!("__cb{}_enc", i); @@ -263,7 +291,7 @@ pub(crate) fn callback_input( .projection .as_ref() .is_none_or(|p| p.kind == ProjectionKind::Unsigned64) - && !registry.is_optional(arg_ty) + && arg_ty.optional_inner().is_none() && matches!(jni_field_access(&arg_wire), Some((_, _, false))); if arg_is_prim { let letter = jni_field_access(&arg_wire).unwrap().1; @@ -286,7 +314,12 @@ pub(crate) fn callback_input( // Typed `run` descriptor of the generated callback interface — the SAME // memoized spec (`SpecKey::Callback`) the wrapper surface and the // interface declaration read, so it cannot drift from the jvalues above. - let spec = ext.iface_spec(registry, &SpecKey::callback(args))?; + // The memo key is spellings — `SpecKey` needs `Ord`, which a `TypeRef` + // cannot give. Keyed off each arg's own `origin.syntax`, which + // `a_callback_identity_is_the_same_from_the_reading_or_the_syntax` pins as + // the SAME identity the signature-derived key produces. + let arg_spellings: Vec = args.iter().map(|a| a.origin.syntax.clone()).collect(); + let spec = ext.iface_spec(registry, &SpecKey::callback(&arg_spellings))?; let descr_lit = syn::LitStr::new(&spec.descr, Span::call_site()); // Local-frame capacity: roughly an encoded wire + a wrapped object per // delivered leaf, plus call temporaries. diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index 4945eb24..07889912 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -983,7 +983,23 @@ fn derive_iface_spec( ) -> Option { match key { SpecKey::Callback(arg_keys) => { - let args: Vec = arg_keys.iter().map(TypeKey::to_type).collect(); + // The one deliberate round trip in this file, and the memo forces + // it: `SpecKey` needs `Ord`, so it holds `TypeKey`s — a `TypeRef` + // could not go in one (its `Origin` carries a `SourceLocation`, so + // two identical readings from different files would compare + // unequal), and `derive_iface_spec` is contractually a pure + // function of the key. + // + // A LOOKUP, not a classification: `Registry::reading` has answered + // only from the type table since #267. `None` means the arg has + // not entered the pipeline yet, which is the same "not yet + // derivable" state `iface_spec` already documents and retries — + // so it defers rather than answering "not optional", which is what + // the accessor used to do here. + let args: Vec = arg_keys + .iter() + .map(|k| registry.reading(&k.to_type())) + .collect::>()?; callback_iface_spec(ext, registry, &args) } SpecKey::Builder(d) => builder_iface_spec(ext, registry, d), @@ -1073,7 +1089,7 @@ fn fixed_reassembly( pub(crate) fn callback_iface_spec( ext: &Declarations, registry: &impl Conversions, - cb_args: &[syn::Type], + cb_args: &[crate::api::core::flat::TypeRef], ) -> Option { // Per-arg grouping over the flat raw leaves. A **fixed-builder** (by-value // `data_class`) arg crosses the wire as decoupled leaves but the user @@ -1124,7 +1140,7 @@ pub(crate) fn callback_iface_spec( // arg into the callback's `run` params here. let plan = registry .callback_arg_plans() - .get(&TypeKey::from_type(t)) + .get(&t.key()) .filter(|p| !super::render::is_iterable_fold(&p.shape)); if let Some(plan) = plan { let leaf_names = plan_leaf_names(&plan.leaves); @@ -1133,15 +1149,14 @@ pub(crate) fn callback_iface_spec( } if plan.fixed_builder { any_fixed = true; - let core = match t { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other.clone(), - }; + // Peeled off the reading — `borrow_target` is the model's + // answer to "is this a borrow", not a syn match. + let core = t.borrow_target().unwrap_or(t).origin.syntax.clone(); let fqn = ext.kotlin_fqn(&TypeKey::from_type(&core))?; let (reassemble, imports) = fixed_reassembly(ext, registry, &core, &plan.leaves, &fqn); groups.push(GroupDesc { - name: whole_value_name(t, i), + name: whole_value_name(&t.origin.syntax, i), typed: Some(kt::KtType::cls(fqn.to_string())), reassemble: Some(reassemble), imports, @@ -1214,18 +1229,18 @@ pub(crate) fn callback_iface_spec( // A plan-less opaque-handle arg is delivered as a raw `jlong` and // wrapped + closed Kotlin-side (Phase 3 — no Rust `new_object`). let owned_handle = registry - .output_entry(t) + .output_entry(&t.origin.syntax) .and_then(|e| e.metadata.projection.as_ref()) .map(|p| p.kind == ProjectionKind::Handle) .unwrap_or(false); leaf_tys.push(LeafDesc::Whole { - name: whole_value_name(t, i), - ty: t.clone(), - nullable: registry.is_optional(t), + name: whole_value_name(&t.origin.syntax, i), + ty: t.origin.syntax.clone(), + nullable: t.optional_inner().is_some(), owned_handle, }); groups.push(GroupDesc { - name: whole_value_name(t, i), + name: whole_value_name(&t.origin.syntax, i), typed: None, reassemble: None, imports: Vec::new(), @@ -1282,14 +1297,14 @@ pub(crate) fn callback_iface_spec( "{}Callback", cb_args .iter() - .map(subject_short) + .map(|t| subject_short(&t.origin.syntax)) .collect::>() .join("") ) }; let package = cb_args .first() - .map(|t| subject_package(ext, t)) + .map(|t| subject_package(ext, &t.origin.syntax)) .unwrap_or_else(|| ext.package.clone()); Some(IfaceSpec { typed_groups, diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 5c72dc75..584c50a8 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -534,7 +534,7 @@ impl Declarations { &self, registry: &Registry, ) -> Result, WriteKotlinError> { - use crate::api::core::types_util::{enum_shape, EnumShape, SumSpec}; + use crate::api::core::types_util::SumSpec; let mut written = Vec::new(); // Deterministic order by canonical Rust type-key. @@ -552,11 +552,17 @@ impl Declarations { let Some(ident) = bare_path_ident(&ty) else { continue; }; - let Some(item_enum) = registry.flat().enum_item(&ident) else { - continue; - }; + // The sum as the MODEL holds it: its alternatives' payloads are + // `TypeRef`s, so the Kotlin type of one asks nothing and cannot be + // asked about a type the model never saw. + // + // A FIELDLESS enum is `Type::Enum`, not `Type::Variant` — the model + // already draws the distinction this arm used to re-derive with + // `enum_shape`. It is a declaration error rather than a skip, so it + // keeps its diagnosis; only the source of the answer changed. + let declared = registry.flat().declared_type(&ident); assert!( - enum_shape(item_enum) == EnumShape::Sum, + matches!(declared, Some(crate::api::core::flat::Type::Variant(_))), "`{}` has no payload variants: declare it with `enum_class!({})`, not \ `sealed_class!({})` — a fieldless enum crosses as a bare discriminant and \ needs no sealed hierarchy", @@ -564,8 +570,11 @@ impl Declarations { ident, ident ); + let Some(crate::api::core::flat::Type::Variant(sum)) = declared else { + unreachable!("asserted just above") + }; - let spec = SumSpec::from_item_enum(item_enum); + let spec = SumSpec::from_item_enum(&sum.origin.syntax); // Every declared `.variant(...)` must name a real variant — // a typo would otherwise silently do nothing. for declared in sum_cfg.variant_names.keys() { @@ -580,8 +589,7 @@ impl Declarations { Some((p, c)) => (p.to_string(), c.to_string()), None => (String::new(), kotlin_fqn.clone()), }; - let mut class = - self.build_sealed_class(registry, &class_name, item_enum, &spec, sum_cfg); + let mut class = self.build_sealed_class(registry, &class_name, sum, &spec, sum_cfg); let mut file = kt::KtFile::new(package); if let Some(iface) = self.apply_class_interface(key, &mut class, &class_name, &[], Vec::new(), true) @@ -602,10 +610,14 @@ impl Declarations { &self, registry: &Registry, class_name: &str, - item_enum: &syn::ItemEnum, + sum: &crate::api::core::flat::Variant, spec: &crate::api::core::types_util::SumSpec, sum_cfg: &SumConfig, ) -> KtClass { + // `SumSpec` owns the leaf-NAMING convention, which is jnigen's own; the + // payload TYPES come from the element beside it. Same split #278 drew + // in `synth_sum_leaves`. + let item_enum = &sum.origin.syntax; let framework_line = format!( "JVM-side surface for the native Rust `{}` sum: exactly one alternative is live.", item_enum.ident @@ -619,7 +631,7 @@ impl Declarations { .kdoc(kdoc); // Nested variant classes, in declaration (tag) order. - for (variant, item_variant) in spec.variants.iter().zip(&item_enum.variants) { + for (variant, alt) in spec.variants.iter().zip(&sum.alternatives) { let vname = self.sum_variant_class_name(sum_cfg, &variant.ident); let mut vclass = if variant.is_unit() { KtClass::new(ClassKind::DataObject, &vname) @@ -628,19 +640,15 @@ impl Declarations { } .vis(Vis::Public) .supertype(KtType::cls(class_name), None); - if let Some(doc) = crate::api::lang::jnigen::util::doc_string(&item_variant.attrs) { + if let Some(doc) = crate::api::lang::jnigen::util::doc_string(&alt.origin.syntax.attrs) + { vclass = vclass.kdoc(doc); } let mut vprops: Vec<(String, KtType)> = Vec::new(); - for (field, item_field) in variant.fields.iter().zip(item_variant.fields.iter()) { + for (field, alt_field) in variant.fields.iter().zip(alt.fields.iter()) { let prop = sum_field_property_name(field); - let ty = self.sum_payload_kt_type( - registry, - item_enum, - &variant.ident, - &prop, - item_field, - ); + let ty = + self.sum_payload_kt_type(registry, &sum.name, &variant.ident, &prop, alt_field); vprops.push((prop.clone(), ty.clone())); vclass = vclass.ctor_param(KtCtorParam::new(&prop, ty).val().vis(Vis::Public)); } @@ -663,17 +671,12 @@ impl Declarations { .annotation("JvmStatic") .param(KtParam::new("tag", KtType::int())) .returns(KtType::cls(class_name)); - for (variant, item_variant) in spec.variants.iter().zip(&item_enum.variants) { + for (variant, alt) in spec.variants.iter().zip(&sum.alternatives) { let vname = self.sum_variant_class_name(sum_cfg, &variant.ident); - for (field, item_field) in variant.fields.iter().zip(item_variant.fields.iter()) { + for (field, alt_field) in variant.fields.iter().zip(alt.fields.iter()) { let prop = sum_field_property_name(field); - let ty = self.sum_payload_kt_type( - registry, - item_enum, - &variant.ident, - &prop, - item_field, - ); + let ty = + self.sum_payload_kt_type(registry, &sum.name, &variant.ident, &prop, alt_field); factory = factory.param(KtParam::new(sum_slot_name(&vname, &prop), ty)); } } @@ -780,24 +783,23 @@ impl Declarations { fn sum_payload_kt_type( &self, registry: &Registry, - item_enum: &syn::ItemEnum, + sum_name: &syn::Ident, variant: &syn::Ident, prop: &str, - field: &syn::Field, + field: &crate::api::core::flat::Field, ) -> KtType { - let where_ = || { - format!( - "sealed_class!({}) payload `{variant}.{prop}`", - item_enum.ident - ) - }; - let out = registry.output_entry(&field.ty).unwrap_or_else(|| { + // The field's own reading: the nullability question below is answered + // from `kind`, so a wrapped spelling answers as the bare one does and + // nothing is looked up (#275). + let field_ty = &field.ty.origin.syntax; + let where_ = || format!("sealed_class!({}) payload `{variant}.{prop}`", sum_name); + let out = registry.output_entry(field_ty).unwrap_or_else(|| { panic!( "{}: `{}` has no resolved OUTPUT converter, so the Kotlin surface for it \ cannot be derived — register converters for the payload type before \ declaring the sealed class", where_(), - field.ty.to_token_stream(), + field_ty.to_token_stream(), ) }); @@ -816,7 +818,7 @@ impl Declarations { panic!( "{}: `{}` has no Kotlin type mapping on its output converter", where_(), - field.ty.to_token_stream(), + field_ty.to_token_stream(), ) }); // The input side must agree on WHICH TYPE the property is — Kotlin @@ -831,7 +833,7 @@ impl Declarations { // boxed value, a present flag, a niche) rather than in the type name. // Comparing the rendered types would reject that legitimate shape — // which is what an `Option` payload does. - if let Some(inp) = registry.input_entry(&field.ty) { + if let Some(inp) = registry.input_entry(field_ty) { if let (Some(in_ty), (Some(a), Some(b))) = ( inp.metadata.kotlin_name.clone(), ( @@ -849,7 +851,7 @@ impl Declarations { type (`{}` in, `{}` out) — a sealed class's properties are read by both \ directions, so they must map to one type", where_(), - field.ty.to_token_stream(), + field_ty.to_token_stream(), in_ty, ty, ); @@ -860,7 +862,7 @@ impl Declarations { // field — the Kotlin type must match that slot. Read from the same // entry the type came from. let primitive_wire = crate::api::lang::jnigen::jni::is_jni_primitive(&out.destination); - if registry.is_optional(&field.ty) && !primitive_wire { + if field.ty.optional_inner().is_some() && !primitive_wire { ty.nullable() } else { ty diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs index e9ffd367..faa05c50 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs @@ -578,3 +578,156 @@ fn fn_plan_memo_shares_one_derivation() { assert_eq!(a.native_symbol, fresh.native_symbol); assert_eq!(a.jni_method, fresh.jni_method); } + +/// A callback identity is the same whether its args come from the **reading** +/// or from the signature's syntax. +/// +/// `SpecKey::Callback` is a memo key, so it holds `TypeKey`s — a `TypeRef` +/// could not go in it (`Ord` is required, and an `Origin` carries a +/// `SourceLocation`, so two identical readings from different files would +/// compare unequal). That means the args reach `SpecKey::callback` as +/// spellings, and #275's last part changes *which* spelling: from +/// `extract_fn_trait_args(&pt.ty)` to each arg `TypeRef`'s `origin.syntax`. +/// +/// If those two disagreed, the memo would split one interface identity into +/// two — an extra generated `fun interface` and a descriptor mismatch. Nothing +/// else would fail: not a panic, not an unresolved type, just a duplicate. So +/// it is pinned here rather than trusted. +#[test] +fn a_callback_identity_is_the_same_from_the_reading_or_the_syntax() { + use crate::SourceLocation; + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sub(cb: impl Fn(ZThing) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + )]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZThing)) + .fun(crate::fun!(z_sub)), + ); + let gen = jni.build_with(registry).expect("resolve"); + let registry = gen.registry(); + + let f = registry.flat().function("z_sub").expect("the declared fn"); + let cb = f + .params + .iter() + .find_map(|p| match &p.ty.kind { + crate::api::core::flat::TypeKind::Callback { args } => Some((p, args)), + _ => None, + }) + .expect("z_sub takes a callback"); + let (param, arg_readings) = cb; + + // The two routes to the same identity. + let from_reading = SpecKey::callback( + &arg_readings + .iter() + .map(|a| a.origin.syntax.clone()) + .collect::>(), + ); + let from_syntax = SpecKey::callback( + &crate::api::core::registry::extract_fn_trait_args(¶m.ty.origin.syntax) + .expect("the param is an impl Fn"), + ); + + assert_eq!( + from_reading, from_syntax, + "a callback keyed off its readings must be the SAME memo identity as one \ + keyed off the signature's syntax — otherwise the memo silently emits two \ + interfaces for one callback" + ); +} + +/// A callback argument spelled as a **wrapped** borrow — `Box<&T>` — is refused, +/// rather than generating a trampoline the consumer cannot compile. +/// +/// `Box` is transparent, so `Box<&T>`'s kind is `Ref` exactly as `&T`'s is: the +/// model answers "borrow" for both, and that answer is correct. What differs is +/// the *spelling*, and the generated trampoline is written in the spelling. Neutralise +/// the guard — pass the canonical `&T` as `produced` at `selector.rs`'s borrow arm +/// instead of the crossing's own `syntax` — and this test fails on emitted code that +/// hands a `Box<&ZThing>` to a `ZThing_to_jlong(v: &myflat::ZThing)`: +/// +/// ```ignore +/// Box::new(move |__cb_arg0: Box<&myflat::ZThing>| { +/// let __cb0_enc = ZThing_to_jlong_11822692(&mut env, __cb_arg0)?; +/// ``` +/// +/// The guard that holds is [`Declarations::output_wrapper_shape`]'s borrowed-opaque +/// arm, which matches `syn::Type::Reference` on `produced` structurally: `Box<&T>` +/// is a `Type::Path`, so it gets no whole-value output converter, and a callback +/// arg's is a required type. Same `kind`-classifies / spelling-decides split as +/// #272's `decoded_vec_satisfies` and `is_unsized_spelling`. +/// +/// A local re-check inside `emit/callback.rs` was tried and dropped (#279 review): +/// it changed no output, and cost a `boundary.ledger` entry for a spelling +/// classification that never fires. This test is the protection instead. +#[test] +fn a_wrapped_borrow_callback_arg_declines() { + use crate::SourceLocation; + let loc = myflat_loc(); + let build = |argty: syn::Type| -> Result { + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct ZThing { + pub v: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sub(cb: impl Fn(#argty) + Send + Sync + 'static) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZThing)) + .fun(crate::fun!(z_sub)), + ); + let dir = unique_test_dir("jnigen_wrapped_cb"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + match jni.build_with(registry) { + Ok(g) => Ok(std::fs::read_to_string( + g.write_rust(dir.join("g.rs")).expect("write_rust"), + ) + .expect("read rust")), + Err(e) => Err(format!("{e}")), + } + }; + + // The canonical borrow still resolves and still clones through the core. + let plain = build(syn::parse_quote!(&ZThing)).expect("a plain borrow resolves"); + assert!( + plain.contains("__cb_arg0: &myflat::ZThing"), + "the trampoline takes the borrow as written:\n{plain}" + ); + + // Wrapped, the clone is inexpressible, so nothing claims it. + let err = build(syn::parse_quote!(Box<&ZThing>)) + .expect_err("a wrapped borrow callback arg must not resolve"); + assert!( + err.contains("could not be resolved"), + "the refusal names the type: {err}" + ); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 55e81012..05f1d338 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -1191,8 +1191,7 @@ impl Declarations { let crate::api::core::flat::TypeKind::Callback { args } = &reading.kind else { return None; }; - let args: Vec = args.iter().map(|a| a.origin.syntax.clone()).collect(); - self.dispatch_fn_input(&args, built) + self.dispatch_fn_input(args, built) }), Direction::Output => self.select_output_type(&reading, built), } @@ -1391,10 +1390,11 @@ impl Declarations { impl Declarations { fn dispatch_fn_input( &self, - args: &[syn::Type], + args: &[crate::api::core::flat::TypeRef], registry: &impl Conversions, ) -> Option> { - let outer_ty = build_fn_type(args); + let spellings: Vec = args.iter().map(|a| a.origin.syntax.clone()).collect(); + let outer_ty = build_fn_type(&spellings); let (wire, body) = callback_input(self, args, registry)?; let niches = default_niches_for_wire(&wire); // `impl Fn(...)` crosses the extern tier as the erased lambda object diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index dfb51745..8fa56307 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -68,9 +68,13 @@ mod spelling_census { //! `Box>` **parameter** rendered non-null while the //! identical-meaning `Option` rendered `String?` — and a non-null //! parameter for an optional value makes the absent case unexpressible. - //! `Conversions::{is_optional, optional_inner, sequence_elem, - //! is_optional_borrow}` ask the model, and every site with a registry in - //! scope should use those. + //! The fix is to **carry the reading** rather than the spelling: a + //! `TypeRef` answers `optional_inner()` / `sequence_elem()` / + //! `borrow_target()` itself, infallibly, because holding one is proof the + //! model classified the type. `Conversions` briefly grew accessors that + //! took a spelling and looked the reading up; they are gone (#275), because + //! a type with no cell answered "no layer" instead of saying it had never + //! entered the pipeline. //! //! ## What this is for //! @@ -293,9 +297,9 @@ mod spelling_census { These helpers read a type's layers off its SPELLING, which the model \ erases wrappers from — see this module's docs. A count going DOWN is \ the goal: drop the row (or lower it) in the same commit. A count going \ - UP needs a reason in review: prefer `Conversions::{{is_optional, \ - optional_inner, sequence_elem, is_optional_borrow}}`, which ask the \ - model, wherever a registry is in scope.", + UP needs a reason in review: take the `TypeRef` instead and ask it \ + directly (`optional_inner`, `sequence_elem`, `borrow_target`), which \ + cannot miss.", drift.join("\n"), ); } From 08f854d800cad7417b43c21fa115c8c4fbad79c6 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 14:23:15 +0200 Subject: [PATCH 31/52] Seal `TypeRef`: only the model may mint one (#280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * flat: seal TypeRef — only the model may mint one `TypeRef` was `pub struct { pub kind, pub origin }`, re-exported at `prebindgen::core::flat`, with four public composers from #278. Anyone could assemble one, and nothing checked that `kind` agreed with `origin.syntax`, so holding a `TypeRef` proved nothing about where it came from. The invariant it now carries: Every TypeRef was classified by the model. Flat classified it from source syntax, or the registry composed it by layering over something already classified. Nothing above the model can mint one. Fields become `pub(super)`, composers `pub(crate)`, and reads go through `kind()` / `syntax()` / `location()`. There is no cheaper version: a public field IS a constructor, so restricting only the composers would block nothing. The invariant is unconditional — no phase, no lifetime, no direction — which is what makes it hold for a STORED value. That was the requirement: a `TypeRef` lives in `UnfoldLeaf::out_ty` and `FoldLeaf::ty`, inside plans the registry itself stores, so any borrow-carrying token would make the registry self-referential. It deliberately does NOT claim the converters exist. That is false by design for stored readings: `unrequire_output` exists precisely to leave a cell whose converter cannot resolve (a `Vec` delivered element-by-element — "a jlong wire is not JObject-shaped"), and a `SumTag` leaf never has one. So converter existence stays a lookup answering `Option`, and the relation is 0..2, not 1-to-1. Two compile-fail doctests are the acceptance test, each verified to fail on privacy specifically: E0451 for the struct literal, E0624 for the composer. Mechanical otherwise: 172 read sites migrated by walking rustc's own E0616 spans rather than by pattern-matching text, so no site was missed and none was guessed. Two `&`-artifacts of that rename would have compiled while cloning a reference instead of a value (`suspicious_double_ref_op`); clippy caught both. Also fixes 6 doc links this would have broken, and 2 that were already broken. Verified: 550 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `cargo clean -p`, covertest-kotlin 48/48, rustdoc no new errors (37 -> 35). * jnigen/core: drop the remaining double borrows the rename left Review on #280 found 12 sites where `&x.y.syntax()` / `&x.y.kind()` produces a `&&syn::Type` / `&&TypeKind` that only compiles through deref coercion and match ergonomics. My own cleanup missed them: the sweep matched a SINGLE identifier before the accessor, so `&reading.syntax()` was fixed while `&field.ty.syntax()` and `&c.subject.kind()` were not. Same blind-spot shape as #271's census — a pattern written for one spelling of the thing it was looking for. Measured, because I had cited clippy as the guard here: clippy does NOT catch this class. Reintroducing one double borrow leaves `clippy --all-targets --all-features -- --deny warnings` at exit 0 with zero warnings. The "clippy clean" line in #280 was true and was not evidence for this. The two `&element.syntax()` in `flat/tests/roundtrip.rs` are deliberately kept: `Element::syntax()` returns an OWNED `syn::Item`, so the borrow is real. That is why this was not a blind regex sweep. Verified: 550 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p`, covertest-kotlin 48/48. * flat: enforce the invariant at api::core, not just at the crate edge P1 review on #280 is correct: the doc claimed "nothing above the model can mint one" while every composer and `Flat::classify` were `pub(crate)`, so any in-crate adapter could still mint — and the tree already did, at `jnigen/emit/sum_out.rs:71`. The compile-fail tests proved only the out-of-crate boundary. The claim was false at the commit that made it. Enforced rather than softened. Four visibilities now draw the boundary at `api::core`: borrowed / optional / scalar pub(in crate::api::core) named pub(super) -- flat alone Flat::classify pub(in crate::api::core) the kind / origin fields pub(super) -- unchanged The one in-crate mint is gone rather than documented: the `SumTag` selector needs a type the model already declares, so the DECLARATION now answers — `flat::Variant::type_ref()` — instead of an emitter composing a reading from an ident. That is also the better model: a consumer holding the element no longer has to mint a reading and hope it matches what the model would have said. Measured, not asserted: an `api::lang` adapter naming all four routes now fails with four `E0624`s. The doctests are relabelled to say what they actually prove (the crate edge, E0451 + E0624) and to point at the visibility table for the stronger claim, which no doctest can reach inside the crate to test. This does NOT resolve #281 — composition still lives in `expand`/`unfold` rather than behind a registry API, and a composed reading is still discarded and re-derived by `ensure_entry`. It removes the adapter-side hole only. Verified: 550 lib tests, 21 doctests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p`, covertest-kotlin 48/48, rustdoc 37 -> 35 errors (no new). * flat: Variant carries its own reading instead of composing one Second P1 on #280 is correct, and it is a hole my own fix for the FIRST P1 opened. `Variant::type_ref()` composed `TypeRef::named(&self.name)`, and `Variant` has public fields with a public `Origin::new`, so a consumer could assemble a `Variant` named `String` and get `Named` over the spelling `String` — which the model reads as `Str`. Exactly the kind/syntax disagreement this PR seals, reachable from OUTSIDE the crate, and invisible to both compile-fail doctests because assembling the element is not minting the type. Reproduced before fixing, out-of-crate against the built rlib: kind = Named { id: TypeId { name: "String" } } syntax = String The parser now takes the reading and `Variant` stores it; `type_ref()` returns it. STORING is what closes it: whatever a caller does with the other fields, the reading is the one the model made, and no caller can mint a different one to put in its place. The field is `pub(super)` as a second line — a `Variant` cannot be assembled outside `flat` at all, so `name` and `reading` cannot be paired inconsistently with each other either. Both halves verified: the out-of-crate forge now fails to compile, and an `api::lang` attempt fails `E0451`. The new compile-fail doctest is documented for exactly what it pins — "a consumer cannot assemble a `Variant`" — and no more. Measured: it still passes with the field made `pub`, as `E0063` rather than `E0451`, because a consumer cannot produce a `TypeRef` to supply either way. The visibility is the check that discriminates, and the compiler runs it every build. Verified: 550 lib tests, 22 doctests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p`, covertest-kotlin 48/48, rustdoc 37 -> 35 errors (no new). --- prebindgen/src/api/core/expand.rs | 25 ++-- prebindgen/src/api/core/expand/tests.rs | 93 +++------------ prebindgen/src/api/core/flat/element.rs | 63 +++++++++++ prebindgen/src/api/core/flat/mod.rs | 6 +- prebindgen/src/api/core/flat/ty.rs | 107 ++++++++++++++++-- prebindgen/src/api/core/registry/order.rs | 2 +- prebindgen/src/api/core/registry/scan.rs | 8 +- prebindgen/src/api/core/registry/tests.rs | 14 +-- prebindgen/src/api/core/resolve.rs | 4 +- prebindgen/src/api/core/unfold.rs | 71 ++++++------ prebindgen/src/api/core/unfold/tests.rs | 77 ++----------- prebindgen/src/api/lang/cbindgen/emit.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/builder.rs | 14 +-- .../src/api/lang/jnigen/jni/emit/callback.rs | 12 +- .../src/api/lang/jnigen/jni/emit/delivery.rs | 12 +- .../api/lang/jnigen/jni/emit/struct_out.rs | 2 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 20 ++-- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 6 +- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 10 +- prebindgen/src/api/lang/jnigen/jni/iface.rs | 26 ++--- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 8 +- prebindgen/src/api/lang/jnigen/jni/render.rs | 4 +- .../src/api/lang/jnigen/jni/selector.rs | 38 +++---- .../src/api/lang/jnigen/jni/struct_plan.rs | 12 +- .../api/lang/jnigen/jni/tests/callbacks.rs | 6 +- .../api/lang/jnigen/jni/tests/value_form.rs | 2 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 16 +-- 27 files changed, 346 insertions(+), 314 deletions(-) diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index b1a942fb..de0dc21b 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -313,7 +313,7 @@ fn process_expand( )?; for leaf in &plan.leaves { - registry.require_input(&leaf.ty.origin.syntax); + registry.require_input(leaf.ty.syntax()); } registry .expansion_plans @@ -365,8 +365,8 @@ fn ctor_signature(registry: &Registry, func: &syn::Ident) -> Result (ok.origin.syntax.clone(), true), - None => (f.ret.origin.syntax.clone(), false), + Some((ok, _)) => (ok.syntax().clone(), true), + None => (f.ret.syntax().clone(), false), }; Ok(CtorSig { params, @@ -429,7 +429,7 @@ fn build_plan( )?; visited.remove(&target.key()); return Ok(FoldPlan { - target: target.origin.syntax.clone(), + target: target.syntax().clone(), by_ref, shape: FoldShape::Optional((), Box::new(FoldShape::Base)), leaves, @@ -439,7 +439,7 @@ fn build_plan( }); }; let sig = ctor_signature(registry, func)?; - check_target(func, &sig.target, &target.origin.syntax)?; + check_target(func, &sig.target, target.syntax())?; if sig.params.len() == 1 { let (_pn, pty) = &sig.params[0]; leaves.push(FoldLeaf { @@ -447,7 +447,7 @@ fn build_plan( ty: pty.optional(), }); return Ok(FoldPlan { - target: target.origin.syntax.clone(), + target: target.syntax().clone(), by_ref, shape: FoldShape::Optional((), Box::new(FoldShape::Base)), leaves, @@ -491,7 +491,7 @@ fn build_plan( inputs.push(arg); } return Ok(FoldPlan { - target: target.origin.syntax.clone(), + target: target.syntax().clone(), by_ref, shape: FoldShape::Optional((), Box::new(FoldShape::Base)), leaves, @@ -523,7 +523,7 @@ fn build_plan( )?; visited.remove(&target.key()); Ok(FoldPlan { - target: target.origin.syntax.clone(), + target: target.syntax().clone(), by_ref, shape: FoldShape::Base, leaves, @@ -553,7 +553,7 @@ fn build_core( if let [Variant::Ctor(func)] = variants { // Single constructor — no selector; args passed directly (not Option-wrapped). let sig = ctor_signature(registry, func)?; - check_target(func, &sig.target, &target.origin.syntax)?; + check_target(func, &sig.target, target.syntax())?; let np = sig.params.len(); let mut args = Vec::new(); for (pname, pty) in &sig.params { @@ -588,7 +588,7 @@ fn build_core( match v { Variant::Ctor(func) => { let sig = ctor_signature(registry, func)?; - check_target(func, &sig.target, &target.origin.syntax)?; + check_target(func, &sig.target, target.syntax())?; let np = sig.params.len(); let mut args = Vec::new(); for (pi, (_pname, pty)) in sig.params.iter().enumerate() { @@ -689,7 +689,7 @@ fn build_arg( )?; visited.remove(&key); Ok(FoldArg::Build(Box::new(FoldBuild { - target: bare.origin.syntax.clone(), + target: bare.syntax().clone(), by_ref: pby_ref, selector, variants: vars, @@ -1085,8 +1085,7 @@ fn constructed_value(reading: &crate::api::core::flat::TypeRef) -> syn::Type { after_opt .borrow_target() .unwrap_or(after_opt) - .origin - .syntax + .syntax() .clone() } diff --git a/prebindgen/src/api/core/expand/tests.rs b/prebindgen/src/api/core/expand/tests.rs index f4292514..361658ad 100644 --- a/prebindgen/src/api/core/expand/tests.rs +++ b/prebindgen/src/api/core/expand/tests.rs @@ -51,12 +51,7 @@ fn single_constructor_plan_and_fold() { assert_eq!(plan.leaves.len(), 1); assert_eq!(plan.leaves[0].name.to_string(), "a"); assert_eq!( - plan.leaves[0] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].ty.syntax().to_token_stream().to_string(), "String" ); @@ -106,31 +101,16 @@ fn constructor_plan_and_fold() { // selector + try_from(String) + identity(ZKeyExpr) = 3 leaves assert_eq!(plan.leaves.len(), 3); assert_eq!( - plan.leaves[0] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].ty.syntax().to_token_stream().to_string(), "i32" ); assert_eq!( - plan.leaves[1] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[1].ty.syntax().to_token_stream().to_string(), "Option < String >" ); // `&ZKeyExpr` consumer ⇒ borrowed identity leaf (clone-preserving). assert_eq!( - plan.leaves[2] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[2].ty.syntax().to_token_stream().to_string(), "Option < & ZKeyExpr >" ); assert_eq!(plan.variants.len(), 2); @@ -186,12 +166,7 @@ fn optional_byvalue_single_ctor() { assert_eq!(plan.leaves.len(), 1); // nullable leaf wrapping the ctor param assert_eq!( - plan.leaves[0] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].ty.syntax().to_token_stream().to_string(), "Option < Vec < u8 > >" ); @@ -242,12 +217,7 @@ fn optional_byref_single_ctor() { assert!(plan.produces_option()); assert!(plan.by_ref, "Option<&T> ⇒ by_ref"); assert_eq!( - plan.leaves[0] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].ty.syntax().to_token_stream().to_string(), "Option < String >" ); assert_eq!( @@ -301,32 +271,17 @@ fn optional_byref_multi_arg_ctor() { assert_eq!(plan.leaves.len(), 3); assert_eq!(plan.leaves[0].name.to_string(), "encoding_present"); assert_eq!( - plan.leaves[0] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].ty.syntax().to_token_stream().to_string(), "bool" ); assert_eq!(plan.leaves[1].name.to_string(), "encoding_id"); assert_eq!( - plan.leaves[1] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[1].ty.syntax().to_token_stream().to_string(), "i32" ); assert_eq!(plan.leaves[2].name.to_string(), "encoding_schema"); assert_eq!( - plan.leaves[2] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[2].ty.syntax().to_token_stream().to_string(), "Option < String >" ); @@ -400,40 +355,20 @@ fn optional_combined_selector_encodes_absence() { assert_eq!(plan.leaves.len(), 4); assert_eq!(plan.leaves[0].name.to_string(), "encoding_sel"); assert_eq!( - plan.leaves[0] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].ty.syntax().to_token_stream().to_string(), "i32" ); assert_eq!( - plan.leaves[1] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[1].ty.syntax().to_token_stream().to_string(), "Option < i32 >" ); assert_eq!( - plan.leaves[2] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[2].ty.syntax().to_token_stream().to_string(), "Option < String >", "already-Option ctor arg is NOT double-wrapped" ); assert_eq!( - plan.leaves[3] - .ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[3].ty.syntax().to_token_stream().to_string(), "Option < & ZEncoding >" ); assert!( @@ -674,7 +609,7 @@ fn recursive_input_nests_param_constructors() { let leaf_tys: Vec = plan .leaves .iter() - .map(|l| l.ty.origin.syntax.to_token_stream().to_string()) + .map(|l| l.ty.syntax().to_token_stream().to_string()) .collect(); assert!( leaf_tys.iter().any(|t| t.contains("i32")), diff --git a/prebindgen/src/api/core/flat/element.rs b/prebindgen/src/api/core/flat/element.rs index da54ba28..78c26101 100644 --- a/prebindgen/src/api/core/flat/element.rs +++ b/prebindgen/src/api/core/flat/element.rs @@ -228,6 +228,69 @@ pub struct Variant { /// Alternatives in declaration order; `alternatives[i].index == i`. pub alternatives: Vec, pub origin: Origin, + /// This sum **as a type**, taken at parse time — see [`Self::type_ref`]. + /// + /// **Stored, not computed**, and that is what makes the accessor safe: a + /// method composing `TypeRef::named(&self.name)` would answer for whatever + /// name a caller put in the struct, so a `Variant` named `String` would + /// yield `Named` over the spelling `String` — which the model reads as + /// `Str`. A stored reading cannot disagree with the model, because the + /// model is what put it there, and an assembler has no way to mint a + /// different one. + /// + /// `pub(super)` is the second line, not the first: it also stops a + /// `Variant` being assembled at all outside `flat` (`E0451`), so `name` and + /// `reading` cannot be paired inconsistently with *each other*. + pub(super) reading: TypeRef, +} + +impl Variant { + /// A reference to this sum **as a type** — what a consumer needs when it + /// has to name the sum rather than walk it (jnigen's `SumTag` selector, + /// which carries *which* sum it chooses between). + /// + /// The **declaration** answers, so no consumer has to mint a reading from + /// the name and hope it matches what the model would have said. + /// + /// This returns state the parser took, **not** a fresh composition, and the + /// difference is the difference between sealing and appearing to. + /// + /// A version that composed `TypeRef::named(&self.name)` would hand a + /// `Variant` assembled with the name `String` a + /// [`Named`](super::TypeKind::Named) over the spelling `String` — which the + /// model reads as [`Str`](super::TypeKind::Str). That is the `kind`/`syntax` + /// disagreement [`TypeRef`]'s private fields exist to prevent, and it was + /// reachable from outside the crate while being invisible to every doctest + /// there, because assembling the *element* is not minting the *type*. + /// + /// Reading a stored value closes it: whatever a caller does with the other + /// fields, the reading here is the one the model made, and no caller can + /// mint a different one to put in its place. + /// + /// The `Variant` is sealed as well — its `reading` field is `pub(super)` — + /// so the two cannot even be paired inconsistently: + /// + /// ```compile_fail + /// # use prebindgen::core::flat::{Origin, Variant}; + /// let assembled = Variant { + /// name: syn::parse_str("String").unwrap(), + /// alternatives: vec![], + /// origin: Origin::new( + /// syn::parse_str("enum String { A(u8) }").unwrap(), + /// std::rc::Rc::new(Default::default()), + /// ), + /// }; + /// let mismatched = assembled.type_ref(); + /// ``` + /// + /// That doctest pins *"a consumer cannot assemble a `Variant`"* and nothing + /// finer: measured, it still fails with the field made `pub` — as `E0063` + /// (missing field) rather than `E0451` (private field), since a consumer + /// cannot produce a `TypeRef` to supply either way. The visibility itself + /// is the check the compiler runs on every build. + pub fn type_ref(&self) -> &TypeRef { + &self.reading + } } /// One alternative of a [`Variant`]. diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index 54b7fb18..ce64246f 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -687,7 +687,10 @@ impl Flat { /// /// `Err` means the spelling is outside the accepted grammar — a real diagnosis /// about a type the *binding* built, not a cache miss. - pub(crate) fn classify(&self, ty: &syn::Type) -> Result { + pub(in crate::api::core) fn classify( + &self, + ty: &syn::Type, + ) -> Result { if let Some(indexed) = self.type_ref(ty) { return Ok(indexed.clone()); } @@ -1401,6 +1404,7 @@ fn lower_variant( }); } Ok(Variant { + reading: TypeRef::named(&e.ident), name: e.ident.clone(), alternatives, origin: Origin::new(e.clone(), Rc::clone(at)), diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index e13a364e..597a3b2a 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -26,10 +26,43 @@ use crate::SourceLocation; /// lossless: a lifetime, an elided argument, a `Box` that changes nothing /// outside Rust all survive there at zero modelling cost, so `kind` can stay /// language-neutral and small. +/// +/// # The invariant +/// +/// > **Every `TypeRef` was classified by the model.** [`Flat`](super::Flat) +/// > classified it from source syntax, or `api::core` composed it by layering +/// > over something already classified. Nothing above `api::core` can mint one. +/// +/// **Scoped to what is enforced, not to what is intended.** The boundary is +/// `api::core`, and it is drawn by visibility at four places, each checked by +/// the compiler on every build: +/// +/// | | | +/// |---|---| +/// | the `kind` and `origin` fields | `pub(super)` — a public field **is** a constructor, so restricting only the composers would block nothing | +/// | `borrowed` / `optional` / `scalar` | `pub(in crate::api::core)` | +/// | `named` | `pub(super)` — `flat` alone | +/// | `Flat::classify` | `pub(in crate::api::core)` | +/// +/// So a language adapter under `api::lang` cannot mint a reading at all — not +/// by field, not by composer, not by classifying a spelling of its own. Where +/// one needs a type the model already declares, the **declaration** answers: +/// see [`Variant::type_ref`](super::Variant::type_ref), which is what the +/// `SumTag` selector uses instead of composing a reading from an ident. +/// +/// The invariant is unconditional — no phase, no lifetime, no direction — so it +/// holds for a **stored** value. That is the point: a `TypeRef` lives in +/// `UnfoldLeaf::out_ty` and `FoldLeaf::ty`, inside plans the registry itself +/// stores, so a borrow-carrying token would make the registry self-referential. +/// +/// It deliberately does **not** claim the type's converters exist. That is +/// false by design for stored readings — `unrequire_output` leaves a cell whose +/// converter genuinely cannot resolve, and a `SumTag` leaf never has one — so +/// converter existence stays a lookup that answers `Option`. #[derive(Clone, Debug)] pub struct TypeRef { /// What the type means — the closed, destination-neutral classification. - pub kind: TypeKind, + pub(super) kind: TypeKind, /// The type as generated Rust must spell it — the source's own tokens, /// normalized to the flat namespace the generated crate can name (see /// [`Flat::parse`](super::Flat::parse)) — plus the source they came @@ -38,7 +71,59 @@ pub struct TypeRef { /// The syntax can say strictly more than `kind` does — `Box` is a /// `Str` here — which is the point: what Rust needs and no destination /// language can see lives in the tokens, not in the classification. - pub origin: Origin, + pub(super) origin: Origin, +} + +impl TypeRef { + /// What the type means. **Classify off this**, never off the spelling. + /// + /// The seal, as a compiled assertion. An out-of-crate consumer cannot + /// assemble a reading, because the fields it would have to name are private + /// (`E0451`): + /// + /// ```compile_fail + /// # use prebindgen::core::flat::{TypeKind, TypeRef}; + /// let forged = TypeRef { kind: TypeKind::Unit, origin: todo!() }; + /// ``` + /// + /// …nor through a composer, which is not visible either (`E0624`): + /// + /// ```compile_fail + /// # use prebindgen::core::flat::{ScalarKind, TypeRef}; + /// let forged = TypeRef::scalar(ScalarKind::Bool); + /// ``` + /// + /// These two prove only the **crate** boundary. The stronger claim — that + /// nothing above `api::core` can mint one either — is enforced by the + /// visibilities tabulated on [`TypeRef`], and a doctest cannot reach inside + /// the crate to test it. The compiler checks it on every build instead: an + /// `api::lang` adapter naming any of the four routes fails with `E0624`. + pub fn kind(&self) -> &TypeKind { + &self.kind + } + + /// The tokens generated Rust must spell. **Spell off this**, never off + /// `kind` — the syntax says strictly more, and re-deriving it from the + /// classification is how `Box>` becomes an `E0308`. + pub fn syntax(&self) -> &syn::Type { + &self.origin.syntax + } + + /// Where the type was written, for diagnostics. A composed type is + /// **placeless** — [`SourceLocation::has_position`] gates what is printed. + pub fn location(&self) -> &SourceLocation { + &self.origin.location + } + + /// An [`Origin`] for a node that exists **because of** this type, sharing + /// its location — a synthesized getter built from a return type, say. + /// + /// Deliberately narrower than handing out the origin: it lends provenance + /// without lending the field, so a `TypeRef`'s own `Origin` still cannot be + /// obtained from outside the model. + pub(crate) fn origin_with(&self, syntax: S) -> Origin { + self.origin.with(syntax) + } } impl TypeRef { @@ -160,7 +245,7 @@ impl TypeRef { /// /// Keeps this type's location: the borrow exists *because of* this value, /// so a diagnostic about it should point where the value came from. - pub fn borrowed(&self) -> TypeRef { + pub(in crate::api::core) fn borrowed(&self) -> TypeRef { let inner = &self.origin.syntax; TypeRef { kind: TypeKind::Ref { @@ -173,7 +258,7 @@ impl TypeRef { /// An optional of this type — `Option` from `T`. Location as /// [`Self::borrowed`]. - pub fn optional(&self) -> TypeRef { + pub(in crate::api::core) fn optional(&self) -> TypeRef { let inner = &self.origin.syntax; TypeRef { kind: TypeKind::Optional(Box::new(self.clone())), @@ -188,7 +273,7 @@ impl TypeRef { /// [`Flat::classify`](super::Flat::classify) does exactly this for a /// composed spelling, and `ensure_entry` gives adapter-authored cells the /// same treatment — `has_position` already gates what a diagnostic prints. - pub fn scalar(kind: ScalarKind) -> TypeRef { + pub(in crate::api::core) fn scalar(kind: ScalarKind) -> TypeRef { // The spelling comes from the kind, so the two cannot drift. let ident = syn::Ident::new(kind.as_str(), proc_macro2::Span::call_site()); TypeRef { @@ -203,7 +288,7 @@ impl TypeRef { /// A nominal reference to a declared type, by name. Placeless for the same /// reason as [`Self::scalar`] — this is the binding naming a type, not a /// source mentioning one. - pub fn named(ident: &syn::Ident) -> TypeRef { + pub(super) fn named(ident: &syn::Ident) -> TypeRef { TypeRef { kind: TypeKind::Named { id: TypeId { @@ -221,7 +306,7 @@ impl TypeRef { /// /// The canonical spelling is what a key *is* (#113), and reading it is /// legitimate — but it should be the model's answer rather than every caller - /// reaching into [`origin`](Self::origin) for it, since a caller that reaches + /// reaching into [`syntax`](Self::syntax) for it, since a caller that reaches /// into `origin` to *reason* is the thing this model exists to stop. pub fn key(&self) -> crate::api::core::registry::TypeKey { crate::api::core::registry::TypeKey::from_type(&self.origin.syntax) @@ -285,7 +370,7 @@ impl TypeRef { /// /// A [`Named`](TypeKind::Named)'s generic arguments are **not** among them: /// [`TypeId`] keeps a name and nothing else, so `MyBox` reaches no `Foo` - /// here. The full spelling is in [`Self::origin`] for whoever needs it. + /// here. The full spelling is in [`Self::syntax`] for whoever needs it. pub fn walk(&self) -> Vec<&TypeRef> { let mut out = Vec::new(); self.collect_refs(&mut out); @@ -333,7 +418,7 @@ impl TypeRef { /// One Rust spelling per concept is **not** the rule here — several are. A /// concept earns a variant when a destination language would act on it; a /// spelling that changes nothing outside Rust folds into the concept it carries -/// and survives in [`TypeRef::origin`]: +/// and survives in [`TypeRef::syntax`]: /// /// | Spelling | Kind | Why | /// |---|---|---| @@ -369,7 +454,7 @@ pub enum TypeKind { /// this module has to take a path apart to learn what a type is. The last /// segment's generic arguments live in `args`, and only the *type* /// arguments: a lifetime argument says nothing a destination language can - /// act on. The full spelling is in [`TypeRef::origin`] for whoever re-emits it. + /// act on. The full spelling is in [`TypeRef::syntax`] for whoever re-emits it. Named { id: TypeId }, /// `[T; N]` — a run of `T` whose length is known at compile time. /// @@ -385,7 +470,7 @@ pub enum TypeKind { extent: Box, }, /// A borrow — `&T`, `&mut T`, or `&mut MaybeUninit`. The lifetime is - /// spelling, so it lives in [`TypeRef::origin`] rather than here. + /// spelling, so it lives in [`TypeRef::syntax`] rather than here. /// /// This is the ownership layer for every concept underneath it: `&str` is /// `Ref(Str)`, `&[T]` is `Ref(Sequence)`. A shared-ownership handle diff --git a/prebindgen/src/api/core/registry/order.rs b/prebindgen/src/api/core/registry/order.rs index c474c071..5a31c8d6 100644 --- a/prebindgen/src/api/core/registry/order.rs +++ b/prebindgen/src/api/core/registry/order.rs @@ -106,7 +106,7 @@ impl Registry { for arg in args { if let Some(plan) = self.callback_arg_plans.get(&TypeKey::from_type(&arg)) { for leaf in &plan.leaves { - out.push((Direction::Output, leaf.out_ty.origin.syntax.clone())); + out.push((Direction::Output, leaf.out_ty.syntax().clone())); } } } diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index 2b0a023a..186f028a 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -337,7 +337,7 @@ impl Registry { .map(|c| &c.subject) { let (children, child_dir): (Vec<&crate::api::core::flat::TypeRef>, Direction) = - match &reading.kind { + match reading.kind() { TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => (vec![t], dir), @@ -354,7 +354,7 @@ impl Registry { | TypeKind::Unit => (Vec::new(), dir), }; for child in children { - out.push((child_dir, child.origin.syntax.clone())); + out.push((child_dir, child.syntax().clone())); } } // A declared type's own fields, read off the element rather than off its @@ -371,7 +371,7 @@ impl Registry { if let Some(name) = self .type_table(dir) .get(&TypeKey::from_type(ty)) - .and_then(|c| match &c.subject.kind { + .and_then(|c| match c.subject.kind() { TypeKind::Named { id } => Some(id.name.clone()), _ => None, }) @@ -387,7 +387,7 @@ impl Registry { Some(Type::Enum(_) | Type::Extern(_)) | None => Vec::new(), }; for field in fields { - out.push((dir, field.ty.origin.syntax.clone())); + out.push((dir, field.ty.syntax().clone())); } } out diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index d19e0aad..3b2b6ac6 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -807,16 +807,16 @@ fn a_source_type_cell_carries_the_models_typeref() { let cell = ®.input_types[&key]; assert!(cell.root, "a top-level parameter is a root"); assert!( - matches!(cell.subject.kind, TypeKind::Optional(_)), + matches!(cell.subject.kind(), TypeKind::Optional(_)), "the frontend classified it, so the cell has that classification" ); // One location per cell, and it is the model's — not a copy the scan made. - assert_eq!(&*cell.subject.origin.location, &loc); + assert_eq!(cell.subject.location(), &loc); // The nested position is in the model too, and is not a root. let inner = ®.input_types[&TypeKey::parse("u64").expect("test type")]; assert!(!inner.root); - assert!(matches!(inner.subject.kind, TypeKind::Scalar(_))); + assert!(matches!(inner.subject.kind(), TypeKind::Scalar(_))); } /// `Registry::reading` is a **lookup**. A type with no cell answers `None`, even @@ -892,11 +892,11 @@ fn an_adapter_authored_type_cell_is_classified_but_placeless() { let cell = ®.input_types[&TypeKey::parse("Foreign").expect("test type")]; assert!(cell.root, "the binding asked for it directly"); assert!( - matches!(&cell.subject.kind, TypeKind::Named { id } if id.name == "Foreign"), + matches!(cell.subject.kind(), TypeKind::Named { id } if id.name == "Foreign"), "a declared name is a name, and the grammar can say so" ); assert!( - !cell.subject.origin.location.has_position(), + !cell.subject.location().has_position(), "nothing wrote it, so there is no position to report" ); } @@ -1425,12 +1425,12 @@ fn a_type_only_a_local_fn_writes_still_has_a_reading() { .flat() .type_ref(&syn::parse_quote!(Option)) .expect("a local fn's parameter type is in the model"); - assert!(matches!(read.kind, TypeKind::Optional(_))); + assert!(matches!(read.kind(), TypeKind::Optional(_))); // … and the cell scanned from that parameter carries that same reading, // rather than a second one made at the table. let cell = ®.input_types[&TypeKey::parse("Option").expect("test type")]; - assert!(matches!(cell.subject.kind, TypeKind::Optional(_))); + assert!(matches!(cell.subject.kind(), TypeKind::Optional(_))); } /// A type with no source position must not get an invented one. diff --git a/prebindgen/src/api/core/resolve.rs b/prebindgen/src/api/core/resolve.rs index 61b5d5ce..4127a258 100644 --- a/prebindgen/src/api/core/resolve.rs +++ b/prebindgen/src/api/core/resolve.rs @@ -150,7 +150,7 @@ fn collect_unresolved_descendants( out.push(UnresolvedEntry { key: key.clone(), direction: dir, - location: Some(&*cell.subject.origin.location) + location: Some(cell.subject.location()) .filter(|l| l.has_position()) .cloned(), }); @@ -191,7 +191,7 @@ pub(crate) fn check_complete(registry: &Registry) -> Result<(), ResolveErr entries.push(UnresolvedEntry { key: key.clone(), direction: dir, - location: Some(&*cell.subject.origin.location) + location: Some(cell.subject.location()) .filter(|l| l.has_position()) .cloned(), }); diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index a4273ebd..de48489d 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -312,7 +312,7 @@ pub fn apply( func: ed.func.clone(), declared: TypeKey::from_type(declared).as_str().to_string(), actual: { - let s = &ret.origin.syntax; + let s = ret.syntax(); quote::quote!(#s).to_string() }, }); @@ -405,7 +405,7 @@ pub fn apply( // The callback's argument types, read off the parameter's // classification. `TypeKind::Callback` carries them as `TypeRef`s, so // there is nothing to re-extract from the signature's syntax. - let crate::api::core::flat::TypeKind::Callback { args } = ¶m.ty.kind else { + let crate::api::core::flat::TypeKind::Callback { args } = param.ty.kind() else { continue; }; for arg_ty in args { @@ -420,7 +420,10 @@ pub fn apply( // `Option` / `Vec` / tuple arg is delivered whole. The model // says which, so a wrapper the language sees through — `Box` — // no longer reads as un-nameable. - if !matches!(core_ty.kind, crate::api::core::flat::TypeKind::Named { .. }) { + if !matches!( + core_ty.kind(), + crate::api::core::flat::TypeKind::Named { .. } + ) { continue; } let key = arg_ty.key(); @@ -459,7 +462,7 @@ pub fn apply( continue; } for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty.origin.syntax); + registry.require_output(leaf.out_ty.syntax()); } registry.callback_arg_plans.insert(key, plan); } @@ -640,7 +643,7 @@ fn wire_fixed_returns( } } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty.origin.syntax); + registry.require_output(leaf.out_ty.syntax()); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -678,7 +681,7 @@ fn wire_fixed_callbacks( // The callback's argument types, read off the parameter's // classification. `TypeKind::Callback` carries them as `TypeRef`s, so // there is nothing to re-extract from the signature's syntax. - let crate::api::core::flat::TypeKind::Callback { args } = ¶m.ty.kind else { + let crate::api::core::flat::TypeKind::Callback { args } = param.ty.kind() else { continue; }; for arg_ty in args { @@ -707,7 +710,7 @@ fn wire_fixed_callbacks( continue; } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty.origin.syntax); + registry.require_output(leaf.out_ty.syntax()); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -776,7 +779,7 @@ pub fn apply_leaf_vec_folds( } else { inner_shape }; - registry.require_output(&vec_elem.origin.syntax); + registry.require_output(vec_elem.syntax()); // The fold delivers the return element-by-element, so the // whole `Vec` / `Option>` converter is not needed. // De-require it: for String / scalar elements it still @@ -784,7 +787,7 @@ pub fn apply_leaf_vec_folds( // opaque-handle element it cannot resolve (`jlong` wire isn't // JObject-shaped), and de-requiring keeps that `None` from // being flagged as an unresolved-required error. - registry.unrequire_output(&ret.origin.syntax); + registry.unrequire_output(ret.syntax()); registry .unfold_plans .insert(func.clone(), whole_leaf_fold_plan(vec_elem, shape)); @@ -796,7 +799,7 @@ pub fn apply_leaf_vec_folds( // The callback's argument types, read off the parameter's // classification. `TypeKind::Callback` carries them as `TypeRef`s, so // there is nothing to re-extract from the signature's syntax. - let crate::api::core::flat::TypeKind::Callback { args } = ¶m.ty.kind else { + let crate::api::core::flat::TypeKind::Callback { args } = param.ty.kind() else { continue; }; for arg_ty in args { @@ -811,7 +814,7 @@ pub fn apply_leaf_vec_folds( if registry.callback_arg_plans.contains_key(&key) { continue; } - registry.require_output(&elem.origin.syntax); + registry.require_output(elem.syntax()); let plan = whole_leaf_fold_plan(elem, UnfoldShape::Iterable(Box::new(UnfoldShape::Base))); registry.callback_arg_plans.insert(key, plan); @@ -829,12 +832,12 @@ fn whole_leaf_fold_plan( shape: UnfoldShape, ) -> UnfoldPlan { UnfoldPlan { - source: vec_elem.origin.syntax.clone(), + source: vec_elem.syntax().clone(), decon: None, by_ref: peel_borrow(vec_elem).0, shape, leaves: vec![], - element: Some(vec_elem.origin.syntax.clone()), + element: Some(vec_elem.syntax().clone()), delivery: Delivery::Callback, convert_out_ty: None, fixed_builder: true, @@ -924,9 +927,9 @@ fn peel(ty: &crate::api::core::flat::TypeRef) -> Layered { layer_types: ty .layer_types() .iter() - .map(|t| t.origin.syntax.clone()) + .map(|t| t.syntax().clone()) .collect(), - core: borrowed.unwrap_or(layered).origin.syntax.clone(), + core: borrowed.unwrap_or(layered).syntax().clone(), by_ref: borrowed.is_some(), } } @@ -1021,9 +1024,9 @@ fn process_decl( // recursive registration also required) — same reasoning as // [`apply_leaf_vec_folds`] for the fixed folds. if ed.target == DeconTarget::Output { - registry.unrequire_output(&ret_ty.origin.syntax); + registry.unrequire_output(ret_ty.syntax()); if optional { - registry.unrequire_output(&after_opt.origin.syntax); + registry.unrequire_output(after_opt.syntax()); } } // Element type peeled of a leading `&` (accessors take `&Element`). @@ -1036,7 +1039,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, element)?; let plan = build_plan(acc, registry, ed, by_ref, element, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty.origin.syntax); + registry.require_output(leaf.out_ty.syntax()); } plan } else { @@ -1045,14 +1048,14 @@ fn process_decl( // No declaration is involved (`decon: None`) — the element // crosses whole through its own converter. let by_ref = peel_borrow(inner).0; - registry.require_output(&inner.origin.syntax); + registry.require_output(inner.syntax()); UnfoldPlan { - source: inner.origin.syntax.clone(), + source: inner.syntax().clone(), decon: None, by_ref, shape, leaves: vec![], - element: Some(inner.origin.syntax.clone()), + element: Some(inner.syntax().clone()), delivery: ed.delivery, convert_out_ty: None, fixed_builder: false, @@ -1082,7 +1085,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, source)?; let plan = build_plan(acc, registry, ed, by_ref, source, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(&leaf.out_ty.origin.syntax); + registry.require_output(leaf.out_ty.syntax()); } plan }; @@ -1110,7 +1113,7 @@ fn process_decl( && plan.leaves.len() == 1 && !plan.leaves[0].nullable; let plan = if single_return { - let leaf_ty = plan.leaves[0].out_ty.origin.syntax.clone(); + let leaf_ty = plan.leaves[0].out_ty.syntax().clone(); let cv_ty: syn::Type = if matches!(plan.shape, UnfoldShape::Optional((), _)) { syn::parse_quote!(Option<#leaf_ty>) } else { @@ -1173,11 +1176,11 @@ fn register_decon_spec( // derived from it, never emitted code — so its hoists are discarded. &mut Vec::new(), )?; - require_unique_leaf_names(&source.origin.syntax, &leaves)?; + require_unique_leaf_names(source.syntax(), &leaves)?; registry.decon_plans.insert( decon.clone(), DeconSpec { - source: source.origin.syntax.clone(), + source: source.syntax().clone(), leaves, }, ); @@ -1250,11 +1253,11 @@ fn build_plan( &mut leaves, &mut hoists, )?; - require_unique_leaf_names(&source.origin.syntax, &leaves)?; - require_root_identity_last(by_ref, &source.origin.syntax, &leaves)?; + require_unique_leaf_names(source.syntax(), &leaves)?; + require_root_identity_last(by_ref, source.syntax(), &leaves)?; Ok(UnfoldPlan { - source: source.origin.syntax.clone(), + source: source.syntax().clone(), decon: Some(decon), by_ref, shape, @@ -1389,7 +1392,7 @@ fn flatten( // call, so the whole record shares a single `Call` step and the // emitter can hoist it. let (takes, _ret) = accessor_signature(registry, func)?; - check_takes(func, &takes, &source.origin.syntax)?; + check_takes(func, &takes, source.syntax())?; // The declarator states whether the value is given away; the // signature has to agree, or the emitted call would not compile // in the consumer's crate. Checked rather than inferred so that @@ -1576,7 +1579,7 @@ fn flatten( DeconRecord::Identity | DeconRecord::Fields { .. } => unreachable!(), }; let (takes, ret) = accessor_signature(registry, &func)?; - check_takes(&func, &takes, &source.origin.syntax)?; + check_takes(&func, &takes, source.syntax())?; // Default unwrap: if the return type has its own deconstructor, // splice it (recurse); otherwise the return is one leaf. Peel an // `Option` (value may be absent) + leading `&` to reach the child. @@ -1723,9 +1726,9 @@ fn accessor_signature( .params .first() .ok_or_else(|| UnfoldError::UnknownAccessor(func.clone()))?; - let takes = match &first.ty.kind { - crate::api::core::flat::TypeKind::Ref { inner, .. } => inner.origin.syntax.clone(), - _ => first.ty.origin.syntax.clone(), + let takes = match first.ty.kind() { + crate::api::core::flat::TypeKind::Ref { inner, .. } => inner.syntax().clone(), + _ => first.ty.syntax().clone(), }; Ok((takes, f.ret.clone())) } @@ -1761,7 +1764,7 @@ fn accessor_consumes(registry: &Registry, func: &syn::Ident) -> bool { .flat() .function(&func) .and_then(|f| f.params.first()) - .is_some_and(|p| !matches!(p.ty.kind, crate::api::core::flat::TypeKind::Ref { .. })) + .is_some_and(|p| !matches!(p.ty.kind(), crate::api::core::flat::TypeKind::Ref { .. })) } fn check_takes( diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index f8ae249e..162df0b0 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -112,12 +112,7 @@ fn accessor_optional_primitive() { "z_timestamp_ntp64" ); assert_eq!( - plan.leaves[0] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].out_ty.syntax().to_token_stream().to_string(), "i64" ); assert!( @@ -175,12 +170,7 @@ fn accessor_plan_byref() { assert!(plan.leaves[0].identity); assert!(plan.leaves[0].path.is_empty()); assert_eq!( - plan.leaves[0] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].out_ty.syntax().to_token_stream().to_string(), "& ZKeyExpr" ); // Accessor leaf: out_ty `&str`, path `[z_keyexpr_as_str]`. @@ -191,12 +181,7 @@ fn accessor_plan_byref() { "z_keyexpr_as_str" ); assert_eq!( - plan.leaves[1] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[1].out_ty.syntax().to_token_stream().to_string(), "& str" ); @@ -542,12 +527,7 @@ fn nested_accessor_flatten() { assert_eq!(path(&plan.leaves[2]), "z_sample_payload.z_zbytes_to_bytes"); assert_eq!(path(&plan.leaves[3]), "z_sample_kind"); assert_eq!( - plan.leaves[3] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[3].out_ty.syntax().to_token_stream().to_string(), "SampleKind" ); assert_eq!( @@ -685,12 +665,7 @@ fn reply_product_double_option_flatten() { // Acc leaf keeping its full `Option<…>` return — not a nesting step. assert_eq!(path(&plan.leaves[0]), "z_reply_replier_zid"); assert_eq!( - plan.leaves[0] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].out_ty.syntax().to_token_stream().to_string(), "Option < ZZenohId >" ); assert!(!plan.leaves[0].nullable && !plan.leaves[0].identity); @@ -856,12 +831,7 @@ fn iterable_decomposed_plan() { "z_zenoh_id_to_string" ); assert_eq!( - plan.leaves[0] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].out_ty.syntax().to_token_stream().to_string(), "String" ); // Identity leaf: owned value (`ZZenohId`, not `&ZZenohId`) since the Vec @@ -869,12 +839,7 @@ fn iterable_decomposed_plan() { assert!(plan.leaves[1].identity); assert!(plan.leaves[1].path.is_empty()); assert_eq!( - plan.leaves[1] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[1].out_ty.syntax().to_token_stream().to_string(), "ZZenohId" ); } @@ -1278,12 +1243,7 @@ fn convert_error_decomposes_result_e() { assert_eq!(plan.delivery, Delivery::Callback); assert_eq!(plan.leaves.len(), 1); assert_eq!( - plan.leaves[0] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].out_ty.syntax().to_token_stream().to_string(), "String" ); assert_eq!(plan.source.to_token_stream().to_string(), "ZError"); @@ -1403,12 +1363,7 @@ fn callback_arg_plan_derived() { "z_sample_key_expr" ); assert_eq!( - plan.leaves[0] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[0].out_ty.syntax().to_token_stream().to_string(), "& ZKeyExpr" ); assert_eq!( @@ -1416,12 +1371,7 @@ fn callback_arg_plan_derived() { "z_keyexpr_as_str" ); assert_eq!( - plan.leaves[2] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[2].out_ty.syntax().to_token_stream().to_string(), "SampleKind" ); // Leaf out_tys registered so the resolver builds their converters. @@ -1496,12 +1446,7 @@ fn callback_arg_borrowed_decomposed() { "z_sample_key_expr" ); assert_eq!( - plan.leaves[2] - .out_ty - .origin - .syntax - .to_token_stream() - .to_string(), + plan.leaves[2].out_ty.syntax().to_token_stream().to_string(), "SampleKind" ); } diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index c9470d58..c524f014 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -297,7 +297,7 @@ impl CbindgenBuilder { f.ret .walk() .iter() - .any(|t| matches!(t.kind, crate::api::core::flat::TypeKind::Sequence(_))) + .any(|t| matches!(t.kind(), crate::api::core::flat::TypeKind::Sequence(_))) }) .unwrap_or(false) }) diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 7c380c2f..cfe96b5a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -852,17 +852,17 @@ impl Declarations { // model already normalized them. let ret = accessor.ret.borrow_target().unwrap_or(&accessor.ret); assert!( - !matches!(ret.kind, crate::api::core::flat::TypeKind::Unit), + !matches!(ret.kind(), crate::api::core::flat::TypeKind::Unit), "expand_return!({}).fields(fields!({func})): `{func}` returns nothing — a \ value form returns the struct holding this type's fields", key.as_str(), ); - let TypeKind::DataStruct { st, .. } = self.type_kind(registry, &ret.origin.syntax) else { + let TypeKind::DataStruct { st, .. } = self.type_kind(registry, ret.syntax()) else { panic!( "expand_return!({}).fields(fields!({func})): `{func}` returns `{}`, which is \ not a struct — a value form returns a struct whose fields become the leaves", key.as_str(), - ret.origin.syntax.to_token_stream(), + ret.syntax().to_token_stream(), ) }; let st = st.clone(); @@ -1001,7 +1001,7 @@ impl Declarations { // must decompose into its selector and groups wherever it appears. let bare = field.ty.optional_inner().unwrap_or(&field.ty); let probe = bare.sequence_elem().unwrap_or(bare); - match self.type_kind(registry, &probe.origin.syntax) { + match self.type_kind(registry, probe.syntax()) { TypeKind::DataStruct { st, cfg: Some(_) } if field.ty.optional_inner().is_none() && field.ty.sequence_elem().is_none() => @@ -1035,7 +1035,7 @@ impl Declarations { decl.func, st.name, dotted, - probe.origin.syntax.to_token_stream(), + probe.syntax().to_token_stream(), ); assert!( field.ty.optional_inner().is_none(), @@ -1048,12 +1048,12 @@ impl Declarations { decl.func, st.name, dotted, - probe.origin.syntax.to_token_stream(), + probe.syntax().to_token_stream(), dotted, ); // The name is the reading's, not a path taken apart to // re-derive one. - let crate::api::core::flat::TypeKind::Named { id } = &probe.kind else { + let crate::api::core::flat::TypeKind::Named { id } = probe.kind() else { panic!("a sum type is a named type") }; let crate::api::core::flat::Type::Variant(sum) = registry diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs index cae87a24..0ff50cf9 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs @@ -49,7 +49,7 @@ pub(crate) fn callback_input( let arg_pat_ty: Vec = args .iter() .map(|t| { - let t = &t.origin.syntax; + let t = t.syntax(); quote!(#t) }) .collect(); @@ -83,7 +83,7 @@ pub(crate) fn callback_input( // Every leaf converter must already be resolved (deferral safety). // A synthesized leaf (a sum's tag) has no converter to wait for. for leaf in plan.leaves.iter().filter(|l| l.has_converter()) { - registry.output_entry(&leaf.out_ty.origin.syntax)?; + registry.output_entry(leaf.out_ty.syntax())?; } let spec = folder_iface_for_plan(ext, registry, plan)?; let holder_slash = @@ -176,7 +176,7 @@ pub(crate) fn callback_input( // would make the trampoline wait forever on an `i32` crossing the // binding may not have. for leaf in plan.leaves.iter().filter(|l| l.has_converter()) { - let e = registry.output_entry(&leaf.out_ty.origin.syntax)?; + let e = registry.output_entry(leaf.out_ty.syntax())?; if leaf.identity && e.metadata.projection.is_none() { return None; } @@ -199,7 +199,7 @@ pub(crate) fn callback_input( // converter and clone the borrow (the callback only borrows the value). The // `data_class` converter composes the whole object via `fromParts`, so the // Kotlin `run(t: T)` receives a ready-made `T`. - let (cb_val, arg_entry) = match registry.output_entry(&arg_ty.origin.syntax) { + let (cb_val, arg_entry) = match registry.output_entry(arg_ty.syntax()) { Some(e) => (quote!(#cb_arg), e), // A borrow: the callback hands out a reference, and the value is // cloned for the JVM. @@ -227,7 +227,7 @@ pub(crate) fn callback_input( let core = arg_ty.borrow_target()?; ( quote!((#cb_arg).clone()), - registry.output_entry(&core.origin.syntax)?, + registry.output_entry(core.syntax())?, ) } }; @@ -318,7 +318,7 @@ pub(crate) fn callback_input( // cannot give. Keyed off each arg's own `origin.syntax`, which // `a_callback_identity_is_the_same_from_the_reading_or_the_syntax` pins as // the SAME identity the signature-derived key produces. - let arg_spellings: Vec = args.iter().map(|a| a.origin.syntax.clone()).collect(); + let arg_spellings: Vec = args.iter().map(|a| a.syntax().clone()).collect(); let spec = ext.iface_spec(registry, &SpecKey::callback(&arg_spellings))?; let descr_lit = syn::LitStr::new(&spec.descr, Span::call_site()); // Local-frame capacity: roughly an encoded wire + a wrapped object per diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index 3e7a0cd7..e4d9c1bf 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -478,7 +478,7 @@ pub(crate) fn reach_leaf_flat( // `single_return` in `core/unfold.rs`. `is_plain_field` is what that rules // out, and it stays as the local statement of the same fact. let reached_is_ours = if leaf.identity { - !matches!(leaf.out_ty.origin.syntax, syn::Type::Reference(_)) + !matches!(leaf.out_ty.syntax(), syn::Type::Reference(_)) } else { consuming }; @@ -980,11 +980,11 @@ pub(crate) fn encode_plan_leaves( let (value, by_ref, path, consuming) = rebase(leaf); let value = &value; let out_entry = registry - .output_entry(&leaf.out_ty.origin.syntax) + .output_entry(leaf.out_ty.syntax()) .unwrap_or_else(|| { panic!( "jnigen unfold: leaf `{}` has no registered output converter", - TypeKey::from_type(&leaf.out_ty.origin.syntax) + TypeKey::from_type(leaf.out_ty.syntax()) ) }); let conv_fail = fail(quote!(__e.to_string())); @@ -1047,7 +1047,7 @@ pub(crate) fn encode_plan_leaves( panic!( "jnigen unfold: identity leaf `{}` has no projection — \ `.accessor_record_id()` requires a ptr_class type", - TypeKey::from_type(&leaf.out_ty.origin.syntax) + TypeKey::from_type(leaf.out_ty.syntax()) ) }); // The place this handle lives, when it is OURS to give away — the @@ -1063,7 +1063,7 @@ pub(crate) fn encode_plan_leaves( // `Option` through the nullable branch's `match`, which moves the // whole `Option` in rather than borrowing it. let owned_place: Option = - if !matches!(leaf.out_ty.origin.syntax, syn::Type::Reference(_)) + if !matches!(leaf.out_ty.syntax(), syn::Type::Reference(_)) && steps_are_movable(&path) { let segs: Vec<&syn::Ident> = path.iter().map(PathStep::ident).collect(); @@ -1376,7 +1376,7 @@ pub(crate) fn leaf_is_prim( if leaf.nullable { return false; } - leaf_ty_is_prim(registry, &leaf.out_ty.origin.syntax) + leaf_ty_is_prim(registry, leaf.out_ty.syntax()) } /// The wire half of [`leaf_is_prim`]: does a leaf of this type occupy a **raw diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs index f2b3f863..4dd69699 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/struct_out.rs @@ -96,7 +96,7 @@ pub(crate) fn synth_value_struct_leaves( let mut leaves: Vec = Vec::new(); for field in &s.fields { let fname = field.name.as_ref()?.clone(); - let effective_ty = field.ty.origin.syntax.clone(); + let effective_ty = field.ty.syntax().clone(); let camel = mangle_kotlin_ident(&kt_snake_to_camel(&fname.to_string())); let leaf_name = if name_prefix.is_empty() { camel diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index 59c07def..cf7e8854 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -60,15 +60,15 @@ pub(crate) fn synth_sum_leaves( // returned value. Nothing looks up a converter for it (`has_converter()` is // false for a `SumTag`): there is no value to convert, the emitter assigns // the tag literal per arm. Its wire is a `jint` by definition. - let enum_ident = &sum.name; let mut leaves = vec![UnfoldLeaf { name: SUM_TAG_LEAF.to_string(), path: Vec::new(), - // Composed: the tag names WHICH sum it selects over, and no source - // wrote that as a standalone type. Nothing resolves a converter for it - // (`has_converter()` is false), but the emitter reads it back to find - // the enum to `match`. - out_ty: crate::api::core::flat::TypeRef::named(enum_ident), + // The tag names WHICH sum it selects over, and no source wrote that as + // a standalone type — so the DECLARATION answers, rather than this + // emitter minting a reading from the name. Nothing resolves a converter + // for it (`has_converter()` is false), but the emitter reads it back to + // find the enum to `match`. + out_ty: sum.type_ref().clone(), identity: false, nullable: false, source: LeafSource::SumTag, @@ -129,7 +129,7 @@ pub(crate) fn leaf_slot( ("I", format_ident!("i")) } else { let wire = registry - .output_entry(&leaf.out_ty.origin.syntax) + .output_entry(leaf.out_ty.syntax()) .expect("leaf_is_prim implies a resolved output entry") .destination .clone(); @@ -187,7 +187,7 @@ pub(crate) fn encode_sum_group( .expect("a sum segment carries its selector leaf"); // The name off the reading — `TypeId` IS the name, so nothing takes a path // apart to re-derive one. - let crate::api::core::flat::TypeKind::Named { id } = &tag_leaf.out_ty.kind else { + let crate::api::core::flat::TypeKind::Named { id } = tag_leaf.out_ty.kind() else { panic!( "jnigen sum unfold: selector type `{}` is not a named type", tag_leaf.out_ty.key() @@ -346,12 +346,12 @@ fn encode_group_leaf( fail: &dyn Fn(TokenStream) -> TokenStream, ) -> TokenStream { let out_entry = registry - .output_entry(&leaf.out_ty.origin.syntax) + .output_entry(leaf.out_ty.syntax()) .unwrap_or_else(|| { panic!( "jnigen sum unfold: payload leaf `{}` (`{}`) has no registered output converter", leaf.name, - TypeKey::from_type(&leaf.out_ty.origin.syntax) + TypeKey::from_type(leaf.out_ty.syntax()) ) }); let wire = out_entry.destination.clone(); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 34832ce9..518367a6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -137,7 +137,7 @@ pub(crate) fn synthetic_getter( ident: syn::Ident, ret: crate::api::core::flat::TypeRef, ) -> crate::api::core::flat::Function { - let ret_syntax = &ret.origin.syntax; + let ret_syntax = ret.syntax(); let item: syn::ItemFn = syn::parse_quote! { pub fn #ident() -> #ret_syntax { unimplemented!() @@ -146,7 +146,7 @@ pub(crate) fn synthetic_getter( crate::api::core::flat::Function { name: ident, params: Vec::new(), - origin: ret.origin.with(item), + origin: ret.origin_with(item), ret, } } @@ -754,7 +754,7 @@ pub(crate) fn emit_expanded_param( debug_assert_eq!(plan.leaves.len(), leaves.len()); for (leaf, classified) in plan.leaves.iter().zip(leaves) { - let leaf_ty = &leaf.ty.origin.syntax; + let leaf_ty = leaf.ty.syntax(); let lookup_entry = || { registry.input_entry(leaf_ty).unwrap_or_else(|| { panic!( diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index 82d4b230..4ec5d2a8 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -430,7 +430,7 @@ impl JniFunctionPlan { // fail to yield a type. for param in &f.params { let ident = param.name.clone(); - let ty = param.ty.origin.syntax.clone(); + let ty = param.ty.syntax().clone(); let form = if let Some(plan) = registry .expansion_plans() @@ -495,7 +495,7 @@ impl JniFunctionPlan { InputKind::Handle { .. } | InputKind::VecBuild { .. } => 2, InputKind::Callback { .. } => 1, InputKind::Unsigned64 { .. } | InputKind::Plain => registry - .input_entry(&leaf.reading.origin.syntax) + .input_entry(leaf.reading.syntax()) .and_then(|entry| JniPrim::from_wire(&entry.destination)) .map_or(1, |prim| match prim { JniPrim::Long | JniPrim::Double => 2, @@ -537,7 +537,7 @@ fn classify_leaf( ) -> Result { // The reading, so the layer questions cannot miss. What generated Rust must // spell is `origin.syntax`, unchanged. - let ty = &reading.origin.syntax; + let ty = reading.syntax(); let optional = reading.optional_inner().is_some(); let as_enum_value = ext.is_kotlin_enum(&enum_probe_type(ty)); let kt_name = kt_param_name(&ident.to_string()); @@ -680,7 +680,7 @@ fn build_output( let is_convert = unfold_plan.is_some(); // The element normalizes an elided return and a written `-> ()` to one // `Unit` reading, so there is no `ReturnType` match here. - let return_ty: syn::Type = f.ret.origin.syntax.clone(); + let return_ty: syn::Type = f.ret.syntax().clone(); let error_plan = registry.error_plans().get(ident); let ok_ty = error_plan.and_then(|_| result_ok_type(&return_ty)); let target_ty = match unfold_plan { @@ -704,7 +704,7 @@ fn build_output( let ret_decl: syn::ReturnType = if is_convert { syn::parse_quote!(-> #target_ty) } else { - let ret = &f.ret.origin.syntax; + let ret = f.ret.syntax(); syn::parse_quote!(-> #ret) }; let (surface, canonical) = ReturnSurface::classify(ext, registry, &ret_decl); diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index 07889912..12811fad 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -720,13 +720,12 @@ fn plan_leaf_param( // here and re-asserted (`!!`) inside its own live arm — the same rule // `nullable_group_part` applies to the parent-inlined `fromParts`. Primitive // slots take their `0`/`false` default and stay unboxed. - let inert_nullable = - leaf.group.is_some() && !leaf_ty_is_prim(registry, &leaf.out_ty.origin.syntax); + let inert_nullable = leaf.group.is_some() && !leaf_ty_is_prim(registry, leaf.out_ty.syntax()); leaf_iface_param( ext, registry, name, - &leaf.out_ty.origin.syntax, + leaf.out_ty.syntax(), leaf.nullable || inert_nullable, true, ) @@ -1151,12 +1150,12 @@ pub(crate) fn callback_iface_spec( any_fixed = true; // Peeled off the reading — `borrow_target` is the model's // answer to "is this a borrow", not a syn match. - let core = t.borrow_target().unwrap_or(t).origin.syntax.clone(); + let core = t.borrow_target().unwrap_or(t).syntax().clone(); let fqn = ext.kotlin_fqn(&TypeKey::from_type(&core))?; let (reassemble, imports) = fixed_reassembly(ext, registry, &core, &plan.leaves, &fqn); groups.push(GroupDesc { - name: whole_value_name(&t.origin.syntax, i), + name: whole_value_name(t.syntax(), i), typed: Some(kt::KtType::cls(fqn.to_string())), reassemble: Some(reassemble), imports, @@ -1182,12 +1181,11 @@ pub(crate) fn callback_iface_spec( }; if leaf.source == LeafSource::SumTag { any_fixed = true; - let fqn = - ext.kotlin_fqn(&TypeKey::from_type(&leaf.out_ty.origin.syntax))?; + let fqn = ext.kotlin_fqn(&TypeKey::from_type(leaf.out_ty.syntax()))?; let (reassemble, imports) = fixed_reassembly( ext, registry, - &leaf.out_ty.origin.syntax, + leaf.out_ty.syntax(), &plan.leaves[k..seg], &fqn, ); @@ -1229,18 +1227,18 @@ pub(crate) fn callback_iface_spec( // A plan-less opaque-handle arg is delivered as a raw `jlong` and // wrapped + closed Kotlin-side (Phase 3 — no Rust `new_object`). let owned_handle = registry - .output_entry(&t.origin.syntax) + .output_entry(t.syntax()) .and_then(|e| e.metadata.projection.as_ref()) .map(|p| p.kind == ProjectionKind::Handle) .unwrap_or(false); leaf_tys.push(LeafDesc::Whole { - name: whole_value_name(&t.origin.syntax, i), - ty: t.origin.syntax.clone(), + name: whole_value_name(t.syntax(), i), + ty: t.syntax().clone(), nullable: t.optional_inner().is_some(), owned_handle, }); groups.push(GroupDesc { - name: whole_value_name(&t.origin.syntax, i), + name: whole_value_name(t.syntax(), i), typed: None, reassemble: None, imports: Vec::new(), @@ -1297,14 +1295,14 @@ pub(crate) fn callback_iface_spec( "{}Callback", cb_args .iter() - .map(|t| subject_short(&t.origin.syntax)) + .map(|t| subject_short(t.syntax())) .collect::>() .join("") ) }; let package = cb_args .first() - .map(|t| subject_package(ext, &t.origin.syntax)) + .map(|t| subject_package(ext, t.syntax())) .unwrap_or_else(|| ext.package.clone()); Some(IfaceSpec { typed_groups, diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 584c50a8..9be3e88d 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -791,7 +791,7 @@ impl Declarations { // The field's own reading: the nullability question below is answered // from `kind`, so a wrapped spelling answers as the bare one does and // nothing is looked up (#275). - let field_ty = &field.ty.origin.syntax; + let field_ty = field.ty.syntax(); let where_ = || format!("sealed_class!({}) payload `{variant}.{prop}`", sum_name); let out = registry.output_entry(field_ty).unwrap_or_else(|| { panic!( @@ -1519,9 +1519,9 @@ impl Declarations { // it `Int` and the wrap has to name the enum class itself — read off the // same output-converter metadata `factory_field` reads for an enum // struct field. - if self.is_kotlin_enum(&enum_probe_type(&leaf.out_ty.origin.syntax)) { - let inner = option_inner_type(&leaf.out_ty.origin.syntax) - .unwrap_or_else(|| leaf.out_ty.origin.syntax.clone()); + if self.is_kotlin_enum(&enum_probe_type(leaf.out_ty.syntax())) { + let inner = option_inner_type(leaf.out_ty.syntax()) + .unwrap_or_else(|| leaf.out_ty.syntax().clone()); let name = registry .output_entry(&inner) .and_then(|e| e.metadata.kotlin_name.clone()) diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index 8503ab1e..b3a5ab3b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -123,7 +123,7 @@ pub(crate) fn build_data_class( // disagreed. Reject it at the declaration instead. if !matches!(pf.kind, PlanFieldKind::Projection { .. }) { if let Some(proj) = registry - .input_entry(&field.ty.origin.syntax) + .input_entry(field.ty.syntax()) .and_then(|e| e.metadata.projection.clone()) { panic!( @@ -1069,7 +1069,7 @@ fn classify_params( let mut params: Vec = Vec::new(); for leaf in fplan.leaves() { let mut name = leaf.kt_name.clone(); - let arg_ty = &leaf.reading.origin.syntax; + let arg_ty = leaf.reading.syntax(); // Instance-method receiver: the first parameter whose peeled Rust type // is the owning class binds to `this` (so `this_ptr`/`this.ptr`/lock or diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index cb29bc52..5e151a36 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -50,7 +50,7 @@ impl Declarations { // comes from here. The converter yields this spelling, so a // `Box>` crossing produces a `Box>` — the shape it // is dispatched as no longer decides what it is called. - let syntax = &ty.origin.syntax; + let syntax = ty.syntax(); // 1. Terminal categories (incl. the terminal user-wrapper lookup). if let Some(c) = self.input_terminal(ty, registry) { @@ -66,13 +66,13 @@ impl Declarations { // that resolves correctly wins. if let Some(target) = inner.borrow_target() { let mutable = matches!( - inner.kind, + inner.kind(), crate::api::core::flat::TypeKind::Ref { mode: RefMode::Exclusive, .. } ); - let t1 = target.origin.syntax.clone(); + let t1 = target.syntax().clone(); if let Some(mut c) = self.input_wrapper_shape( WrapperShape::OptionRef { mutable }, syntax, @@ -91,14 +91,14 @@ impl Declarations { // wrapped optional borrow stops here rather than resolving wrong. if inner.borrow_target().is_some() { let canonical: syn::Type = { - let b = &inner.origin.syntax; + let b = inner.syntax(); syn::parse_quote!(Option<#b>) }; if syntax.to_token_stream().to_string() != canonical.to_token_stream().to_string() { return None; } } - let inner_ty = inner.origin.syntax.clone(); + let inner_ty = inner.syntax().clone(); if let Some(mut c) = self.input_wrapper_shape(WrapperShape::Optional, syntax, &inner_ty, registry) { @@ -108,7 +108,7 @@ impl Declarations { return None; } if let Some(elem) = ty.sequence_elem().filter(|_| !is_unsized_spelling(syntax)) { - let elem_ty = elem.origin.syntax.clone(); + let elem_ty = elem.syntax().clone(); if let Some(mut c) = self.input_wrapper_shape(WrapperShape::Sequence, syntax, &elem_ty, registry) { @@ -117,7 +117,7 @@ impl Declarations { } return None; } - if let crate::api::core::flat::TypeKind::Ref { mode, inner } = &ty.kind { + if let crate::api::core::flat::TypeKind::Ref { mode, inner } = ty.kind() { // `&[T]` shared slice borrow: there is no owned `[T]` to decode, so // reuse the `Vec<_>` shape — decode the Java `List` into an owned // `Vec`; the call site borrows it (`&Vec` deref-coerces to @@ -138,9 +138,9 @@ impl Declarations { // NOT: passing `&Vec` there does not compile. Those fall through to // the plain borrow arm below, which hands the whole spelling on as the // sub, exactly as the old syntactic slice check did. - if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(&inner.origin.syntax) { + if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(inner.syntax()) { if let Some(elem) = inner.sequence_elem() { - let elem_ty = elem.origin.syntax.clone(); + let elem_ty = elem.syntax().clone(); // The one place `produced` is NOT the crossing's spelling: // there is no owned `[T]` to decode into, so the converter // yields an owned `Vec` and the call site borrows it. @@ -158,7 +158,7 @@ impl Declarations { } } let mutable = matches!(mode, RefMode::Exclusive); - let t1 = inner.origin.syntax.clone(); + let t1 = inner.syntax().clone(); if let Some(mut c) = self.input_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, &t1, registry) { @@ -184,7 +184,7 @@ impl Declarations { // detected its layers with `option_inner_type`/`vec_inner_type`, which // read the last path segment's ident. A `Box>` answered // "neither", and got no converter at all (#270). - let syntax = &ty.origin.syntax; + let syntax = ty.syntax(); // 1. Terminal categories (incl. the terminal user-wrapper lookup). if let Some(c) = self.output_terminal(ty, registry) { @@ -204,7 +204,7 @@ impl Declarations { // whose inner converter is the `&Handle` borrow entry (no deep // output handler). if let Some(inner) = ty.optional_inner() { - let inner_ty = inner.origin.syntax.clone(); + let inner_ty = inner.syntax().clone(); if let Some(mut c) = self.output_wrapper_shape(WrapperShape::Optional, syntax, &inner_ty, registry) { @@ -214,7 +214,7 @@ impl Declarations { return None; } if let Some(elem) = ty.sequence_elem().filter(|_| !is_unsized_spelling(syntax)) { - let elem_ty = elem.origin.syntax.clone(); + let elem_ty = elem.syntax().clone(); if let Some(mut c) = self.output_wrapper_shape(WrapperShape::Sequence, syntax, &elem_ty, registry) { @@ -223,19 +223,19 @@ impl Declarations { } return None; } - if let crate::api::core::flat::TypeKind::Ref { mode, inner } = &ty.kind { + if let crate::api::core::flat::TypeKind::Ref { mode, inner } = ty.kind() { // `&[T]` shared slice (a callback argument crossing native→JVM): // build a `List` from the borrowed slice. Dual of the `&[T]` // input branch, and the same split: `kind` says it is a borrow of a // run of values; whether the generated Rust can iterate the borrow // directly is a question about the SPELLING. - if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(&inner.origin.syntax) { + if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(inner.syntax()) { if let Some(elem) = inner.sequence_elem() { - return self.output_slice(&elem.origin.syntax, registry); + return self.output_slice(elem.syntax(), registry); } } let mutable = matches!(mode, RefMode::Exclusive); - let t1 = inner.origin.syntax.clone(); + let t1 = inner.syntax().clone(); if let Some(mut c) = self.output_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, &t1, registry) { @@ -264,8 +264,8 @@ fn fallible_parts( registry: &impl Conversions, ) -> Option<(syn::Type, syn::Type)> { use crate::api::core::flat::TypeKind; - if let Some(TypeKind::Fallible { ok, err }) = registry.flat().type_ref(ty).map(|t| &t.kind) { - return Some((ok.origin.syntax.clone(), err.origin.syntax.clone())); + if let Some(TypeKind::Fallible { ok, err }) = registry.flat().type_ref(ty).map(|t| t.kind()) { + return Some((ok.syntax().clone(), err.syntax().clone())); } crate::api::core::types_util::result_parts(ty) } diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 8823c16e..2f59a355 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -225,7 +225,7 @@ pub(crate) fn classify_field( // classified this type. Taking a `syn::Type` meant asking the registry per // question, and a type it had never seen answered "no layer" rather than // saying so — which is the missing `?` of #273 waiting to happen again. - let effective_ty = reading.origin.syntax.clone(); + let effective_ty = reading.syntax().clone(); // A sum is classified FIRST, because it is the one kind with no converter // of its own: it crosses as a tag plus one leaf group per variant, never @@ -244,9 +244,9 @@ pub(crate) fn classify_field( // other about the same field (#273). let optional_inner = reading.optional_inner(); let bare_ref = optional_inner.unwrap_or(reading); - let bare = bare_ref.origin.syntax.clone(); + let bare = bare_ref.syntax().clone(); let seq_elem = bare_ref.sequence_elem(); - let core = seq_elem.map_or_else(|| bare.clone(), |e| e.origin.syntax.clone()); + let core = seq_elem.map_or_else(|| bare.clone(), |e| e.syntax().clone()); if matches!(ext.type_kind(registry, &core), TypeKind::Sum) { // A `Vec` of tag-gated groups has variable arity, exactly like a `Vec` // of nested data classes — the flattened bridge is fixed-layout by @@ -282,7 +282,7 @@ pub(crate) fn classify_field( return Some(PlanFieldKind::Enum { conv, kotlin }); } // `Option` leaf. - if let Some(inner) = optional_inner.map(|i| i.origin.syntax.clone()) { + if let Some(inner) = optional_inner.map(|i| i.syntax().clone()) { if ext.is_kotlin_enum(&inner) { let kotlin = registry .output_entry(&inner)? @@ -322,8 +322,8 @@ pub(crate) fn classify_field( None => { // Object-shaped wire with no fixed descriptor; the JVM slot // must be the field's actual declared type (Option-stripped). - let slot_ty = optional_inner - .map_or_else(|| effective_ty.clone(), |i| i.origin.syntax.clone()); + let slot_ty = + optional_inner.map_or_else(|| effective_ty.clone(), |i| i.syntax().clone()); let descriptor = registry .output_entry(&slot_ty) .and_then(|e| jni_field_access(&e.destination)) diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs index faa05c50..c769e7c4 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/callbacks.rs @@ -621,7 +621,7 @@ fn a_callback_identity_is_the_same_from_the_reading_or_the_syntax() { let cb = f .params .iter() - .find_map(|p| match &p.ty.kind { + .find_map(|p| match p.ty.kind() { crate::api::core::flat::TypeKind::Callback { args } => Some((p, args)), _ => None, }) @@ -632,11 +632,11 @@ fn a_callback_identity_is_the_same_from_the_reading_or_the_syntax() { let from_reading = SpecKey::callback( &arg_readings .iter() - .map(|a| a.origin.syntax.clone()) + .map(|a| a.syntax().clone()) .collect::>(), ); let from_syntax = SpecKey::callback( - &crate::api::core::registry::extract_fn_trait_args(¶m.ty.origin.syntax) + &crate::api::core::registry::extract_fn_trait_args(param.ty.syntax()) .expect("the param is an impl Fn"), ); diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index 40ec0a32..26bd5e5f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -241,7 +241,7 @@ fn deriving_matches_the_equivalent_hand_written_list() { .map(|l| { ( l.name.clone(), - l.out_ty.origin.syntax.to_token_stream().to_string(), + l.out_ty.syntax().to_token_stream().to_string(), ) }) .collect() diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 05f1d338..e48fdf35 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -1188,7 +1188,7 @@ impl Declarations { // rides `immediate_edges` rather than this converter's `subs`. // The arguments are `TypeRef`s on the classification, so nothing // is re-extracted from the signature's syntax. - let crate::api::core::flat::TypeKind::Callback { args } = &reading.kind else { + let crate::api::core::flat::TypeKind::Callback { args } = reading.kind() else { return None; }; self.dispatch_fn_input(args, built) @@ -1361,7 +1361,7 @@ impl Declarations { // `Vec` / `Option>` return. The model's `ret` already // normalizes an elided return to `()`, so there is no arm for it. { - let ret = &f.ret.origin.syntax; + let ret = f.ret.syntax(); let after_opt = crate::api::core::types_util::option_inner_type(ret).unwrap_or(ret.clone()); if let Some(elem) = crate::api::core::types_util::vec_inner_type(&after_opt) { @@ -1393,7 +1393,7 @@ impl Declarations { args: &[crate::api::core::flat::TypeRef], registry: &impl Conversions, ) -> Option> { - let spellings: Vec = args.iter().map(|a| a.origin.syntax.clone()).collect(); + let spellings: Vec = args.iter().map(|a| a.syntax().clone()).collect(); let outer_ty = build_fn_type(&spellings); let (wire, body) = callback_input(self, args, registry)?; let niches = default_niches_for_wire(&wire); @@ -1800,7 +1800,7 @@ impl Declarations { // Classify off `kind`, spell off `syntax`: the arms below that ask what // a type IS use `reading`, and everything that has to name it in // generated Rust uses this. - let ty = &reading.origin.syntax; + let ty = reading.syntax(); // Structured-config overrides first (opaque handles, then user- // registered rank-0 wrappers, then built-ins). let key = TypeKey::from_type(ty); @@ -1893,7 +1893,7 @@ impl Declarations { // `str` is handled above, separately and deliberately: it is unsized, // so its converter yields an owned `String` the call site borrows — // a different contract, not a different spelling. - if matches!(reading.kind, crate::api::core::flat::TypeKind::Str) { + if matches!(reading.kind(), crate::api::core::flat::TypeKind::Str) { let wire: syn::Type = syn::parse_quote!(jni::objects::JString); let body: syn::Expr = syn::parse_quote!({ let s = env.get_string(v).map_err(|e| { @@ -2021,7 +2021,7 @@ impl Declarations { registry: &impl Conversions, ) -> Option> { // Classify off `kind`, spell off `syntax` — see `input_terminal`. - let ty = &reading.origin.syntax; + let ty = reading.syntax(); // Structured-config overrides first (opaque handles, then built-ins). let key = TypeKey::from_type(ty); if let Some(cfg) = self.types.get(&key) { @@ -2091,7 +2091,7 @@ impl Declarations { // Plain `String` keeps its own earlier arm in `primitive_output`, whose // body this matches exactly; this one is reached for the wrapped // spellings that arm's key cannot name. - if matches!(reading.kind, crate::api::core::flat::TypeKind::Str) { + if matches!(reading.kind(), crate::api::core::flat::TypeKind::Str) { let wire: syn::Type = syn::parse_quote!(jni::objects::JString); let body: syn::Expr = syn::parse_quote!({ env.new_string(v.as_str()).map_err(|e| { @@ -2123,7 +2123,7 @@ impl Declarations { // Wire is `()`. Body just returns `v`. No Kotlin name — Unit // returns are dropped from emitted signatures, so metadata stays // empty. - if matches!(reading.kind, crate::api::core::flat::TypeKind::Unit) { + if matches!(reading.kind(), crate::api::core::flat::TypeKind::Unit) { let wire: syn::Type = syn::parse_quote!(()); let body: syn::Expr = syn::parse_quote!(v); return Some(ConverterImpl { From 31304f0c669b3bda6c66a64f4eeb6ab2f4a380b0 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 15:15:31 +0200 Subject: [PATCH 32/52] The registration path carries readings instead of re-deriving them (#283) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * core: the registration path carries readings instead of re-deriving them Closes #281. A composed reading was built, thrown away, and independently re-derived: `expand.rs` composes `pty.optional()`, `unfold.rs` handed only its SPELLING to `require_output`, and `ensure_entry` classified those tokens from scratch and stored its own twin. Two classifications of one type, by two paths that never met, and nothing compared them. The issue proposed moving the composers behind a registry API. That would not have closed it, and the PR says so on the issue rather than silently skipping it: the loss is at the door, not at the composer, and it happened again at every recursion step — `immediate_edges` had each child as a `&TypeRef` and did `child.syntax().clone()` so the next level could re-classify it. One rule now: a type enters the registry as a READING; only a spelling nobody has classified yet goes through `classify`. ensure_entry(dir, &TypeRef, root) stores the caller's reading, INFALLIBLE register_type_{recursive,inner} take &TypeRef, infallible require_*/unrequire_* take &TypeRef immediate_edges returns (Direction, TypeRef) intern / intern_recursive the one fallible door, for a spelling Infallibility falls out rather than being claimed: `ensure_entry` was fallible for exactly one reason — `classify` refusing a spelling — and a reading has already been through that. #281 planned to assert layering is total and pin it with a test; carrying the reading makes the question not arise. Ten of the twelve `require_*` sites already held a `TypeRef` and called `.syntax()` on it at the door, so those are deletions. `unfold.rs`'s composed `cv_ty` now uses `optional()` instead of `parse_quote!(Option<#leaf_ty>)`, pairing kind with spelling in one place. `Flat::classify` is down to ONE production caller, `intern`. Acceptance test, verified to fail with the fix reverted: `a_composed_reading_reaches_the_cell_unchanged` composes `Option` — a spelling the source never writes — and asserts the cell keeps the source location. Reverted, the cell holds a PLACELESS reading, which is what a diagnostic about that crossing would have printed. Two mistakes caught in progress rather than shipped: `intern` does not recurse, so six sites that were `register_type_recursive` needed `intern_recursive` (caught by cbindgen's example panicking, not by a test); and the regex that dropped `.syntax()` added `&` to values that were already references (`needless_borrow`). regen-check byte-identical — which here is EVIDENCE, not a regression check: the cell used to hold `classify(spelling)` and now holds the caller's reading, so identical output is the first confirmation that the two answers agree for every type the examples exercise. Verified: 551 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p` (forced rebuild confirmed by the Compiling/Generated lines), covertest-kotlin 48/48, boundary ledger unchanged at 127. * core: narrow `intern` to `api::core`, matching what #280 sealed Review on #283 is correct. I widened `intern` to `pub(crate)` so two test modules could reach it, and never checked what else that admitted: classifying a spelling MINTS a reading, so a `pub(crate)` door let `api::lang` hand the registry tokens of its own and receive a `TypeRef` back — exactly the capability #280 closed by making the composers and `Flat::classify` `pub(in crate::api::core)`. A one-door design is only worth having if the door is no wider than the entrances it replaces. Both test callers are under `api::core` (`resolve/tests.rs`, `unfold/tests.rs`), and every production caller is in `core::registry`, so the narrowing costs nothing. Measured rather than assumed: an `api::lang` call to `intern` now fails `E0624`. The doc records why the visibility is what it is, so a future widening has to argue against the reason rather than rediscover it. Verified: 551 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p`, covertest-kotlin 48/48. * core: intern reuses a known reading; unrequire and Layered stop downgrading Two review points on #283, both correct. [P2] `intern` classified unconditionally and only then called `ensure_entry`, whose existing-cell arm discards the reading. So a repeated registration of the same key still derived a second reading that never met the first — the very shape this PR removes, surviving as redundant work rather than as a replaced cell, and contradicting the stated rule that only a spelling nobody has classified yet goes through `classify`. `intern` now looks the key up first, in EITHER direction (a reading is direction-free), clones the authoritative answer, marks or creates the directional cell, and calls `Flat::classify` only on a genuine miss. That also restores the old `ensure_entry` property of classifying only when a cell is new. [Copilot] `unrequire_*` still took `&syn::Type` while the PR description claimed the whole registration surface was reading-based. The description was the thing that was wrong, so the code is now what it claimed: unrequire_output(&TypeRef) pairs with require_output clear_root(dir, &TypeKey) the keyed primitive underneath Keyed is the honest signature for `clear_root`: un-requiring creates no cell and classifies nothing, so it is the one registration-adjacent operation with no reading to carry. `run.rs` already held keys and now passes them straight in, dropping a `to_type()` round trip. Two consequences, both taken rather than worked around: * `Layered::layer_types` was `Vec`, built by mapping `.syntax()` over `TypeRef::layer_types()` — the same discard one layer down. It now carries readings, which is what let the `unrequire_output` call site pass one. * `unrequire_input` has no callers once `run.rs` uses `clear_root`, so it is deleted rather than kept as dead code behind a symmetry argument. Two lines to restore if an input-side caller appears. Verified: 551 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p`, covertest-kotlin 48/48. No `.syntax()` downgrade remains at any registry door. --- prebindgen/src/api/core/expand.rs | 2 +- prebindgen/src/api/core/registry/order.rs | 12 +- prebindgen/src/api/core/registry/run.rs | 7 +- prebindgen/src/api/core/registry/scan.rs | 254 ++++++++++++++-------- prebindgen/src/api/core/registry/tests.rs | 77 ++++++- prebindgen/src/api/core/resolve.rs | 2 +- prebindgen/src/api/core/resolve/tests.rs | 9 +- prebindgen/src/api/core/unfold.rs | 45 ++-- prebindgen/src/api/core/unfold/tests.rs | 5 +- 9 files changed, 284 insertions(+), 129 deletions(-) diff --git a/prebindgen/src/api/core/expand.rs b/prebindgen/src/api/core/expand.rs index de0dc21b..b095944b 100644 --- a/prebindgen/src/api/core/expand.rs +++ b/prebindgen/src/api/core/expand.rs @@ -313,7 +313,7 @@ fn process_expand( )?; for leaf in &plan.leaves { - registry.require_input(leaf.ty.syntax()); + registry.require_input(&leaf.ty); } registry .expansion_plans diff --git a/prebindgen/src/api/core/registry/order.rs b/prebindgen/src/api/core/registry/order.rs index 5a31c8d6..992a3a6d 100644 --- a/prebindgen/src/api/core/registry/order.rs +++ b/prebindgen/src/api/core/registry/order.rs @@ -64,15 +64,21 @@ impl Registry { let mut edges: Vec = self .immediate_edges(dir, &ty) .into_iter() - .chain(self.plan_edges(dir, &ty)) + // The structural edges arrive as readings, so the key is the + // model's own answer rather than one re-derived from a spelling. + .map(|(d, t)| (d, t.key())) + .chain( + self.plan_edges(dir, &ty) + .into_iter() + .map(|(d, t)| (d, TypeKey::from_type(&t))), + ) .chain( self.declared .edges .iter() .filter(|(from, _)| *from == node) - .map(|(_, on)| (on.0, on.1.to_type())), + .map(|(_, on)| (on.0, on.1.clone())), ) - .map(|(d, t)| (d, TypeKey::from_type(&t))) // Only crossings the scan actually registered: a structural edge to // a type nothing asked for is not a crossing. .filter(|c| self.type_table(c.0).contains_key(&c.1)) diff --git a/prebindgen/src/api/core/registry/run.rs b/prebindgen/src/api/core/registry/run.rs index 10d47d92..64a6da8e 100644 --- a/prebindgen/src/api/core/registry/run.rs +++ b/prebindgen/src/api/core/registry/run.rs @@ -59,9 +59,10 @@ impl Registry { // Drop it both ways; the cell stays, so a converter is still produced // if one happens to resolve. for key in &declared.decompositions.replaces { - let ty = key.to_type(); - self.unrequire_input(&ty); - self.unrequire_output(&ty); + // The key is what a root flag is stored under, so it goes straight + // in — no `to_type()` round trip to be re-keyed on the far side. + self.clear_root(Direction::Input, key); + self.clear_root(Direction::Output, key); } Ok(()) } diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index 186f028a..d1e466ae 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -102,7 +102,7 @@ impl Registry { // the type is required in the output direction only. for ident in declared.consts.iter().flatten() { if let Some(item_const) = self.flat.constant(&ident).map(|c| c.origin.syntax.clone()) { - self.ensure_entry(Direction::Output, &item_const.ty, true)?; + self.intern(Direction::Output, &item_const.ty, true)?; } else { missing.push(("constant", ident.to_string())); } @@ -116,7 +116,7 @@ impl Registry { // Declared crossings with no element behind them (a foreign class type, // a synthesized constant's value type), each in its own direction. for (dir, ty) in &declared.crossings { - self.ensure_entry(*dir, ty, true)?; + self.intern(*dir, ty, true)?; } // Scan declared types. @@ -130,13 +130,13 @@ impl Registry { .map(|s| s.origin.syntax.clone()) { self.scan_struct(&s)?; - self.ensure_entry(Direction::Input, &ty, true)?; - self.ensure_entry(Direction::Output, &ty, true)?; + self.intern(Direction::Input, &ty, true)?; + self.intern(Direction::Output, &ty, true)?; matched = true; } else if let Some(e) = self.flat.enum_item(&ident).cloned() { self.scan_enum(&e)?; - self.ensure_entry(Direction::Input, &ty, true)?; - self.ensure_entry(Direction::Output, &ty, true)?; + self.intern(Direction::Input, &ty, true)?; + self.intern(Direction::Output, &ty, true)?; matched = true; } } @@ -145,8 +145,8 @@ impl Registry { // `ptr_class(ZKeyExpr<'static>)` on a re-exported // foreign type). Still mark required so the resolver // tries to produce a converter for it. - self.ensure_entry(Direction::Input, &ty, true)?; - self.ensure_entry(Direction::Output, &ty, true)?; + self.intern(Direction::Input, &ty, true)?; + self.intern(Direction::Output, &ty, true)?; } } @@ -168,7 +168,7 @@ impl Registry { match input { syn::FnArg::Receiver(_) => continue, syn::FnArg::Typed(pt) => { - self.register_type_recursive(Direction::Input, &pt.ty, true)?; + self.intern_recursive(Direction::Input, &pt.ty, true)?; } } } @@ -176,20 +176,20 @@ impl Registry { syn::ReturnType::Default => syn::parse_quote!(()), syn::ReturnType::Type(_, ty) => (**ty).clone(), }; - self.register_type_recursive(Direction::Output, &ret_ty, true)?; + self.intern_recursive(Direction::Output, &ret_ty, true)?; Ok(()) } pub(super) fn scan_struct(&mut self, s: &syn::ItemStruct) -> Result<(), ScanError> { // The struct itself can appear in either direction. let ty: syn::Type = crate::api::core::flat::type_from_ident(&s.ident); - self.ensure_entry(Direction::Input, &ty, false)?; - self.ensure_entry(Direction::Output, &ty, false)?; + self.intern(Direction::Input, &ty, false)?; + self.intern(Direction::Output, &ty, false)?; if let syn::Fields::Named(named) = &s.fields { for field in &named.named { - self.register_type_recursive(Direction::Input, &field.ty, false)?; - self.register_type_recursive(Direction::Output, &field.ty, false)?; + self.intern_recursive(Direction::Input, &field.ty, false)?; + self.intern_recursive(Direction::Output, &field.ty, false)?; } } Ok(()) @@ -197,13 +197,13 @@ impl Registry { pub(super) fn scan_enum(&mut self, e: &syn::ItemEnum) -> Result<(), ScanError> { let ty: syn::Type = crate::api::core::flat::type_from_ident(&e.ident); - self.ensure_entry(Direction::Input, &ty, false)?; - self.ensure_entry(Direction::Output, &ty, false)?; + self.intern(Direction::Input, &ty, false)?; + self.intern(Direction::Output, &ty, false)?; for variant in &e.variants { for field in &variant.fields { - self.register_type_recursive(Direction::Input, &field.ty, false)?; - self.register_type_recursive(Direction::Output, &field.ty, false)?; + self.intern_recursive(Direction::Input, &field.ty, false)?; + self.intern_recursive(Direction::Output, &field.ty, false)?; } } Ok(()) @@ -215,72 +215,124 @@ impl Registry { pub(super) fn register_type_recursive( &mut self, dir: Direction, - ty: &syn::Type, + reading: &crate::api::core::flat::TypeRef, root: bool, - ) -> Result<(), ScanError> { + ) { let mut visited: HashSet = HashSet::new(); - self.register_type_inner(dir, ty, root, &mut visited) + self.register_type_inner(dir, reading, root, &mut visited) } + /// Infallible, and structurally so: every type reached here is a reading — + /// the caller's, or one the model already holds for a child — so there is + /// nothing left to classify and nothing left to refuse. pub(super) fn register_type_inner( &mut self, dir: Direction, - ty: &syn::Type, + reading: &crate::api::core::flat::TypeRef, is_top: bool, visited: &mut HashSet, - ) -> Result<(), ScanError> { - // A disallowed `impl Trait` cannot reach here: every fn whose signature - // reaches this point passed the frontend's grammar — captured items at - // ingestion, binding-local ones at synthesis — and it names the - // parameter the bad type sits on. - - let key = TypeKey::from_type(ty); - if !visited.insert(key.clone()) { - return Ok(()); // cycle guard + ) { + let key = reading.key(); + if !visited.insert(key) { + return; // cycle guard } - self.ensure_entry(dir, ty, is_top)?; + self.ensure_entry(dir, reading, is_top); - for (child_dir, sub) in self.immediate_edges(dir, ty) { - self.register_type_inner(child_dir, &sub, false, visited)?; + for (child_dir, sub) in self.immediate_edges(dir, reading.syntax()) { + self.register_type_inner(child_dir, &sub, false, visited); } - Ok(()) } - /// Create the cell for `ty` in `dir` if it has none, and mark it a root when - /// the binding asked for it directly. + /// Create the cell for `reading` in `dir` if it has none, and mark it a root + /// when the binding asked for it directly. /// /// The one place a cell is born, and therefore the one place a type **enters - /// the pipeline** — including a spelling the source never wrote, since expansion - /// composes those (an `Option` around a `T` it found) and hands them straight - /// here via `require_input` / `require_output`. + /// the pipeline** — including a spelling the source never wrote, since + /// expansion composes those (an `Option` around a `T` it found) and hands + /// them straight here via `require_input` / `require_output`. /// - /// The reading is taken **here, once**, and lives in the cell. The model is - /// consulted for it — [`Flat::classify`](crate::api::core::flat::Flat::classify) - /// is the grammar's one answer — but the model is not extended: a composed - /// spelling is an intermediate in *this binding's* crossing graph, not something - /// the source API mentions, and the table that tracks crossings is where it - /// belongs. So `Flat` stays what the source said, and every type the pipeline - /// works with has its reading in the table by the time the builder is finished. + /// **The caller's reading is what gets stored.** It is not re-derived from + /// the spelling, and that is the point (#281): the reading a caller holds and + /// the one `classify` would produce for its spelling are two answers from two + /// paths, and nothing was comparing them. Now there is only one answer, + /// because there is only one classification. /// - /// A spelling the grammar refuses is reported by name, rather than becoming a - /// cell that quietly means less than its neighbours. Only an *entry point* can - /// reach that: a type the walk found came from an existing reading's - /// `origin.syntax`, so it lowered once already. + /// Which is also why this is **infallible**. It was fallible for exactly one + /// reason — `classify` refusing a spelling — and a reading has already been + /// through that. Only [`intern`](Self::intern), the door for a spelling + /// nobody has classified yet, can still fail. + /// + /// The model is consulted, never extended: a composed spelling is an + /// intermediate in *this binding's* crossing graph, not something the source + /// API mentions, so `Flat` stays what the source said while every type the + /// pipeline works with has its reading in the table. pub(super) fn ensure_entry( &mut self, dir: Direction, - ty: &syn::Type, + reading: &crate::api::core::flat::TypeRef, root: bool, - ) -> Result<(), ScanError> { - let key = TypeKey::from_type(ty); - // Classify only when the cell is actually new: the reading of a given key - // cannot change, so an existing cell already holds it. + ) { + let key = reading.key(); + // The reading of a given key cannot change, so an existing cell already + // holds an equal one — only the root flag can still move. if let Some(cell) = self.type_table_mut(dir).get_mut(&key) { cell.root |= root; - return Ok(()); + return; + } + self.type_table_mut(dir).insert( + key, + TypeCell { + subject: Box::new(reading.clone()), + root, + entry: None, + }, + ); + } + + /// Classify a **spelling** and register it — the one door for a type that + /// has no reading yet, and the only fallible way into the table. + /// + /// Everything the pipeline composes or walks already holds a + /// [`TypeRef`](crate::api::core::flat::TypeRef) and goes through + /// [`ensure_entry`](Self::ensure_entry) instead. What genuinely arrives as + /// tokens is a spelling *authored outside the model*: a build script's + /// declared crossing, a constant's declared type, a `syn` type the plan + /// engines assemble for their own wire shape. + /// + /// A spelling the grammar refuses is reported by name here, rather than + /// becoming a cell that quietly means less than its neighbours. + /// + /// **`pub(in crate::api::core)` deliberately**, matching + /// `Flat::classify` and the `TypeRef` composers. Classifying a spelling + /// *mints a reading*, and #280 sealed that to `api::core`: an adapter under + /// `api::lang` must not be able to hand the registry tokens of its own and + /// receive a `TypeRef` back. A one-door design that widened the door would + /// have re-opened exactly the capability #280 closed — so this must stay no + /// wider than the composers it replaces as an entry point. + pub(in crate::api::core) fn intern( + &mut self, + dir: Direction, + ty: &syn::Type, + root: bool, + ) -> Result { + // The registry's own answer first, in EITHER direction — a reading is + // direction-free, and a cell that exists already holds the authoritative + // one. Classifying anyway would derive a second reading for a key that + // has one, which `ensure_entry` would then discard: the same + // two-answers-that-never-meet shape this PR removes, surviving as + // redundant work rather than as a replaced cell. + let key = TypeKey::from_type(ty); + if let Some(known) = self + .input_types + .get(&key) + .or_else(|| self.output_types.get(&key)) + .map(|c| (*c.subject).clone()) + { + self.ensure_entry(dir, &known, root); + return Ok(known); } - let subject = self + let reading = self .flat .classify(ty) .map_err(|source| ScanError::NotExpressible { @@ -290,14 +342,24 @@ impl Registry { location: SourceLocation::default(), }], })?; - self.type_table_mut(dir).insert( - key, - TypeCell { - subject: Box::new(subject), - root, - entry: None, - }, - ); + self.ensure_entry(dir, &reading, root); + Ok(reading) + } + + /// [`intern`](Self::intern), then register every nested position — the + /// recursive door, for a spelling whose children must become crossings too + /// (a parameter, a return, a declared field). + /// + /// Only the top type is classified: the walk below it takes each child's + /// reading off the parent's, so nothing under here is re-derived. + pub(super) fn intern_recursive( + &mut self, + dir: Direction, + ty: &syn::Type, + root: bool, + ) -> Result<(), ScanError> { + let reading = self.intern(dir, ty, root)?; + self.register_type_recursive(dir, &reading, root); Ok(()) } @@ -327,10 +389,10 @@ impl Registry { &self, dir: Direction, ty: &syn::Type, - ) -> Vec<(Direction, syn::Type)> { + ) -> Vec<(Direction, crate::api::core::flat::TypeRef)> { use crate::api::core::flat::TypeKind; - let mut out: Vec<(Direction, syn::Type)> = Vec::new(); + let mut out: Vec<(Direction, crate::api::core::flat::TypeRef)> = Vec::new(); if let Some(reading) = self .type_table(dir) .get(&TypeKey::from_type(ty)) @@ -353,8 +415,12 @@ impl Registry { | TypeKind::Str | TypeKind::Unit => (Vec::new(), dir), }; + // The child reading itself, not its spelling: it has already been + // classified — by the model, or by whoever composed the parent — so + // handing back tokens for the caller to re-classify is the discard + // this walk exists to avoid (#281). for child in children { - out.push((child_dir, child.syntax().clone())); + out.push((child_dir, child.clone())); } } // A declared type's own fields, read off the element rather than off its @@ -387,7 +453,7 @@ impl Registry { Some(Type::Enum(_) | Type::Extern(_)) | None => Vec::new(), }; for field in fields { - out.push((dir, field.ty.syntax().clone())); + out.push((dir, field.ty.clone())); } } out @@ -409,7 +475,7 @@ impl Registry { root: bool, entry: Option>, ) { - self.ensure_entry(dir, &key.to_type(), root) + self.intern(dir, &key.to_type(), root) .unwrap_or_else(|e| panic!("fixture key `{key}` is not expressible: {e}")); self.type_table_mut(dir) .get_mut(key) @@ -444,21 +510,24 @@ impl Registry { .map(|cell| (*cell.subject).clone()) } - /// Register `ty` (and its nested positions) as a required **input** so + /// Register `reading` (and its nested positions) as a required **input** so /// the resolver produces a converter for it. Used by /// [`crate::api::core::expand`] to pull in the leaf types a fold needs. - pub(crate) fn require_input(&mut self, ty: &syn::Type) { - // Leaf/expansion types are concrete (no disallowed `impl Trait`), so - // the recursive registration cannot fail here. - let _ = self.register_type_recursive(Direction::Input, ty, true); + /// + /// Takes the **reading**, not its spelling. Every caller already holds one — + /// a plan leaf's `ty` — and used to call `.syntax()` on it here, which is the + /// discard #281 is about: the registry would then re-classify the tokens and + /// store its own answer beside the caller's. + pub(crate) fn require_input(&mut self, reading: &crate::api::core::flat::TypeRef) { + self.register_type_recursive(Direction::Input, reading, true); } /// Register `ty` (and its nested positions) as a required **output** so the /// resolver produces a converter for it. The output-side peer of /// [`Self::require_input`]; used by [`crate::api::core::unfold`] to pull in /// the leaf types a decomposition delivers. - pub(crate) fn require_output(&mut self, ty: &syn::Type) { - let _ = self.register_type_recursive(Direction::Output, ty, true); + pub(crate) fn require_output(&mut self, reading: &crate::api::core::flat::TypeRef) { + self.register_type_recursive(Direction::Output, reading, true); } /// Drop `ty` from the required-output scan set. The type's table entry is @@ -470,25 +539,20 @@ impl Registry { /// the whole-collection converter is genuinely not needed — and for a /// `Vec` it cannot resolve at all (a `jlong` wire is not /// JObject-shaped), so requiring it would wrongly fail resolution. - pub(crate) fn unrequire_output(&mut self, ty: &syn::Type) { - self.clear_root(Direction::Output, ty); - } - - /// Drop `ty` from the required-input scan set — the input-side peer of - /// [`Self::unrequire_output`]. Used by [`Self::apply_adapter_plans`] for - /// the adapter's boundary-only types: a fold plan replaces every direct - /// crossing of the type with its ingredients, so the type's own input - /// converter is genuinely not needed (and for an undeclared type cannot - /// resolve at all). - pub(crate) fn unrequire_input(&mut self, ty: &syn::Type) { - self.clear_root(Direction::Input, ty); + pub(crate) fn unrequire_output(&mut self, reading: &crate::api::core::flat::TypeRef) { + self.clear_root(Direction::Output, &reading.key()); } - /// Stop treating `ty` as a root. The cell stays, so the resolver still fills - /// it if it can — only the demand that it *must* resolve is dropped. - pub(super) fn clear_root(&mut self, dir: Direction, ty: &syn::Type) { - let key = TypeKey::from_type(ty); - if let Some(cell) = self.type_table_mut(dir).get_mut(&key) { + /// Stop treating `key` as a root. The cell stays, so the resolver still + /// fills it if it can — only the demand that it *must* resolve is dropped. + /// + /// Keyed, because that is genuinely all this needs: un-requiring creates no + /// cell and classifies nothing, so it is the one registration-adjacent + /// operation with no reading to carry. `unrequire_*` take a `TypeRef` anyway + /// — they pair with `require_*`, and a caller holding one should not have to + /// know which of the two wants a key. + pub(super) fn clear_root(&mut self, dir: Direction, key: &TypeKey) { + if let Some(cell) = self.type_table_mut(dir).get_mut(key) { cell.root = false; } } diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 3b2b6ac6..67e32606 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -819,6 +819,81 @@ fn a_source_type_cell_carries_the_models_typeref() { assert!(matches!(inner.subject.kind(), TypeKind::Scalar(_))); } +/// A **composed** reading reaches the cell intact, rather than being thrown away +/// and re-derived from its spelling. +/// +/// #281's acceptance test, and it needs a case where the two answers visibly +/// differ or it would pass on a coincidence. `Option` is a spelling the +/// source never writes, so: +/// +/// * composing it keeps the **source location** of the `Thing` it layers over — +/// the composer pairs `kind` with spelling and inherits the place; +/// * re-deriving it hands the tokens to `Flat::classify`, which finds nothing in +/// the model index for that spelling and builds a **placeless** reading. +/// +/// So the location is the discriminator, and it is not decorative: it is what a +/// diagnostic about this crossing prints. Verified to fail with the fix +/// reverted. +/// +/// Nothing in the tree could have caught the old path: the composed reading +/// lived in the plan leaf, the re-derived one lived in the cell, and the two +/// were never compared. Byte-identical goldens say the answers agree for every +/// type the examples exercise; this says the mechanism no longer permits them to +/// differ. +#[test] +fn a_composed_reading_reaches_the_cell_unchanged() { + use crate::api::core::flat::TypeKind; + + let loc = SourceLocation { + file: "src/lib.rs".into(), + line: 11, + column: 3, + crate_name: Some("myflat".into()), + }; + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::parse_str("pub struct Thing { pub v: u64 }").unwrap(), + loc.clone(), + ), + ( + syn::parse_str("pub fn f(t: Thing) -> u64 { t.v }").unwrap(), + loc.clone(), + ), + ]; + let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); + let mut ext = StubExt::default(); + ext.functions.insert(syn::parse_str("f").unwrap()); + ext.types.insert(TypeKey::parse("Thing").unwrap()); + let mut reg = ext + .declare_into_any(reg) + .expect("declare") + .scanned() + .unwrap(); + + // The source's own reading for `Thing`, which carries where it was written. + let thing = reg + .reading(&syn::parse_quote!(Thing)) + .expect("the declared struct is registered"); + assert_eq!(thing.location(), &loc, "fixture precondition"); + + // `Option` — composed here, written nowhere. + let composed = thing.optional(); + assert!(matches!(composed.kind(), TypeKind::Optional(_))); + assert_eq!(composed.location(), &loc, "the layer inherits the place"); + + reg.require_output(&composed); + + let cell = ®.output_types[&composed.key()]; + assert!(matches!(cell.subject.kind(), TypeKind::Optional(_))); + assert_eq!( + cell.subject.location(), + &loc, + "the cell holds the reading that was handed to it. A re-derivation would \ + classify the `Option` tokens, find no such spelling in the model \ + index, and store a PLACELESS reading instead" + ); +} + /// `Registry::reading` is a **lookup**. A type with no cell answers `None`, even /// when the grammar would classify it happily. /// @@ -1658,7 +1733,7 @@ fn a_recursive_type_is_handed_out_once_and_terminates() { next.extend( reg.immediate_edges(Direction::Output, &t) .into_iter() - .map(|(_, sub)| sub), + .map(|(_, sub)| sub.syntax().clone()), ); } if revisited { diff --git a/prebindgen/src/api/core/resolve.rs b/prebindgen/src/api/core/resolve.rs index 4127a258..a5c1b894 100644 --- a/prebindgen/src/api/core/resolve.rs +++ b/prebindgen/src/api/core/resolve.rs @@ -132,7 +132,7 @@ fn collect_unresolved_descendants( seen: &mut std::collections::HashSet<(Direction, TypeKey)>| { let ty = key.to_type(); for (child_dir, sub) in registry.immediate_edges(dir, &ty) { - let dep = (child_dir, TypeKey::from_type(&sub)); + let dep = (child_dir, sub.key()); if seen.insert(dep.clone()) { queue.push_back(dep); } diff --git a/prebindgen/src/api/core/resolve/tests.rs b/prebindgen/src/api/core/resolve/tests.rs index a30b3d1b..7b4a12ce 100644 --- a/prebindgen/src/api/core/resolve/tests.rs +++ b/prebindgen/src/api/core/resolve/tests.rs @@ -17,7 +17,14 @@ fn final_invariant_reports_unresolved_field_of_unresolved_struct() { // under test is one the pipeline can actually produce. let mut reg: Registry<()> = crate::api::test_util::scanned_with(&["pub struct Outer { pub inner: ZKeyExpr }"]); - reg.require_input(&syn::parse_quote!(Outer)); + let outer = reg + .intern( + crate::api::core::registry::Direction::Input, + &syn::parse_quote!(Outer), + true, + ) + .expect("fixture type"); + reg.require_input(&outer); let zke_key = TypeKey::parse("ZKeyExpr").expect("test type"); assert!(!reg.input_types[&zke_key].root, "the field is not a root"); diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index de48489d..efc8b7da 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -462,7 +462,7 @@ pub fn apply( continue; } for leaf in &plan.leaves { - registry.require_output(leaf.out_ty.syntax()); + registry.require_output(&leaf.out_ty); } registry.callback_arg_plans.insert(key, plan); } @@ -643,7 +643,7 @@ fn wire_fixed_returns( } } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(leaf.out_ty.syntax()); + registry.require_output(&leaf.out_ty); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -710,7 +710,7 @@ fn wire_fixed_callbacks( continue; } for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(leaf.out_ty.syntax()); + registry.require_output(&leaf.out_ty); } let plan = UnfoldPlan { source: vd.source.clone(), @@ -779,7 +779,7 @@ pub fn apply_leaf_vec_folds( } else { inner_shape }; - registry.require_output(vec_elem.syntax()); + registry.require_output(vec_elem); // The fold delivers the return element-by-element, so the // whole `Vec` / `Option>` converter is not needed. // De-require it: for String / scalar elements it still @@ -787,7 +787,7 @@ pub fn apply_leaf_vec_folds( // opaque-handle element it cannot resolve (`jlong` wire isn't // JObject-shaped), and de-requiring keeps that `None` from // being flagged as an unresolved-required error. - registry.unrequire_output(ret.syntax()); + registry.unrequire_output(&ret); registry .unfold_plans .insert(func.clone(), whole_leaf_fold_plan(vec_elem, shape)); @@ -814,7 +814,7 @@ pub fn apply_leaf_vec_folds( if registry.callback_arg_plans.contains_key(&key) { continue; } - registry.require_output(elem.syntax()); + registry.require_output(elem); let plan = whole_leaf_fold_plan(elem, UnfoldShape::Iterable(Box::new(UnfoldShape::Base))); registry.callback_arg_plans.insert(key, plan); @@ -905,7 +905,7 @@ struct Layered { /// The arity layers, outermost first. shape: UnfoldShape, /// Every type on the way down, outermost first — what a registration walks. - layer_types: Vec, + layer_types: Vec, /// Past the borrow too: what actually crosses. core: syn::Type, /// Whether the core is reached through a borrow. @@ -924,11 +924,7 @@ fn peel(ty: &crate::api::core::flat::TypeRef) -> Layered { let borrowed = layered.borrow_target(); Layered { shape, - layer_types: ty - .layer_types() - .iter() - .map(|t| t.syntax().clone()) - .collect(), + layer_types: ty.layer_types().into_iter().cloned().collect(), core: borrowed.unwrap_or(layered).syntax().clone(), by_ref: borrowed.is_some(), } @@ -1024,9 +1020,9 @@ fn process_decl( // recursive registration also required) — same reasoning as // [`apply_leaf_vec_folds`] for the fixed folds. if ed.target == DeconTarget::Output { - registry.unrequire_output(ret_ty.syntax()); + registry.unrequire_output(&ret_ty); if optional { - registry.unrequire_output(after_opt.syntax()); + registry.unrequire_output(after_opt); } } // Element type peeled of a leading `&` (accessors take `&Element`). @@ -1039,7 +1035,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, element)?; let plan = build_plan(acc, registry, ed, by_ref, element, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(leaf.out_ty.syntax()); + registry.require_output(&leaf.out_ty); } plan } else { @@ -1048,7 +1044,7 @@ fn process_decl( // No declaration is involved (`decon: None`) — the element // crosses whole through its own converter. let by_ref = peel_borrow(inner).0; - registry.require_output(inner.syntax()); + registry.require_output(inner); UnfoldPlan { source: inner.syntax().clone(), decon: None, @@ -1085,7 +1081,7 @@ fn process_decl( register_decon_spec(registry, acc, &decon, &records, source)?; let plan = build_plan(acc, registry, ed, by_ref, source, shape, &records, decon)?; for leaf in &plan.leaves { - registry.require_output(leaf.out_ty.syntax()); + registry.require_output(&leaf.out_ty); } plan }; @@ -1113,16 +1109,19 @@ fn process_decl( && plan.leaves.len() == 1 && !plan.leaves[0].nullable; let plan = if single_return { - let leaf_ty = plan.leaves[0].out_ty.syntax().clone(); - let cv_ty: syn::Type = if matches!(plan.shape, UnfoldShape::Optional((), _)) { - syn::parse_quote!(Option<#leaf_ty>) + // Composed with the model's own layering rather than by spelling + // `Option<#leaf_ty>` and handing the tokens over: `optional()` pairs + // the `kind` with its spelling in one place, so the reading that + // reaches the table is the one this plan carries (#281). + let cv = if matches!(plan.shape, UnfoldShape::Optional((), _)) { + plan.leaves[0].out_ty.optional() } else { - leaf_ty + plan.leaves[0].out_ty.clone() }; - registry.require_output(&cv_ty); + registry.require_output(&cv); UnfoldPlan { delivery: Delivery::Return, - convert_out_ty: Some(cv_ty), + convert_out_ty: Some(cv.syntax().clone()), ..plan } } else { diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index 162df0b0..11846a87 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -1813,7 +1813,10 @@ fn sum_return_layers_ride_the_shape_fold() { fn a_vec_only_sum_return_drops_the_bare_requirement() { let mut reg: Registry<()> = reg_with(&["fn read_all(n: i32) -> Vec { todo!() }"]); let bare: syn::Type = syn::parse_quote!(Reading); - reg.require_output(&bare); + let bare_reading = reg + .intern(crate::api::core::registry::Direction::Output, &bare, true) + .expect("fixture type"); + reg.require_output(&bare_reading); assert!( reg.output_types[&TypeKey::from_type(&bare)].root, "fixture precondition: the bare element starts out required" From b40728424164264be49e675b898ce6b80f75cdaf Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 16:51:47 +0200 Subject: [PATCH 33/52] jnigen: the wrapper-shape handlers take the inner type as a reading (#285) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First of #284's three steps, and it is a PREREQUISITE rather than a win on its own — stated plainly because the numbers say so: .syntax() calls 19 -> 24 (UP: selector 15 -> 15, trait_impl 4 -> 9) boundary ledger 127 -> 127 (unchanged) My own plan predicted the ledger would fall here. It does not, and reading the sites says why: selector's 3 and trait_impl's 11 counted matches are `is_unsized_spelling`, `decoded_vec_satisfies` and the `Type::Reference` bridgeability guards — all genuine SPELLING questions, and the documented exemption. Nothing was owed there. What it does change is the type of the inner parameter. `input_wrapper_shape` / `output_wrapper_shape` and their four sub-handlers took `t1: &syn::Type`, and `selector.rs` produced it by destructuring a reading it already held. Now `t1` is a `&TypeRef`, so a handler cannot be reached with tokens that have no reading, and #284's step 2 has something to pass to `input_entry`/`output_entry` when those take a reading. `produced` deliberately stays a `&syn::Type`, and the slice case is why: at `selector.rs`'s `&[T]` arm the adapter COMPOSES `Vec<#elem>`, and #280 sealed minting to the model — `api::lang` has no `Vec` reading to make. That turns out to be consistent rather than awkward: `produced` is defined as the tokens the converter yields, and every question asked of it (`is_canonical_spelling`, the `Type::Reference` guards) is a spelling question. So the split is meaningful — `produced` = what is emitted, `t1` = what is wrapped. Verified: 551 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p` (rebuild confirmed by the Compiling lines), covertest-kotlin 48/48. --- .../src/api/lang/jnigen/jni/selector.rs | 53 ++++++----- .../src/api/lang/jnigen/jni/trait_impl.rs | 93 +++++++++++-------- 2 files changed, 80 insertions(+), 66 deletions(-) diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index 5e151a36..9250211a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -72,14 +72,13 @@ impl Declarations { .. } ); - let t1 = target.syntax().clone(); if let Some(mut c) = self.input_wrapper_shape( WrapperShape::OptionRef { mutable }, syntax, - &t1, + target, registry, ) { - c.subs = vec![t1]; + c.subs = vec![target.syntax().clone()]; return Some(c); } } @@ -98,21 +97,19 @@ impl Declarations { return None; } } - let inner_ty = inner.syntax().clone(); if let Some(mut c) = - self.input_wrapper_shape(WrapperShape::Optional, syntax, &inner_ty, registry) + self.input_wrapper_shape(WrapperShape::Optional, syntax, inner, registry) { - c.subs = vec![inner_ty]; + c.subs = vec![inner.syntax().clone()]; return Some(c); } return None; } if let Some(elem) = ty.sequence_elem().filter(|_| !is_unsized_spelling(syntax)) { - let elem_ty = elem.syntax().clone(); if let Some(mut c) = - self.input_wrapper_shape(WrapperShape::Sequence, syntax, &elem_ty, registry) + self.input_wrapper_shape(WrapperShape::Sequence, syntax, elem, registry) { - c.subs = vec![elem_ty]; + c.subs = vec![elem.syntax().clone()]; return Some(c); } return None; @@ -144,13 +141,19 @@ impl Declarations { // The one place `produced` is NOT the crossing's spelling: // there is no owned `[T]` to decode into, so the converter // yields an owned `Vec` and the call site borrows it. + // + // It is also why `produced` stays a spelling while `t1` + // becomes a reading: this one is composed by the ADAPTER, + // and #280 sealed minting to the model — there is no + // `Vec` reading for `api::lang` to make. Which is + // consistent rather than awkward: `produced` is defined as + // the tokens the converter yields, and every question asked + // of it (`is_canonical_spelling`, the `Type::Reference` + // bridgeability guards) is a spelling question. let produced: syn::Type = syn::parse_quote!(Vec<#elem_ty>); - if let Some(mut c) = self.input_wrapper_shape( - WrapperShape::Sequence, - &produced, - &elem_ty, - registry, - ) { + if let Some(mut c) = + self.input_wrapper_shape(WrapperShape::Sequence, &produced, elem, registry) + { c.subs = vec![elem_ty]; return Some(c); } @@ -158,11 +161,10 @@ impl Declarations { } } let mutable = matches!(mode, RefMode::Exclusive); - let t1 = inner.syntax().clone(); if let Some(mut c) = - self.input_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, &t1, registry) + self.input_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, inner, registry) { - c.subs = vec![t1]; + c.subs = vec![inner.syntax().clone()]; return Some(c); } } @@ -204,21 +206,19 @@ impl Declarations { // whose inner converter is the `&Handle` borrow entry (no deep // output handler). if let Some(inner) = ty.optional_inner() { - let inner_ty = inner.syntax().clone(); if let Some(mut c) = - self.output_wrapper_shape(WrapperShape::Optional, syntax, &inner_ty, registry) + self.output_wrapper_shape(WrapperShape::Optional, syntax, inner, registry) { - c.subs = vec![inner_ty]; + c.subs = vec![inner.syntax().clone()]; return Some(c); } return None; } if let Some(elem) = ty.sequence_elem().filter(|_| !is_unsized_spelling(syntax)) { - let elem_ty = elem.syntax().clone(); if let Some(mut c) = - self.output_wrapper_shape(WrapperShape::Sequence, syntax, &elem_ty, registry) + self.output_wrapper_shape(WrapperShape::Sequence, syntax, elem, registry) { - c.subs = vec![elem_ty]; + c.subs = vec![elem.syntax().clone()]; return Some(c); } return None; @@ -235,11 +235,10 @@ impl Declarations { } } let mutable = matches!(mode, RefMode::Exclusive); - let t1 = inner.syntax().clone(); if let Some(mut c) = - self.output_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, &t1, registry) + self.output_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, inner, registry) { - c.subs = vec![t1]; + c.subs = vec![inner.syntax().clone()]; return Some(c); } } diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index e48fdf35..9b5fb4ee 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -775,9 +775,12 @@ impl Declarations { &self, shape: WrapperShape, produced: &syn::Type, - t1: &syn::Type, + t1: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + // `t1`'s spelling, for the parts that ask spelling questions; the + // READING stays in `t1` for the lookups (#284). + let t1_ty = t1.syntax(); let WrapperShape::Borrow { mutable } = shape else { return None; }; @@ -787,14 +790,14 @@ impl Declarations { // — `Box<&T>` — must not resolve here (it would pass an owned `T` where // `Box<&T>` is expected). let canonical: syn::Type = if mutable { - syn::parse_quote!(&mut #t1) + syn::parse_quote!(&mut #t1_ty) } else { - syn::parse_quote!(&#t1) + syn::parse_quote!(&#t1_ty) }; if !is_canonical_spelling(produced, &canonical) { return None; } - let inner = registry.input_entry(t1)?; + let inner = registry.input_entry(t1_ty)?; let outer_ty = produced.clone(); // `&T` / `&mut T` are Kotlin-side no-ops — inherit the inner // type's name, unless the user pinned an explicit override @@ -831,9 +834,12 @@ impl Declarations { &self, shape: WrapperShape, produced: &syn::Type, - t1: &syn::Type, + t1: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + // `t1`'s spelling, for the parts that ask spelling questions; the + // READING stays in `t1` for the lookups (#284). + let t1_ty = t1.syntax(); let WrapperShape::OptionRef { mutable } = shape else { return None; }; @@ -841,14 +847,14 @@ impl Declarations { // `.as_deref()` — again not the spelled type, so a wrapped spelling has // nothing to bridge and must not resolve. See `input_borrow`. let canonical: syn::Type = if mutable { - syn::parse_quote!(Option<&mut #t1>) + syn::parse_quote!(Option<&mut #t1_ty>) } else { - syn::parse_quote!(Option<&#t1>) + syn::parse_quote!(Option<&#t1_ty>) }; if !is_canonical_spelling(produced, &canonical) { return None; } - let inner = registry.input_entry(t1)?; + let inner = registry.input_entry(t1_ty)?; if !inner.metadata.is_direct_handle() { // Non-opaque: let the general `Option<_>` handler take it. return None; @@ -863,7 +869,7 @@ impl Declarations { pub(crate) unsafe fn #name<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &#inner_wire, - ) -> ::core::result::Result>, __JniErr> { + ) -> ::core::result::Result>, __JniErr> { Ok({ if *v == 0 { None } else { Some(#inner_conv(env, v)?) } }) @@ -899,14 +905,17 @@ impl Declarations { &self, shape: WrapperShape, produced: &syn::Type, - t1: &syn::Type, + t1: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + // `t1`'s spelling, for the parts that ask spelling questions; the + // READING stays in `t1` for the lookups (#284). + let t1_ty = t1.syntax(); if shape != WrapperShape::Sequence { return None; } - let inner = registry.input_entry(t1)?; - reject_vec_of_handle(&inner.metadata.projection, t1); + let inner = registry.input_entry(t1_ty)?; + reject_vec_of_handle(&inner.metadata.projection, t1_ty); let inner_wire = inner.destination.clone(); if !is_jobject_shaped_wire(&inner_wire) { return None; @@ -919,7 +928,7 @@ impl Declarations { quote::quote!(&__elem_wire), ); let outer_ty = produced.clone(); - let canonical: syn::Type = syn::parse_quote!(Vec<#t1>); + let canonical: syn::Type = syn::parse_quote!(Vec<#t1_ty>); // Bridgeable first — see `box_layers_to`. let build = build_from_canonical(produced, &canonical, quote::quote!(__out))?; let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); @@ -928,12 +937,12 @@ impl Declarations { .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-from-env: {}", e)))?; let mut __it = __list.iter(env) .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-iter: {}", e)))?; - let mut __out: Vec<#t1> = Vec::new(); + let mut __out: Vec<#t1_ty> = Vec::new(); while let Some(__obj) = __it.next(env) .map_err(|e| <__JniErr as ::core::convert::From>::from(format!("Vec<_>: list-next: {}", e)))? { let __elem_wire: #inner_wire = __obj.into(); - let __elem: #t1 = #inner_conv; + let __elem: #t1_ty = #inner_conv; __out.push(__elem); } #build @@ -967,15 +976,18 @@ impl Declarations { &self, shape: WrapperShape, produced: &syn::Type, - t1: &syn::Type, + t1: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + // `t1`'s spelling, for the parts that ask spelling questions; the + // READING stays in `t1` for the lookups (#284). + let t1_ty = t1.syntax(); if shape == WrapperShape::Optional { - let inner = registry.input_entry(t1)?; + let inner = registry.input_entry(t1_ty)?; if inner.metadata.is_direct_handle() { let inner_wire = inner.destination.clone(); let outer_ty = produced.clone(); - let canonical: syn::Type = syn::parse_quote!(Option<#t1>); + let canonical: syn::Type = syn::parse_quote!(Option<#t1_ty>); let build = build_from_canonical(produced, &canonical, quote::quote!(__v))?; let name = input_name(&outer_ty, &inner_wire); let gen_allow = generated_converter_attr(); @@ -986,7 +998,7 @@ impl Declarations { v: &#inner_wire, ) -> ::core::result::Result<#outer_ty, __JniErr> { Ok({ - let __v: ::core::option::Option<#t1> = if *v == 0 { + let __v: ::core::option::Option<#t1_ty> = if *v == 0 { None } else if (*v & 1) == 1 { // Tagged (closed) handle raced past the Kotlin @@ -998,7 +1010,7 @@ impl Declarations { ), ); } else { - Some(*std::boxed::Box::from_raw(*v as *mut #t1)) + Some(*std::boxed::Box::from_raw(*v as *mut #t1_ty)) }; #build }) @@ -1030,19 +1042,19 @@ impl Declarations { } if shape == WrapperShape::Optional { let outer_ty = produced.clone(); - let canonical: syn::Type = syn::parse_quote!(Option<#t1>); + let canonical: syn::Type = syn::parse_quote!(Option<#t1_ty>); let build = build_from_canonical(produced, &canonical, quote::quote!(__v))?; - let (wire, inner_body, niches) = option_input(t1, registry)?; + let (wire, inner_body, niches) = option_input(t1_ty, registry)?; // `option_input` yields the canonical `Option`; the converter // yields the spelling. let body: syn::Expr = syn::parse_quote!({ - let __v: ::core::option::Option<#t1> = #inner_body; + let __v: ::core::option::Option<#t1_ty> = #inner_body; #build }); // Inherit the inner's name; user pins on `Option` win. // The nullability marker (`?`) is added by the use site. let inherited = registry - .input_entry(t1) + .input_entry(t1_ty) .and_then(|e| e.metadata.kotlin_name.clone()); let kotlin_name = self.override_kotlin_name(&outer_ty, inherited); // Fold a Nullable layer over the inner projection (if any). The @@ -1050,9 +1062,9 @@ impl Declarations { // an inner niche, the wire stays identical to the inner's // destination and `None` is the niche slot sentinel; the boxed // fallback widens the wire to `JObject`. - let nullable_kind = nullable_kind_for(&wire, t1, registry); + let nullable_kind = nullable_kind_for(&wire, t1_ty, registry); let projection = registry - .input_entry(t1) + .input_entry(t1_ty) .and_then(|e| e.metadata.projection.clone()) .map(|h| Projection { strategy: FoldStrategy::Optional(nullable_kind, Box::new(h.strategy)), @@ -1998,7 +2010,7 @@ impl Declarations { &self, shape: WrapperShape, produced: &syn::Type, - t1: &syn::Type, + t1: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { // Disjoint shapes (see [`WrapperShape`]), tried in priority order. The @@ -2181,9 +2193,12 @@ impl Declarations { &self, shape: WrapperShape, produced: &syn::Type, - t1: &syn::Type, + t1: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { + // `t1`'s spelling, for the parts that ask spelling questions; the + // READING stays in `t1` for the lookups (#284). + let t1_ty = t1.syntax(); // Borrowed opaque-handle output (`&T` / `&'static T` where `T` is a // declared opaque handle). Canonical zenoh-flat's `z_*` accessors // return *borrowed* handles for the C tier's zero-copy borrows, but @@ -2197,11 +2212,11 @@ impl Declarations { if r.mutability.is_none() && self .types - .get(&TypeKey::from_type(t1)) + .get(&TypeKey::from_type(t1_ty)) .is_some_and(|c| c.is_opaque()) { let mut ref_ty = r.clone(); - *ref_ty.elem = t1.clone(); + *ref_ty.elem = t1_ty.clone(); let outer_ty = syn::Type::Reference(ref_ty); let wire: syn::Type = syn::parse_quote!(jni::sys::jlong); let body: syn::Expr = syn::parse_quote!(std::boxed::Box::into_raw( @@ -2213,7 +2228,7 @@ impl Declarations { destination: wire, pre_stages: vec![], niches: Niches::one(syn::parse_quote!(0i64), syn::parse_quote!(*v == 0)), - metadata: self.opaque_leaf_meta(t1), + metadata: self.opaque_leaf_meta(t1_ty), }); } } @@ -2223,7 +2238,7 @@ impl Declarations { // intermediate owned `String`). The unsized `str` sub resolves via the // rank-0 arm to the same fn (see [`Self::str_ref_output`]). if let syn::Type::Reference(r) = produced { - if r.mutability.is_none() && TypeKey::from_type(t1).as_str() == "str" { + if r.mutability.is_none() && TypeKey::from_type(t1_ty).as_str() == "str" { return Some(self.str_ref_output()); } } @@ -2232,17 +2247,17 @@ impl Declarations { // `.throwable()`. if shape == WrapperShape::Optional { let outer_ty = produced.clone(); - let canonical: syn::Type = syn::parse_quote!(Option<#t1>); + let canonical: syn::Type = syn::parse_quote!(Option<#t1_ty>); // Bridgeable first: an unsupported representation must not resolve // and then emit code the consumer cannot compile. let read = read_as_canonical(produced, &canonical)?; - let (wire, inner_body, niches) = option_output(t1, registry)?; + let (wire, inner_body, niches) = option_output(t1_ty, registry)?; let body: syn::Expr = syn::parse_quote!({ let v: #canonical = #read; #inner_body }); let inherited = registry - .output_entry(t1) + .output_entry(t1_ty) .and_then(|e| e.metadata.kotlin_name.clone()); let kotlin_name = self.override_kotlin_name(&outer_ty, inherited); // Fold a Nullable layer over the inner projection (if any). The @@ -2250,9 +2265,9 @@ impl Declarations { // [`nullable_kind_for`]): niche-fulfilled keeps the inner wire // and treats the slot value as `None`; boxed widens to `JObject` // and uses JVM null. - let nullable_kind = nullable_kind_for_output(&wire, t1, registry); + let nullable_kind = nullable_kind_for_output(&wire, t1_ty, registry); let projection = registry - .output_entry(t1) + .output_entry(t1_ty) .and_then(|e| e.metadata.projection.clone()) .map(|h| Projection { strategy: FoldStrategy::Optional(nullable_kind, Box::new(h.strategy)), @@ -2284,7 +2299,7 @@ impl Declarations { // Symmetric to the input handler. `Vec` is special-cased at // rank-0 (primitive_output → JByteArray) so rank-1 never sees it. if shape == WrapperShape::Sequence { - let inner = registry.output_entry(t1)?; + let inner = registry.output_entry(t1_ty)?; // `Vec` output is delivered by the Kotlin-side leaf // fold (`apply_leaf_vec_folds` → typed-handle wrap), so this // whole-`ArrayList` converter is bypassed for it. A handle's `jlong` @@ -2300,7 +2315,7 @@ impl Declarations { quote::quote!(__elem), ); let outer_ty = produced.clone(); - let canonical: syn::Type = syn::parse_quote!(Vec<#t1>); + let canonical: syn::Type = syn::parse_quote!(Vec<#t1_ty>); let read = read_as_canonical(produced, &canonical)?; let wire: syn::Type = syn::parse_quote!(jni::objects::JObject); let body: syn::Expr = syn::parse_quote!({ From 995d110adc45fabbb538072498b14cfbb88d900e Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 16:57:17 +0200 Subject: [PATCH 34/52] core: registry lookups take the reading, not a spelling (#286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Second of #284's three steps, and the one that pays for the first. reading(&TypeKey) the ONE keyed door conversion/input_entry/output_entry take &TypeRef reading_of(&syn::Type) the visible "I only had tokens" step The guarantee: an entry lookup cannot be called about a type the registry does not know. #280 sealed minting, so a `TypeRef` can only come from the model or from `reading`/`reading_of` — and those answer `None` for an unregistered type, which the caller must now handle. Before, any tokens could be passed and got a silent `None` back. The important find was NOT in the trait. `Registry` carried INHERENT `input_entry`/`output_entry` taking a `&syn::Type` (`scan.rs:579/585`), and an inherent method wins over a trait method on a concrete receiver — so every caller holding a `Registry` used the spelling door and the trait's signature could not close it. Changing the trait alone left 104 sites silently compiling against the old path; closing the inherent pair is what surfaced them. Same "second door inside the room" that hid `classify` behind `Registry::reading` until #267, and it is the reason this PR is larger than the plan predicted. PR #285's payoff lands here: every `t1_ty` in the wrapper-shape handlers became `t1`, because the handler already holds the reading. Honest numbers: to_type() (prod) 45 -> 37 boundary ledger 127 -> 127 (unchanged, and structurally so — it counts syn variant mentions, and a signature change names none) The 37 that remain are spelling needs — C type names, `quote!` targets, diagnostics. NONE feeds a lookup, which is this issue's acceptance test: `grep to_type() | grep -E "input_entry|output_entry|conversion|reading"` is empty. `reading_of` is deliberately not a convenience wrapper for the entry lookups. It returns a reading, so the `None` stays visible at the call site; a spelling-taking `entry_of` would have restored exactly the door being removed. Verified: 551 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p`, covertest-kotlin 48/48. --- prebindgen/src/api/core/registry/declare.rs | 12 +- prebindgen/src/api/core/registry/scan.rs | 36 +++-- prebindgen/src/api/core/registry/tests.rs | 6 +- prebindgen/src/api/core/registry/view.rs | 71 ++++++--- prebindgen/src/api/lang/cbindgen/emit.rs | 136 +++++++++++------- .../src/api/lang/cbindgen/trait_impl.rs | 93 +++++++++--- prebindgen/src/api/lang/jnigen/jni/builder.rs | 13 +- .../src/api/lang/jnigen/jni/emit/callback.rs | 11 +- .../src/api/lang/jnigen/jni/emit/convert.rs | 14 +- .../src/api/lang/jnigen/jni/emit/delivery.rs | 34 +++-- .../api/lang/jnigen/jni/emit/flat_input.rs | 73 +++++++--- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 18 ++- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 42 +++--- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 20 ++- prebindgen/src/api/lang/jnigen/jni/iface.rs | 20 ++- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 25 ++-- .../src/api/lang/jnigen/jni/overloads.rs | 3 +- prebindgen/src/api/lang/jnigen/jni/render.rs | 5 +- prebindgen/src/api/lang/jnigen/jni/report.rs | 3 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 10 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 43 +++--- 21 files changed, 467 insertions(+), 221 deletions(-) diff --git a/prebindgen/src/api/core/registry/declare.rs b/prebindgen/src/api/core/registry/declare.rs index 17953371..0534f365 100644 --- a/prebindgen/src/api/core/registry/declare.rs +++ b/prebindgen/src/api/core/registry/declare.rs @@ -427,14 +427,18 @@ impl RegistryBuilder { /// /// [`conversion`]: Conversions::conversion impl Conversions for RegistryBuilder { - fn reading(&self, ty: &syn::Type) -> Option { - self.registry.reading(ty) + fn reading(&self, key: &TypeKey) -> Option { + self.registry.reading(key) } fn flat(&self) -> &crate::api::core::flat::Flat { &self.registry.flat } - fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { - self.built.get(&(dir, TypeKey::from_type(ty))) + fn conversion( + &self, + dir: Direction, + reading: &crate::api::core::flat::TypeRef, + ) -> Option<&TypeEntry> { + self.built.get(&(dir, reading.key())) } fn crossing_keys(&self, dir: Direction) -> Vec { self.order diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index d1e466ae..4f3e8021 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -502,11 +502,10 @@ impl Registry { /// /// `None` therefore means the type never entered the pipeline — a caller /// asking out of order, not a cache miss to paper over. - pub(crate) fn reading(&self, ty: &syn::Type) -> Option { - let key = TypeKey::from_type(ty); + pub(crate) fn reading(&self, key: &TypeKey) -> Option { self.input_types - .get(&key) - .or_else(|| self.output_types.get(&key)) + .get(key) + .or_else(|| self.output_types.get(key)) .map(|cell| (*cell.subject).clone()) } @@ -573,18 +572,31 @@ impl Registry { } } - /// Look up the resolved input entry for `ty`, returning `None` if it + /// Look up the resolved input entry for `reading`, returning `None` if it /// was never registered or is still unresolved. The returned entry's /// `function.sig.ident` is the converter's call name; `destination` is /// its wire form. - pub fn input_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { - let key = TypeKey::from_type(ty); - self.type_table(Direction::Input).get(&key)?.entry.as_ref() + /// + /// Takes a `TypeRef` for the reason the trait methods do (#284) — and this + /// pair matters more than they do, because an **inherent** method wins over + /// a trait method on a concrete `Registry`. While these took a spelling they + /// were a second door into the table that the trait's signature could not + /// close, and every caller with a `Registry` in hand silently used it. The + /// same "second door inside the room" that hid `classify` behind + /// `Registry::reading` until #267. + pub fn input_entry(&self, reading: &crate::api::core::flat::TypeRef) -> Option<&TypeEntry> { + self.type_table(Direction::Input) + .get(&reading.key())? + .entry + .as_ref() } - /// Look up the resolved output entry for `ty`. See [`Self::input_entry`]. - pub fn output_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { - let key = TypeKey::from_type(ty); - self.type_table(Direction::Output).get(&key)?.entry.as_ref() + /// Look up the resolved output entry for `reading`. See + /// [`Self::input_entry`]. + pub fn output_entry(&self, reading: &crate::api::core::flat::TypeRef) -> Option<&TypeEntry> { + self.type_table(Direction::Output) + .get(&reading.key())? + .entry + .as_ref() } } diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 67e32606..4ad9db6f 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -872,7 +872,7 @@ fn a_composed_reading_reaches_the_cell_unchanged() { // The source's own reading for `Thing`, which carries where it was written. let thing = reg - .reading(&syn::parse_quote!(Thing)) + .reading(&TypeKey::parse("Thing").unwrap()) .expect("the declared struct is registered"); assert_eq!(thing.location(), &loc, "fixture precondition"); @@ -922,13 +922,13 @@ fn reading_is_a_lookup_not_a_classification() { // Registered by the declared fn — the lookup hits. assert!( - reg.reading(&syn::parse_quote!(u64)).is_some(), + reg.reading(&TypeKey::parse("u64").unwrap()).is_some(), "a type the scan registered has its reading in a cell" ); // Never registered, and perfectly expressible. The grammar's answer is not // this method's to give. assert!( - reg.reading(&syn::parse_quote!(i64)).is_none(), + reg.reading(&TypeKey::parse("i64").unwrap()).is_none(), "`reading` answers from the type table; it must not classify on a miss" ); } diff --git a/prebindgen/src/api/core/registry/view.rs b/prebindgen/src/api/core/registry/view.rs index a6f13b2d..65ffbb68 100644 --- a/prebindgen/src/api/core/registry/view.rs +++ b/prebindgen/src/api/core/registry/view.rs @@ -35,19 +35,49 @@ pub trait Conversions { /// rebuilding a spelling from the key and classifying that — the round trip /// `api/core` removed from itself in #263, which is the same defect one layer /// out. - fn reading(&self, ty: &syn::Type) -> Option; + /// + /// **Keyed**, because that is the one thing a caller has before it has a + /// reading. This is the door FROM identity TO the model's answer, and the + /// only lookup on this trait that does not already take a `TypeRef` — the + /// rest take one precisely because this exists to hand them one (#284). + fn reading(&self, key: &TypeKey) -> Option; - /// The conversion for `ty` in `dir`, if there is one. - fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry>; + /// The conversion for `reading` in `dir`, if there is one. + /// + /// Takes the **reading**, not a spelling. A caller that has to ask what a + /// type converts to has already established what the type *is*; asking with + /// tokens instead let a spelling nobody classified reach the table, and cost + /// a `TypeKey::from_type` on every call for an identity the reading already + /// carries. + fn conversion( + &self, + dir: Direction, + reading: &crate::api::core::flat::TypeRef, + ) -> Option<&TypeEntry>; + + /// The reading for a **spelling** — identify, then look up. + /// + /// The door for a caller holding tokens it peeled or composed itself, which + /// is a real position: an adapter may strip a `&` or name a wire type, and + /// #280 sealed minting so it cannot make a reading for the result. It asks + /// instead, and `None` means the registry never saw that type. + /// + /// Kept separate from the entry lookups on purpose. Those take a `TypeRef`, + /// so they cannot be called about a type the registry does not know — which + /// is the guarantee, and it survives only while getting a reading from + /// tokens is a visible step with a `None` to handle. + fn reading_of(&self, ty: &syn::Type) -> Option { + self.reading(&TypeKey::from_type(ty)) + } /// Wire → rust. - fn input_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { - self.conversion(Direction::Input, ty) + fn input_entry(&self, reading: &crate::api::core::flat::TypeRef) -> Option<&TypeEntry> { + self.conversion(Direction::Input, reading) } /// Rust → wire. - fn output_entry(&self, ty: &syn::Type) -> Option<&TypeEntry> { - self.conversion(Direction::Output, ty) + fn output_entry(&self, reading: &crate::api::core::flat::TypeRef) -> Option<&TypeEntry> { + self.conversion(Direction::Output, reading) } /// The decomposition of a callback argument type, if it has one. @@ -93,11 +123,15 @@ impl Conversions for Building<'_, M> { fn flat(&self) -> &crate::api::core::flat::Flat { &self.registry.flat } - fn reading(&self, ty: &syn::Type) -> Option { - self.registry.reading(ty) + fn reading(&self, key: &TypeKey) -> Option { + self.registry.reading(key) } - fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { - self.built.get(&(dir, TypeKey::from_type(ty))) + fn conversion( + &self, + dir: Direction, + reading: &crate::api::core::flat::TypeRef, + ) -> Option<&TypeEntry> { + self.built.get(&(dir, reading.key())) } fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan> { self.registry.callback_arg_plans.get(key) @@ -129,14 +163,15 @@ impl Conversions for Registry { fn flat(&self) -> &crate::api::core::flat::Flat { &self.flat } - fn reading(&self, ty: &syn::Type) -> Option { - Registry::reading(self, ty) + fn reading(&self, key: &TypeKey) -> Option { + Registry::reading(self, key) } - fn conversion(&self, dir: Direction, ty: &syn::Type) -> Option<&TypeEntry> { - self.type_table(dir) - .get(&TypeKey::from_type(ty))? - .entry - .as_ref() + fn conversion( + &self, + dir: Direction, + reading: &crate::api::core::flat::TypeRef, + ) -> Option<&TypeEntry> { + self.type_table(dir).get(&reading.key())?.entry.as_ref() } fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan> { self.callback_arg_plans.get(key) diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index c524f014..caa5281a 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -9,24 +9,31 @@ impl CbindgenBuilder { let string_ty: syn::Type = syn::parse_quote!(String); // A `String` return hands out a `char*` — unless `String` is declared // `opaque_ptr` (then it crosses as `string_t *`, freed by `string_drop`). - if registry.output_entry(&string_ty).is_some() + if registry + .reading_of(&string_ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_some() && !self.opaque.contains_key(&TypeKey::from_type(&string_ty)) { return true; } // Opaque error types are marshalled to a malloc'd `char*` message. - if self - .opaque_errors - .keys() - .any(|key| registry.output_entry(&key.to_type()).is_some()) - { + if self.opaque_errors.keys().any(|key| { + registry + .reading(key) + .and_then(|tr| registry.output_entry(&tr)) + .is_some() + }) { return true; } // A tagged union with a `String` payload hands out a `char*` per active // arm — allocated by its output converter, released by its typed drop. if self.tagged_unions.keys().any(|key| { let ty = key.to_type(); - registry.output_entry(&ty).is_some() + registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_some() && self .enum_variants(registry, &ty) .map(|vs| { @@ -40,7 +47,10 @@ impl CbindgenBuilder { } self.data.keys().any(|key| { let ty = key.to_type(); - registry.output_entry(&ty).is_some() + registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_some() && self .struct_fields(registry, &ty) .map(|fields| fields.iter().any(|(_, fty)| is_string(fty))) @@ -132,7 +142,7 @@ impl CbindgenBuilder { // legitimately differ (a `String`'s const-ness above), which is why a // disagreement is `None` — a rejection naming the payload — rather // than a silent pick of one side. - let out_entry = registry.output_entry(fty).ok_or_else(|| { + let out_entry = registry.reading_of(fty).and_then(|tr| registry.output_entry(&tr)).ok_or_else(|| { "no resolved OUTPUT converter — a payload crosses as its converter's destination, so \ it must be a scalar, a `String`, or a type this binding declares (`enum_type`, \ `data_struct`, `opaque_ptr`, or a `convert!` conversion)" @@ -149,7 +159,10 @@ impl CbindgenBuilder { ); } let out = out_entry.destination.clone(); - if let Some(inp) = registry.input_entry(fty) { + if let Some(inp) = registry + .reading_of(fty) + .and_then(|tr| registry.input_entry(&tr)) + { if TypeKey::from_type(&inp.destination) != TypeKey::from_type(&out) { return Err(format!( "its input and output converters disagree on the wire (`{}` in, `{}` out) \ @@ -264,7 +277,10 @@ impl CbindgenBuilder { /// anything that is not a declared tagged union. pub(super) fn tagged_union_has_drop(&self, fty: &syn::Type, registry: &Registry<()>) -> bool { if !self.tagged_unions.contains_key(&TypeKey::from_type(fty)) - || registry.output_entry(fty).is_none() + || registry + .reading_of(fty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() { return false; } @@ -447,7 +463,8 @@ impl CbindgenBuilder { return false; }; registry - .input_entry(&pt.ty) + .reading_of(&pt.ty) + .and_then(|tr| registry.input_entry(&tr)) .map(|e| returns_result(&e.function.sig.output)) .unwrap_or(false) }); @@ -470,13 +487,16 @@ impl CbindgenBuilder { TypeKey::from_type(err_ty), TypeKey::from_type(err_ty), ); - let entry = registry.output_entry(err_ty).unwrap_or_else(|| { - panic!( - "Cbindgen::on_function: error type `{}` of `{}` has no output converter", - TypeKey::from_type(err_ty), - orig - ) - }); + let entry = registry + .reading_of(err_ty) + .and_then(|tr| registry.output_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "Cbindgen::on_function: error type `{}` of `{}` has no output converter", + TypeKey::from_type(err_ty), + orig + ) + }); ( entry.destination.clone(), entry.function.sig.ident.clone(), @@ -665,12 +685,15 @@ impl CbindgenBuilder { (scalar, data struct, String, or handle), not a composite", TypeKey::from_type(&elem), ); - let entry = registry.output_entry(&elem).unwrap_or_else(|| { - panic!( - "Cbindgen: `Vec` element `{}` has no output converter", - TypeKey::from_type(&elem) - ) - }); + let entry = registry + .reading_of(&elem) + .and_then(|tr| registry.output_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "Cbindgen: `Vec` element `{}` has no output converter", + TypeKey::from_type(&elem) + ) + }); let elem_wire = entry.destination.clone(); return ValueShape { fields: vec![ @@ -689,12 +712,15 @@ impl CbindgenBuilder { // `Cow<'_, [T]>` → `T_wire* + size_t`. The C side receives an owned // malloc'd copy, just like `Vec` outputs. if let Some(elem) = cow_slice_elem(ty) { - let entry = registry.output_entry(&elem).unwrap_or_else(|| { - panic!( - "Cbindgen: `Cow` slice element `{}` has no output converter", - TypeKey::from_type(&elem) - ) - }); + let entry = registry + .reading_of(&elem) + .and_then(|tr| registry.output_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "Cbindgen: `Cow` slice element `{}` has no output converter", + TypeKey::from_type(&elem) + ) + }); let elem_wire = entry.destination.clone(); return ValueShape { fields: vec![ @@ -735,12 +761,15 @@ impl CbindgenBuilder { // Base value: one wire component from its rank-0/1 converter. Custom // conversions may declare scalar niches; otherwise a pointer wire // (String, opaque handle, `&'static`) carries a free NULL niche. - let entry = registry.output_entry(ty).unwrap_or_else(|| { - panic!( - "Cbindgen::on_function: type `{}` has no output converter", - TypeKey::from_type(ty) - ) - }); + let entry = registry + .reading_of(ty) + .and_then(|tr| registry.output_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "Cbindgen::on_function: type `{}` has no output converter", + TypeKey::from_type(ty) + ) + }); let wire = entry.destination.clone(); let niches = if entry.niches.is_empty() && matches!(wire, syn::Type::Ptr(_)) { let null = null_for(&wire); @@ -769,7 +798,10 @@ impl CbindgenBuilder { } if is_vec(ty) { let elem = first_type_arg(ty).expect("Vec has a type argument"); - let entry = registry.output_entry(&elem).expect("Vec element converter"); + let entry = registry + .reading_of(&elem) + .and_then(|tr| registry.output_entry(&tr)) + .expect("Vec element converter"); let elem_conv = entry.function.sig.ident.clone(); let elem_wire = entry.destination.clone(); let t_ptr = &targets[0]; @@ -797,7 +829,8 @@ impl CbindgenBuilder { } if let Some(elem) = cow_slice_elem(ty) { let entry = registry - .output_entry(&elem) + .reading_of(&elem) + .and_then(|tr| registry.output_entry(&tr)) .expect("Cow slice element converter"); let elem_conv = entry.function.sig.ident.clone(); let elem_wire = entry.destination.clone(); @@ -851,7 +884,10 @@ impl CbindgenBuilder { ); } // Base value: run its output converter into the single target. - let entry = registry.output_entry(ty).expect("base value converter"); + let entry = registry + .reading_of(ty) + .and_then(|tr| registry.output_entry(&tr)) + .expect("base value converter"); let conv = entry.function.sig.ident.clone(); let t0 = &targets[0]; if returns_result(&entry.function.sig.output) { @@ -871,7 +907,8 @@ impl CbindgenBuilder { return Self::output_is_fallible(&inner, registry); } registry - .output_entry(ty) + .reading_of(ty) + .and_then(|tr| registry.output_entry(&tr)) .is_some_and(|entry| returns_result(&entry.function.sig.output)) } @@ -1057,13 +1094,16 @@ impl CbindgenBuilder { continue; } - let entry = registry.input_entry(arg_ty).unwrap_or_else(|| { - panic!( - "Cbindgen::on_function: input type `{}` of `{}` has no input converter", - TypeKey::from_type(arg_ty), - orig - ) - }); + let entry = registry + .reading_of(arg_ty) + .and_then(|tr| registry.input_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "Cbindgen::on_function: input type `{}` of `{}` has no input converter", + TypeKey::from_type(arg_ty), + orig + ) + }); let wire = &entry.destination; let conv = &entry.function.sig.ident; diff --git a/prebindgen/src/api/lang/cbindgen/trait_impl.rs b/prebindgen/src/api/lang/cbindgen/trait_impl.rs index e8e3b914..1b35c0ea 100644 --- a/prebindgen/src/api/lang/cbindgen/trait_impl.rs +++ b/prebindgen/src/api/lang/cbindgen/trait_impl.rs @@ -533,7 +533,15 @@ impl CbindgenBuilder { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.opaque) { let ty = key.to_type(); - if registry.input_entry(&ty).is_none() && registry.output_entry(&ty).is_none() { + if registry + .reading_of(&ty) + .and_then(|tr| registry.input_entry(&tr)) + .is_none() + && registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() + { continue; } let c_struct = self.c_type_ident(&ty); @@ -568,7 +576,15 @@ impl CbindgenBuilder { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.data) { let ty = key.to_type(); - if registry.input_entry(&ty).is_none() && registry.output_entry(&ty).is_none() { + if registry + .reading_of(&ty) + .and_then(|tr| registry.input_entry(&tr)) + .is_none() + && registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() + { continue; } let Some(fields) = self.struct_fields(registry, &ty) else { @@ -610,7 +626,15 @@ impl CbindgenBuilder { vo.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str())); for (key, cfg) in vo { let ty = key.to_type(); - if registry.input_entry(&ty).is_none() && registry.output_entry(&ty).is_none() { + if registry + .reading_of(&ty) + .and_then(|tr| registry.input_entry(&tr)) + .is_none() + && registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() + { continue; } let src = self.src_ty(&ty); @@ -807,7 +831,15 @@ impl CbindgenBuilder { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.enums) { let ty = key.to_type(); - if registry.input_entry(&ty).is_none() && registry.output_entry(&ty).is_none() { + if registry + .reading_of(&ty) + .and_then(|tr| registry.input_entry(&tr)) + .is_none() + && registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() + { continue; } let Some(e) = enum_item(registry, &ty) else { @@ -849,7 +881,15 @@ impl CbindgenBuilder { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.tagged_unions) { let ty = key.to_type(); - if registry.input_entry(&ty).is_none() && registry.output_entry(&ty).is_none() { + if registry + .reading_of(&ty) + .and_then(|tr| registry.input_entry(&tr)) + .is_none() + && registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() + { continue; } let Some(e) = enum_item(registry, &ty) else { @@ -1079,7 +1119,11 @@ impl CbindgenBuilder { // degrades to a passthrough and the generated code does not compile. for v in &e.variants { for f in &v.fields { - if self.payload_needs_converter(&f.ty) && r.input_entry(&f.ty).is_none() { + if self.payload_needs_converter(&f.ty) + && r.reading_of(&f.ty) + .and_then(|tr| r.input_entry(&tr)) + .is_none() + { return None; } } @@ -1204,7 +1248,11 @@ impl CbindgenBuilder { // Deferral, as in `in_tagged_union` — the output counterpart. for v in &e.variants { for f in &v.fields { - if self.payload_needs_converter(&f.ty) && r.output_entry(&f.ty).is_none() { + if self.payload_needs_converter(&f.ty) + && r.reading_of(&f.ty) + .and_then(|tr| r.output_entry(&tr)) + .is_none() + { return None; } } @@ -1319,7 +1367,10 @@ impl CbindgenBuilder { // came from that converter's destination, so the two cannot disagree. // A fallible one propagates with `?`, which the union's own `Result` // already provides. - match registry.input_entry(fty) { + match registry + .reading_of(fty) + .and_then(|tr| registry.input_entry(&tr)) + { Some(entry) => { let conv = &entry.function.sig.ident; if returns_result(&entry.function.sig.output) { @@ -1372,7 +1423,10 @@ impl CbindgenBuilder { // including the refusal of a FALLIBLE output converter, which a union // cannot report through — is decided once in `payload_field_wire`, so // this site only emits the call. - match registry.output_entry(fty) { + match registry + .reading_of(fty) + .and_then(|tr| registry.output_entry(&tr)) + { Some(entry) => { let conv = entry.function.sig.ident.clone(); quote!(#conv(#b)) @@ -1397,7 +1451,11 @@ impl CbindgenBuilder { let args: Vec = key.iter().map(|t| t.to_type()).collect(); // Emit only if the callback is required (its input resolved); skip a // declared-but-unused signature. - if registry.input_entry(&callback_fn_type(&args)).is_none() { + if registry + .reading_of(&callback_fn_type(&args)) + .and_then(|tr| registry.input_entry(&tr)) + .is_none() + { continue; } let takeable = &self.callbacks.get(key).expect("callback cfg").takeable; @@ -1411,7 +1469,8 @@ impl CbindgenBuilder { continue; } let wire = registry - .output_entry(a) + .reading_of(a) + .and_then(|tr| registry.output_entry(&tr)) .unwrap_or_else(|| { panic!( "Cbindgen: callback arg `{}` has no output converter (declare it \ @@ -1583,7 +1642,9 @@ impl CbindgenBuilder { call_args.push(quote!(#ai.len())); continue; } - let entry = registry.output_entry(arg)?; + let entry = registry + .reading_of(arg) + .and_then(|tr| registry.output_entry(&tr))?; let conv = entry.function.sig.ident.clone(); let opaque = entry.destination.clone(); let fallible = matches!( @@ -1985,7 +2046,7 @@ impl CbindgenBuilder { // converter, never the owned one. if is_option(ty) { let inner = first_type_arg(ty)?; - let entry = r.input_entry(&inner)?; + let entry = r.reading_of(&inner).and_then(|tr| r.input_entry(&tr))?; let inner_wire = entry.destination.clone(); let inner_conv = entry.function.sig.ident.clone(); let (inner_ok, fallible): (syn::Type, bool) = match &entry.function.sig.output { @@ -2301,7 +2362,7 @@ impl CbindgenBuilder { // `Option` / `Vec` marker. if is_option(ty) || is_vec(ty) { let inner = first_type_arg(ty)?; - r.output_entry(&inner)?; + r.reading_of(&inner).and_then(|tr| r.output_entry(&tr))?; let kind = if is_option(ty) { "option" } else { "vec" }; let name = format_ident!( "__cbg_outmark_{}_{}", @@ -2324,7 +2385,7 @@ impl CbindgenBuilder { // `Cow<'_, [T]>` marker. The actual C ABI shape is structural in // `lower_shape`/`encode_value`, like `Vec`. if let Some(inner) = cow_slice_elem(ty) { - r.output_entry(&inner)?; + r.reading_of(&inner).and_then(|tr| r.output_entry(&tr))?; let name = format_ident!( "__cbg_outmark_cow_slice_{}", sanitize(&TypeKey::from_type(&inner)) @@ -2352,7 +2413,7 @@ impl CbindgenBuilder { .value_opaque_slice_elem(ty) .or_else(|| scalar_slice_elem(ty)) { - r.output_entry(&elem)?; + r.reading_of(&elem).and_then(|tr| r.output_entry(&tr))?; let name = format_ident!( "__cbg_outmark_slice_{}", sanitize(&TypeKey::from_type(&elem)) diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index cfe96b5a..62c4ebbd 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -1756,7 +1756,7 @@ impl Declarations { // demand a bare one does. Walking the reading also drops the // re-lookup the old loop did: it peeled a spelling and re-keyed // each result, where the layers are already right here (#273). - let Some(mut reading) = registry.reading(&candidate.to_type()) else { + let Some(mut reading) = registry.reading(candidate) else { return 0; }; let mut depth = 0; @@ -1840,7 +1840,9 @@ impl Declarations { let inner = if is_self { None } else { - registry.input_entry(&ty) + registry + .reading_of(&ty) + .and_then(|tr| registry.input_entry(&tr)) }; match inner { None if is_self || is_wire_type(&ty) => { @@ -1976,14 +1978,17 @@ impl Declarations { let inner = if is_self { None } else { - registry.output_entry(&ty) + registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) }; match inner { None if is_self || is_wire_type(&ty) => { // Terminal: `ty` is the wire; the body produces it from `outer`. let (kotlin_name, value_rust_key) = if let Some(a0) = arg0 { registry - .output_entry(a0) + .reading_of(a0) + .and_then(|tr| registry.output_entry(&tr)) .map(|e| (e.metadata.kotlin_name.clone(), Some(TypeKey::from_type(a0)))) .unwrap_or((None, None)) } else { diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs index 0ff50cf9..3360bbee 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/callback.rs @@ -83,7 +83,7 @@ pub(crate) fn callback_input( // Every leaf converter must already be resolved (deferral safety). // A synthesized leaf (a sum's tag) has no converter to wait for. for leaf in plan.leaves.iter().filter(|l| l.has_converter()) { - registry.output_entry(leaf.out_ty.syntax())?; + registry.output_entry(&leaf.out_ty)?; } let spec = folder_iface_for_plan(ext, registry, plan)?; let holder_slash = @@ -176,7 +176,7 @@ pub(crate) fn callback_input( // would make the trampoline wait forever on an `i32` crossing the // binding may not have. for leaf in plan.leaves.iter().filter(|l| l.has_converter()) { - let e = registry.output_entry(leaf.out_ty.syntax())?; + let e = registry.output_entry(&leaf.out_ty)?; if leaf.identity && e.metadata.projection.is_none() { return None; } @@ -199,7 +199,7 @@ pub(crate) fn callback_input( // converter and clone the borrow (the callback only borrows the value). The // `data_class` converter composes the whole object via `fromParts`, so the // Kotlin `run(t: T)` receives a ready-made `T`. - let (cb_val, arg_entry) = match registry.output_entry(arg_ty.syntax()) { + let (cb_val, arg_entry) = match registry.output_entry(arg_ty) { Some(e) => (quote!(#cb_arg), e), // A borrow: the callback hands out a reference, and the value is // cloned for the JVM. @@ -225,10 +225,7 @@ pub(crate) fn callback_input( // classification that never fires. None => { let core = arg_ty.borrow_target()?; - ( - quote!((#cb_arg).clone()), - registry.output_entry(core.syntax())?, - ) + (quote!((#cb_arg).clone()), registry.output_entry(core)?) } }; let arg_wire = arg_entry.destination.clone(); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs b/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs index 9aa9197a..133a30ad 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/convert.rs @@ -245,7 +245,9 @@ pub(crate) fn option_input( t1: &syn::Type, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr, Niches)> { - let inner_entry = registry.input_entry(t1)?; + let inner_entry = registry + .reading_of(t1) + .and_then(|tr| registry.input_entry(&tr))?; let inner_wire = inner_entry.destination.clone(); let inner_decode = composed_inner_input(inner_entry, quote!(v)); @@ -311,7 +313,9 @@ pub(crate) fn option_output( t1: &syn::Type, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr, Niches)> { - let inner_entry = registry.output_entry(t1)?; + let inner_entry = registry + .reading_of(t1) + .and_then(|tr| registry.output_entry(&tr))?; let inner_wire = inner_entry.destination.clone(); let inner_encode = composed_inner_output(inner_entry, quote!(value)); @@ -488,7 +492,8 @@ pub(crate) fn nullable_kind_for( registry: &impl Conversions, ) -> NullableKind { let inner_dest = registry - .input_entry(inner_ty) + .reading_of(inner_ty) + .and_then(|tr| registry.input_entry(&tr)) .map(|e| e.destination.clone()) .expect( "nullable_kind_for: Option<_> input handler reached here only after option_input \ @@ -507,7 +512,8 @@ pub(crate) fn nullable_kind_for_output( registry: &impl Conversions, ) -> NullableKind { let inner_dest = registry - .output_entry(inner_ty) + .reading_of(inner_ty) + .and_then(|tr| registry.output_entry(&tr)) .map(|e| e.destination.clone()) .expect( "nullable_kind_for_output: Option<_> output handler reached here only after \ diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index e4d9c1bf..5312f056 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -142,12 +142,15 @@ pub(crate) fn emit_unfold_delivery( // a raw typed jvalue for a primitive-wire element, a JObject // otherwise (mirrors `leaf_is_prim`; the folder interface // declares the matching typed param). - let out_entry = registry.output_entry(element).unwrap_or_else(|| { - panic!( - "emit_unfold_delivery: Vec element `{}` has no registered output converter", - TypeKey::from_type(element) - ) - }); + let out_entry = registry + .reading_of(element) + .and_then(|tr| registry.output_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "emit_unfold_delivery: Vec element `{}` has no registered output converter", + TypeKey::from_type(element) + ) + }); // The element's COMPLETE Rust -> wire chain. No `convert!` type is // known to reach THIS path today (a fold element is single-leaf and // whole, and the collection converters claim the shapes a converted @@ -979,14 +982,12 @@ pub(crate) fn encode_plan_leaves( }; let (value, by_ref, path, consuming) = rebase(leaf); let value = &value; - let out_entry = registry - .output_entry(leaf.out_ty.syntax()) - .unwrap_or_else(|| { - panic!( - "jnigen unfold: leaf `{}` has no registered output converter", - TypeKey::from_type(leaf.out_ty.syntax()) - ) - }); + let out_entry = registry.output_entry(&leaf.out_ty).unwrap_or_else(|| { + panic!( + "jnigen unfold: leaf `{}` has no registered output converter", + TypeKey::from_type(leaf.out_ty.syntax()) + ) + }); let conv_fail = fail(quote!(__e.to_string())); // The leaf's COMPLETE Rust -> wire chain: the rust-side stages a custom // `convert!` declaration inserts (`Duration -> u64`), then the @@ -1384,7 +1385,10 @@ pub(crate) fn leaf_is_prim( /// about a leaf whose own `nullable` flag it is in the middle of computing (an /// inert sum group slot). pub(crate) fn leaf_ty_is_prim(registry: &impl Conversions, out_ty: &syn::Type) -> bool { - let Some(entry) = registry.output_entry(out_ty) else { + let Some(entry) = registry + .reading_of(out_ty) + .and_then(|tr| registry.output_entry(&tr)) + else { return false; }; // No projection (plain primitive/enum wire) — or an opaque HANDLE, whose diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 6192b831..67078158 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -29,7 +29,9 @@ pub(crate) fn struct_input_body( // Defer if any field's input converter isn't resolved yet — the // fixed-point loop will retry on the next iteration. - let field_entry = registry.input_entry(&field.ty)?; + let field_entry = registry + .reading_of(&field.ty) + .and_then(|tr| registry.input_entry(&tr))?; let field_wire = field_entry.destination.clone(); // The field's COMPLETE decode, stages included — a `convert!` type // reaches its Rust value through them (`jlong -> u64 -> Duration`). @@ -98,7 +100,9 @@ pub(crate) fn struct_input_body( FoldStrategy::Optional(NullableKind::Niche, _) ); let inner_conv = composed_entry_decode( - registry.input_entry(&inner_ty)?, + registry + .reading_of(&inner_ty) + .and_then(|tr| registry.input_entry(&tr))?, &raw_ident, &fname_ident, ); @@ -159,7 +163,9 @@ pub(crate) fn struct_input_body( { let sig = format!("L{};", fqn.replace('.', "/")); let inner_conv = composed_entry_decode( - registry.input_entry(&f_inner)?, + registry + .reading_of(&f_inner) + .and_then(|tr| registry.input_entry(&tr))?, &raw_ident, &fname_ident, ); @@ -222,7 +228,8 @@ pub(crate) fn struct_input_body( // `Vec` field. let slot_ty = option_inner_type(&field.ty).unwrap_or_else(|| field.ty.clone()); let sig = registry - .input_entry(&slot_ty) + .reading_of(&slot_ty) + .and_then(|tr| registry.input_entry(&tr)) .and_then(|e| jni_field_access(&e.destination)) .and_then(|(sig, _, is_obj)| { if is_obj { @@ -385,7 +392,9 @@ fn read_kotlin_property( bind: &syn::Ident, err_prefix: &str, ) -> Option<(TokenStream, TokenStream)> { - let entry = registry.input_entry(ty)?; + let entry = registry + .reading_of(ty) + .and_then(|tr| registry.input_entry(&tr))?; let wire = entry.destination.clone(); let raw = format_ident!("{}_raw", bind); // The COMPLETE wire → Rust chain, not just the wire-facing converter: a @@ -461,7 +470,13 @@ fn read_kotlin_property( // Under `Option`, JVM null is `None` and the INNER converter decodes // the discriminant; the outer converter would expect a boxed Integer. let decode = if option_inner_type(ty).is_some() { - let inner_conv = composed_entry_decode(registry.input_entry(&enum_inner)?, &raw, bind); + let inner_conv = composed_entry_decode( + registry + .reading_of(&enum_inner) + .and_then(|tr| registry.input_entry(&tr))?, + &raw, + bind, + ); quote! { let #bind = if #obj.is_null() { ::core::option::Option::None @@ -908,7 +923,9 @@ fn build_flat_sum_field( let kotlin = ext.sum_variant_class_name(sum_cfg, &v.ident); let mut fields = Vec::new(); for (f, item_field) in v.fields.iter().zip(item_variant.fields.iter()) { - let entry = registry.input_entry(&item_field.ty)?; + let entry = registry + .reading_of(&item_field.ty) + .and_then(|tr| registry.input_entry(&tr))?; // A projection payload (handle) carries ownership // and locking rules the tag-gated group does not model yet. if entry.metadata.projection.is_some() { @@ -1154,7 +1171,10 @@ pub(crate) fn build_flat_input_plan( // surfaces as `"Any"` Dispatch or a foreign source type). The resolved // param's Kotlin type (compared by short name, since metadata carries the // FQN) must equal the struct's data-class name. - let Some(entry) = registry.input_entry(arg_ty) else { + let Some(entry) = registry + .reading_of(arg_ty) + .and_then(|tr| registry.input_entry(&tr)) + else { return Ok(None); }; if entry.metadata.projection.is_some() { @@ -1305,7 +1325,10 @@ fn build_flat_struct_node( } let path = child_native.clone(); - let Some(fentry) = registry.input_entry(&field.ty) else { + let Some(fentry) = registry + .reading_of(&field.ty) + .and_then(|tr| registry.input_entry(&tr)) + else { return Err(flat_error( root, &path, @@ -1320,7 +1343,10 @@ fn build_flat_struct_node( // `(present, value)` representation at every recursion depth. if let Some(inner_ty) = option_inner_type(&field.ty) { if !matches!(inner_ty, syn::Type::Reference(_)) { - if let Some(inner) = registry.input_entry(&inner_ty) { + if let Some(inner) = registry + .reading_of(&inner_ty) + .and_then(|tr| registry.input_entry(&tr)) + { if let Some(prim) = JniPrim::from_wire(&inner.destination) { if inner.niches.clone().carve().is_none() && inner.metadata.projection.is_none() @@ -1370,16 +1396,19 @@ fn build_flat_struct_node( if proj.kind == ProjectionKind::Unsigned64 { if let Some(inner_ty) = option_inner_type(&field.ty) { if JniPrim::from_wire(&fentry.destination).is_none() { - let inner = registry.input_entry(&inner_ty).ok_or_else(|| { - flat_error( - root, - &path, - format!( - "unsigned field representation `{}` has no input converter", - TypeKey::from_type(&inner_ty) - ), - ) - })?; + let inner = registry + .reading_of(&inner_ty) + .and_then(|tr| registry.input_entry(&tr)) + .ok_or_else(|| { + flat_error( + root, + &path, + format!( + "unsigned field representation `{}` has no input converter", + TypeKey::from_type(&inner_ty) + ), + ) + })?; let present_index = push_present_leaf( leaves, &format!("{child_native}_present"), @@ -1807,7 +1836,9 @@ pub(crate) fn build_option_scalar_input_plan( if matches!(inner, syn::Type::Reference(_)) { return None; } - let inner_entry = registry.input_entry(&inner)?; + let inner_entry = registry + .reading_of(&inner) + .and_then(|tr| registry.input_entry(&tr))?; let value_wire = inner_entry.destination.clone(); // Only the boxed-primitive fallback shape: primitive wire, no niche, // no projection, no composed pre-stages. diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index cf7e8854..706a6464 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -129,7 +129,7 @@ pub(crate) fn leaf_slot( ("I", format_ident!("i")) } else { let wire = registry - .output_entry(leaf.out_ty.syntax()) + .output_entry(&leaf.out_ty) .expect("leaf_is_prim implies a resolved output entry") .destination .clone(); @@ -345,15 +345,13 @@ fn encode_group_leaf( bind: &syn::Ident, fail: &dyn Fn(TokenStream) -> TokenStream, ) -> TokenStream { - let out_entry = registry - .output_entry(leaf.out_ty.syntax()) - .unwrap_or_else(|| { - panic!( - "jnigen sum unfold: payload leaf `{}` (`{}`) has no registered output converter", - leaf.name, - TypeKey::from_type(leaf.out_ty.syntax()) - ) - }); + let out_entry = registry.output_entry(&leaf.out_ty).unwrap_or_else(|| { + panic!( + "jnigen sum unfold: payload leaf `{}` (`{}`) has no registered output converter", + leaf.name, + TypeKey::from_type(leaf.out_ty.syntax()) + ) + }); let wire = out_entry.destination.clone(); let conv_fail = fail(quote!(__e.to_string())); let enc = format_ident!("__enc_{}", obj_ident); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 518367a6..d1edd6bd 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -111,7 +111,7 @@ pub(crate) fn const_expr_getter_fn( // no element carries it. A miss means the declared type never entered the // pipeline, which is a binding error worth naming rather than a `None` to // absorb. - let ret = registry.reading(ty).unwrap_or_else(|| { + let ret = registry.reading_of(ty).unwrap_or_else(|| { panic!( "constant_expr `{kotlin_name}`: type `{}` is not a type this binding crosses — \ declare it, or name one that is", @@ -221,7 +221,8 @@ pub(crate) fn emit_jni_function_wrapper_with_callee( let output_entry = match &plan.output { FnOutputPlan::Value(v) => Some( registry - .output_entry(&v.target_ty) + .reading_of(&v.target_ty) + .and_then(|tr| registry.output_entry(&tr)) .expect("output entry validated at plan build"), ), FnOutputPlan::Unfold(_) => None, @@ -581,7 +582,8 @@ fn emit_input_param( // the pre-lock guard — is rejected before any dereference. InputKind::Handle { direct: true } if !matches!(arg_ty, syn::Type::Reference(_)) => { let entry = registry - .input_entry(arg_ty) + .reading_of(arg_ty) + .and_then(|tr| registry.input_entry(&tr)) .expect("plan classified Handle ⇒ entry present"); let wire_ident = if matches!(&entry.destination, syn::Type::Ptr(_)) { format_ident!("{}_ptr", arg_ident) @@ -608,13 +610,16 @@ fn emit_input_param( | InputKind::Handle { .. } | InputKind::Unsigned64 { .. } | InputKind::Plain => { - let entry = registry.input_entry(arg_ty).unwrap_or_else(|| { - panic!( - "JniGen::on_function: input type `{}` for `{}` is unresolved", - TypeKey::from_type(arg_ty), - original_ident, - ) - }); + let entry = registry + .reading_of(arg_ty) + .and_then(|tr| registry.input_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "JniGen::on_function: input type `{}` for `{}` is unresolved", + TypeKey::from_type(arg_ty), + original_ident, + ) + }); emit_plain_decode(entry, arg_ident, arg_ty, on_err) } } @@ -756,13 +761,16 @@ pub(crate) fn emit_expanded_param( for (leaf, classified) in plan.leaves.iter().zip(leaves) { let leaf_ty = leaf.ty.syntax(); let lookup_entry = || { - registry.input_entry(leaf_ty).unwrap_or_else(|| { - panic!( - "JniGen expand: leaf type `{}` (parameter `{}`) is unresolved", - TypeKey::from_type(leaf_ty), - orig_param, - ) - }) + registry + .reading_of(leaf_ty) + .and_then(|tr| registry.input_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "JniGen expand: leaf type `{}` (parameter `{}`) is unresolved", + TypeKey::from_type(leaf_ty), + orig_param, + ) + }) }; let local = format_ident!("__exp_{}", leaf.name); diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index 4ec5d2a8..42023361 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -495,7 +495,7 @@ impl JniFunctionPlan { InputKind::Handle { .. } | InputKind::VecBuild { .. } => 2, InputKind::Callback { .. } => 1, InputKind::Unsigned64 { .. } | InputKind::Plain => registry - .input_entry(leaf.reading.syntax()) + .input_entry(&leaf.reading) .and_then(|entry| JniPrim::from_wire(&entry.destination)) .map_or(1, |prim| match prim { JniPrim::Long | JniPrim::Double => 2, @@ -551,7 +551,8 @@ fn classify_leaf( kt_name, kt_public: None, kt_meta: registry - .input_entry(ty) + .reading_of(ty) + .and_then(|tr| registry.input_entry(&tr)) .and_then(|e| e.metadata.kotlin_name.clone()), optional, as_enum_value, @@ -561,7 +562,10 @@ fn classify_leaf( // Every non-callback leaf requires a resolved input entry — the same // hard boundary the Rust emitter has always enforced. - let Some(entry) = registry.input_entry(ty) else { + let Some(entry) = registry + .reading_of(ty) + .and_then(|tr| registry.input_entry(&tr)) + else { let key = TypeKey::from_type(ty); return Err(if expanded { PlanError::UnresolvedLeaf { @@ -690,7 +694,10 @@ fn build_output( .expect("Return delivery carries convert_out_ty"), None => ok_ty.unwrap_or(return_ty), }; - let Some(entry) = registry.output_entry(&target_ty) else { + let Some(entry) = registry + .reading_of(&target_ty) + .and_then(|tr| registry.output_entry(&tr)) + else { return Err(PlanError::UnresolvedOutput { ty: TypeKey::from_type(&target_ty), }); @@ -737,7 +744,10 @@ impl ReturnSurface { syn::ReturnType::Default => return (Self::Unit, syn::parse_quote!(())), syn::ReturnType::Type(_, t) => &**t, }; - let outer_meta = registry.output_entry(ty).map(|e| e.metadata.clone()); + let outer_meta = registry + .reading_of(ty) + .and_then(|tr| registry.output_entry(&tr)) + .map(|e| e.metadata.clone()); // Unit returns (incl. `ZResult<()>`, whose inner identity rides // `value_rust_key`) declare no Kotlin return type. The peeled type // comes straight off the stored key — no reparse, no silent diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index 12811fad..74357346 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -757,7 +757,11 @@ fn leaf_iface_param( // entry was never required. let mut out_ty = out_ty; let peeled: syn::Type; - if registry.output_entry(out_ty).is_none() { + if registry + .reading_of(out_ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() + { if let syn::Type::Reference(r) = out_ty { peeled = (*r.elem).clone(); out_ty = &peeled; @@ -766,7 +770,8 @@ fn leaf_iface_param( let (builder_kt, _wire_kt, _wrap, is_value_projection) = unfold_leaf_kt(ext, registry, out_ty, nullable, "x")?; let proj = registry - .output_entry(out_ty) + .reading_of(out_ty) + .and_then(|tr| registry.output_entry(&tr)) .and_then(|e| e.metadata.projection.as_ref()); let nullable_kt = |t: kt::KtType| { if builder_kt.is_nullable() { @@ -861,7 +866,12 @@ pub(crate) fn owned_handle_iface_param( out_ty: &syn::Type, nullable: bool, ) -> Option { - let proj = registry.output_entry(out_ty)?.metadata.projection.clone()?; + let proj = registry + .reading_of(out_ty) + .and_then(|tr| registry.output_entry(&tr))? + .metadata + .projection + .clone()?; let fqn = ext.kotlin_fqn(&proj.leaf_key)?.to_string(); let typed = kt::KtType::cls(fqn.clone()); let (typed, raw) = if nullable { @@ -997,7 +1007,7 @@ fn derive_iface_spec( // the accessor used to do here. let args: Vec = arg_keys .iter() - .map(|k| registry.reading(&k.to_type())) + .map(|k| registry.reading(k)) .collect::>()?; callback_iface_spec(ext, registry, &args) } @@ -1227,7 +1237,7 @@ pub(crate) fn callback_iface_spec( // A plan-less opaque-handle arg is delivered as a raw `jlong` and // wrapped + closed Kotlin-side (Phase 3 — no Rust `new_object`). let owned_handle = registry - .output_entry(t.syntax()) + .output_entry(t) .and_then(|e| e.metadata.projection.as_ref()) .map(|p| p.kind == ProjectionKind::Handle) .unwrap_or(false); diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 9be3e88d..c62e552c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -793,15 +793,18 @@ impl Declarations { // nothing is looked up (#275). let field_ty = field.ty.syntax(); let where_ = || format!("sealed_class!({}) payload `{variant}.{prop}`", sum_name); - let out = registry.output_entry(field_ty).unwrap_or_else(|| { - panic!( - "{}: `{}` has no resolved OUTPUT converter, so the Kotlin surface for it \ + let out = registry + .reading_of(field_ty) + .and_then(|tr| registry.output_entry(&tr)) + .unwrap_or_else(|| { + panic!( + "{}: `{}` has no resolved OUTPUT converter, so the Kotlin surface for it \ cannot be derived — register converters for the payload type before \ declaring the sealed class", - where_(), - field_ty.to_token_stream(), - ) - }); + where_(), + field_ty.to_token_stream(), + ) + }); if let Some(h) = out.metadata.projection.clone() { let leaf = projection_leaf_kt(self, &h).unwrap_or_else(|| { @@ -833,7 +836,10 @@ impl Declarations { // boxed value, a present flag, a niche) rather than in the type name. // Comparing the rendered types would reject that legitimate shape — // which is what an `Option` payload does. - if let Some(inp) = registry.input_entry(field_ty) { + if let Some(inp) = registry + .reading_of(field_ty) + .and_then(|tr| registry.input_entry(&tr)) + { if let (Some(in_ty), (Some(a), Some(b))) = ( inp.metadata.kotlin_name.clone(), ( @@ -1523,7 +1529,8 @@ impl Declarations { let inner = option_inner_type(leaf.out_ty.syntax()) .unwrap_or_else(|| leaf.out_ty.syntax().clone()); let name = registry - .output_entry(&inner) + .reading_of(&inner) + .and_then(|tr| registry.output_entry(&tr)) .and_then(|e| e.metadata.kotlin_name.clone()) .and_then(|t| t.leaf_name().map(str::to_string)) .unwrap_or_else(|| { diff --git a/prebindgen/src/api/lang/jnigen/jni/overloads.rs b/prebindgen/src/api/lang/jnigen/jni/overloads.rs index bb168b10..3a5bbccc 100644 --- a/prebindgen/src/api/lang/jnigen/jni/overloads.rs +++ b/prebindgen/src/api/lang/jnigen/jni/overloads.rs @@ -156,7 +156,8 @@ fn rust_type_erased( } } if let Some(kt) = registry - .input_entry(peeled) + .reading_of(peeled) + .and_then(|tr| registry.input_entry(&tr)) .and_then(|e| e.metadata.kotlin_name.clone()) { return erase_kt_type(&[], &kt); diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index b3a5ab3b..c9f3cc5b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -123,7 +123,7 @@ pub(crate) fn build_data_class( // disagreed. Reject it at the declaration instead. if !matches!(pf.kind, PlanFieldKind::Projection { .. }) { if let Some(proj) = registry - .input_entry(field.ty.syntax()) + .input_entry(&field.ty) .and_then(|e| e.metadata.projection.clone()) { panic!( @@ -2007,7 +2007,8 @@ pub(crate) fn unfold_leaf_kt( pk: &str, ) -> Option<(kt::KtType, String, String, bool)> { let proj = registry - .output_entry(out_ty) + .reading_of(out_ty) + .and_then(|tr| registry.output_entry(&tr)) .and_then(|e| e.metadata.projection.clone()); let is_value_projection = proj .as_ref() diff --git a/prebindgen/src/api/lang/jnigen/jni/report.rs b/prebindgen/src/api/lang/jnigen/jni/report.rs index 5e472df7..bb4171a6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/report.rs +++ b/prebindgen/src/api/lang/jnigen/jni/report.rs @@ -132,7 +132,8 @@ impl super::JniGen { .kotlin_fqn(key) .unwrap_or_else(|| key.as_str().to_string()); let wire = registry - .output_entry(&key.to_type()) + .reading(key) + .and_then(|tr| registry.output_entry(&tr)) .map(|e| e.wire_type().to_token_stream().to_string()) .unwrap_or_else(|| "?".to_string()); out.push_str(&format!( diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 2f59a355..867140f0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -261,7 +261,9 @@ pub(crate) fn classify_field( return sum_plan_kind(ext, registry, &bare, owner, optional_inner.is_some(), depth); } - let field_entry = registry.output_entry(&effective_ty)?; + let field_entry = registry + .reading_of(&effective_ty) + .and_then(|tr| registry.output_entry(&tr))?; let conv = ConvChain::of(field_entry); { @@ -285,7 +287,8 @@ pub(crate) fn classify_field( if let Some(inner) = optional_inner.map(|i| i.syntax().clone()) { if ext.is_kotlin_enum(&inner) { let kotlin = registry - .output_entry(&inner)? + .reading_of(&inner) + .and_then(|tr| registry.output_entry(&tr))? .metadata .kotlin_name .clone()?; @@ -325,7 +328,8 @@ pub(crate) fn classify_field( let slot_ty = optional_inner.map_or_else(|| effective_ty.clone(), |i| i.syntax().clone()); let descriptor = registry - .output_entry(&slot_ty) + .reading_of(&slot_ty) + .and_then(|tr| registry.output_entry(&tr)) .and_then(|e| jni_field_access(&e.destination)) .and_then(|(sig, _, is_obj)| { if is_obj { diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 9b5fb4ee..b01302c6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -572,7 +572,15 @@ pub(crate) fn build_handle_destructor_items( // Skip handles the (feature-aware) scan never references — their // type may not be in scope in the generated module. let ty = key.to_type(); - if registry.input_entry(&ty).is_none() && registry.output_entry(&ty).is_none() { + if registry + .reading_of(&ty) + .and_then(|tr| registry.input_entry(&tr)) + .is_none() + && registry + .reading_of(&ty) + .and_then(|tr| registry.output_entry(&tr)) + .is_none() + { continue; } let class_fqn = cfg @@ -797,7 +805,7 @@ impl Declarations { if !is_canonical_spelling(produced, &canonical) { return None; } - let inner = registry.input_entry(t1_ty)?; + let inner = registry.input_entry(t1)?; let outer_ty = produced.clone(); // `&T` / `&mut T` are Kotlin-side no-ops — inherit the inner // type's name, unless the user pinned an explicit override @@ -854,7 +862,7 @@ impl Declarations { if !is_canonical_spelling(produced, &canonical) { return None; } - let inner = registry.input_entry(t1_ty)?; + let inner = registry.input_entry(t1)?; if !inner.metadata.is_direct_handle() { // Non-opaque: let the general `Option<_>` handler take it. return None; @@ -914,7 +922,7 @@ impl Declarations { if shape != WrapperShape::Sequence { return None; } - let inner = registry.input_entry(t1_ty)?; + let inner = registry.input_entry(t1)?; reject_vec_of_handle(&inner.metadata.projection, t1_ty); let inner_wire = inner.destination.clone(); if !is_jobject_shaped_wire(&inner_wire) { @@ -983,7 +991,7 @@ impl Declarations { // READING stays in `t1` for the lookups (#284). let t1_ty = t1.syntax(); if shape == WrapperShape::Optional { - let inner = registry.input_entry(t1_ty)?; + let inner = registry.input_entry(t1)?; if inner.metadata.is_direct_handle() { let inner_wire = inner.destination.clone(); let outer_ty = produced.clone(); @@ -1054,7 +1062,7 @@ impl Declarations { // Inherit the inner's name; user pins on `Option` win. // The nullability marker (`?`) is added by the use site. let inherited = registry - .input_entry(t1_ty) + .input_entry(t1) .and_then(|e| e.metadata.kotlin_name.clone()); let kotlin_name = self.override_kotlin_name(&outer_ty, inherited); // Fold a Nullable layer over the inner projection (if any). The @@ -1064,7 +1072,7 @@ impl Declarations { // fallback widens the wire to `JObject`. let nullable_kind = nullable_kind_for(&wire, t1_ty, registry); let projection = registry - .input_entry(t1_ty) + .input_entry(t1) .and_then(|e| e.metadata.projection.clone()) .map(|h| Projection { strategy: FoldStrategy::Optional(nullable_kind, Box::new(h.strategy)), @@ -1188,11 +1196,12 @@ impl Declarations { built: &Building<'_, KotlinMeta>, ) -> Option> { let (dir, key) = crossing; - let ty = key.to_type(); - // The reading the scan already took for this crossing. Rebuilding a - // spelling from the key and classifying *that* is the round trip #263 - // removed from `api/core`; this is the same door, one layer out. - let reading = built.reading(&ty)?; + // The reading the scan already took for this crossing, fetched by the + // key the crossing IS. This used to go `key -> to_type() -> reading`, + // and its own comment called that "the same door, one layer out" as the + // round trip #263 removed from `api/core`. The door is now keyed, so + // there is no spelling to rebuild (#284). + let reading = built.reading(key)?; match dir { Direction::Input => self.select_input_type(&reading, built).or_else(|| { // `impl Fn(args)` that nothing else claimed. Callback args cross @@ -2257,7 +2266,7 @@ impl Declarations { #inner_body }); let inherited = registry - .output_entry(t1_ty) + .output_entry(t1) .and_then(|e| e.metadata.kotlin_name.clone()); let kotlin_name = self.override_kotlin_name(&outer_ty, inherited); // Fold a Nullable layer over the inner projection (if any). The @@ -2267,7 +2276,7 @@ impl Declarations { // and uses JVM null. let nullable_kind = nullable_kind_for_output(&wire, t1_ty, registry); let projection = registry - .output_entry(t1_ty) + .output_entry(t1) .and_then(|e| e.metadata.projection.clone()) .map(|h| Projection { strategy: FoldStrategy::Optional(nullable_kind, Box::new(h.strategy)), @@ -2299,7 +2308,7 @@ impl Declarations { // Symmetric to the input handler. `Vec` is special-cased at // rank-0 (primitive_output → JByteArray) so rank-1 never sees it. if shape == WrapperShape::Sequence { - let inner = registry.output_entry(t1_ty)?; + let inner = registry.output_entry(t1)?; // `Vec` output is delivered by the Kotlin-side leaf // fold (`apply_leaf_vec_folds` → typed-handle wrap), so this // whole-`ArrayList` converter is bypassed for it. A handle's `jlong` @@ -2382,7 +2391,9 @@ impl Declarations { elem: &syn::Type, registry: &impl Conversions, ) -> Option> { - let inner = registry.output_entry(elem)?; + let inner = registry + .reading_of(elem) + .and_then(|tr| registry.output_entry(&tr))?; // A `&[opaque-handle]` callback arg is delivered by the Kotlin-side leaf // fold (typed-handle wrap), bypassing this whole-`ArrayList` converter; a // handle's `jlong` wire isn't JObject-shaped, so it returns `None` here. From 83603aa879a8781c65d3bd8c92478f66f25266e3 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 17:03:07 +0200 Subject: [PATCH 35/52] jnigen: struct_plan peels off the model instead of the tokens (#288) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First piece of #284 step 3. Small on purpose — see the finding below for why the rest is not a helper swap. `struct_plan.rs` already held `reading`, `optional_inner` and `bare_ref` as `TypeRef`s, then downgraded all three to spellings and looked them back up: let effective_ty = reading.syntax().clone(); ... registry.reading_of(&effective_ty).and_then(|tr| registry.output_entry(&tr))? Three of those round trips are gone; the file now has ZERO `reading_of` calls. Two of the five sites were not round trips but latent defects of the #270/#272 family — asking a spelling a question the model answers: * `pat_match_top(&slot_ty, "Vec")` compares the last path segment, so a `Box>` answered FALSE. Now `slot.sequence_elem().is_some()`. * `bare_path_ident(&slot_ty)` takes the spelling apart to get a name, which answers about the WRAPPER for `Box`. Now the name comes off `TypeKind::Named { id }`. Neither is reachable from the in-tree examples — goldens are byte-identical — so these are the same "correct output, no signal" shape as #266/#273 rather than observed breakage. FINDING that resizes the rest of step 3: `emit/flat_input.rs` holds 20 of the 34 `option_inner_type` callers and 12 `reading_of` sites, and the reason is not the helper — it walks `syn::Fields::Named` directly, while `flat::Struct::fields` already carries a `TypeRef` per field. So that file needs the ELEMENT-WALKING change (take `&flat::Struct`, walk `struct.fields`), which is the same follow-up the umbrella records from #283 for `scan_struct`/`scan_enum` — not a peel substitution. It is its own PR rather than a rushed extension of this one. Ledger unchanged at 127: `types_util` only falls when its callers stop needing it, and `option_inner_type` still has 34. Verified: 551 lib tests, `--all --all-features` 14/14 suites, clippy `--deny warnings` clean, fmt (CLI config), regen-check byte-identical after `git clean -fd examples/` + `cargo clean -p`, covertest-kotlin 48/48. --- .../src/api/lang/jnigen/jni/struct_plan.rs | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 867140f0..753cf598 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -261,9 +261,7 @@ pub(crate) fn classify_field( return sum_plan_kind(ext, registry, &bare, owner, optional_inner.is_some(), depth); } - let field_entry = registry - .reading_of(&effective_ty) - .and_then(|tr| registry.output_entry(&tr))?; + let field_entry = registry.output_entry(reading)?; let conv = ConvChain::of(field_entry); { @@ -284,14 +282,9 @@ pub(crate) fn classify_field( return Some(PlanFieldKind::Enum { conv, kotlin }); } // `Option` leaf. - if let Some(inner) = optional_inner.map(|i| i.syntax().clone()) { - if ext.is_kotlin_enum(&inner) { - let kotlin = registry - .reading_of(&inner) - .and_then(|tr| registry.output_entry(&tr))? - .metadata - .kotlin_name - .clone()?; + if let Some(inner) = optional_inner { + if ext.is_kotlin_enum(inner.syntax()) { + let kotlin = registry.output_entry(inner)?.metadata.kotlin_name.clone()?; return Some(PlanFieldKind::OptionEnum { conv, kotlin }); } } @@ -325,11 +318,11 @@ pub(crate) fn classify_field( None => { // Object-shaped wire with no fixed descriptor; the JVM slot // must be the field's actual declared type (Option-stripped). - let slot_ty = - optional_inner.map_or_else(|| effective_ty.clone(), |i| i.syntax().clone()); + // Option-stripped off the MODEL: `optional_inner` is the + // layer's own reading, so there is nothing to re-look-up. + let slot = optional_inner.unwrap_or(reading); let descriptor = registry - .reading_of(&slot_ty) - .and_then(|tr| registry.output_entry(&tr)) + .output_entry(slot) .and_then(|e| jni_field_access(&e.destination)) .and_then(|(sig, _, is_obj)| { if is_obj { @@ -344,13 +337,23 @@ pub(crate) fn classify_field( } }) .or_else(|| { - bare_path_ident(&slot_ty).and_then(|name| { + // The NAME off the classification, not off the last + // path segment: `Box` IS `T` here, and taking the + // spelling apart would answer about the wrapper. + match slot.kind() { + crate::api::core::flat::TypeKind::Named { id } => id.ident(), + _ => None, + } + .and_then(|name| { ext.kotlin_fqn(&TypeKey::from_ident(&name)) .map(|v| format!("L{};", v.replace('.', "/"))) }) }) .or_else(|| { - if pat_match_top(&slot_ty, "Vec") { + // A run of values is what `kind` says it is. + // `pat_match_top(.., "Vec")` compared the last path + // segment, so a `Box>` answered false. + if slot.sequence_elem().is_some() { Some("Ljava/util/List;".to_string()) } else { // The wire table already names every reference wire's From e2994262e976cd405e0a1ebaf54a5e06739d7bda Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 19:53:16 +0200 Subject: [PATCH 36/52] jnigen: readings instead of spellings across the input plan (#290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continues #284 step 3: the remaining `option_inner_type` / `reading_of` sites, one consumer at a time. `TypeRef` gains three accessors, each answering a question a spelling was being asked before: `callback_args()` (the reading counterpart of the `extract_fn_trait_args` *classifier*), `erased_wrapper()`, and jnigen's `enum_probe()` + `Declarations::is_kotlin_enum_reading()`. Converted: `classify_leaf` (now zero `reading_of` calls and no spelling local at all), `build_flat_input_plan`, `build_option_scalar_input_plan`, `vec_build_elem`/`vec_build_helpers`/`collect_vec_build_elem_types`, `sum_ctor_arg`, four round-tripped entry lookups, and `PlanError`, which now carries `Box` and names a source position. Two helpers fell out **provably dead** — `impl_into_target` (the model refuses `impl Trait` that is not the callback form, so it was already unreachable before this branch; `cargo check` said so) and `slice_or_vec_elem`. Both are replaced by a comment recording what stood there. `extract_fn_trait_args` is gone from jnigen production code entirely. Fixed along the way: `Box` and `Option>` now reach their `enum_class!` declaration instead of missing on a `Box < Priority >` key; and `build_output`'s two distinct failures no longer share one message that gave correct advice for one and actively wrong advice for the other. Ledgers move **down**, which is the direction they exist to reward: boundary 127 → 122, spelling census `vec_build.rs` and `kotlin_emit.rs` to zero. ## Review found a real defect, twice The rule "model peels are always better" is **not** unconditional, and I applied it past an exception the codebase already documented (`decoded_vec_satisfies`). The sharper split: * **`kind` decides what the destination sees** — surface type *and wire*. * **`syntax` decides how the value is converted**, and Rust tells apart what the model erases. The specialized input lowerings do not decode their parameter, they **rebuild** it — so selecting them off `kind` alone made a `Box>` parameter receive a bare `Option`: `E0308` in the generated crate. Round two found the same defect one layer out: an erasure sits *outside* the layer it wraps, so `Box<&Vec>` classifies as `Ref` and a guard that reads `kind` first discards the wrapper before looking. Both are fixed by asking the **model** (`erased_wrapper()`, since it is the only thing holding both halves) before each peel, never by re-adding spelling probes: net spelling probes added is zero. Refusing is a **gap, not a requirement** — `Box::new(v)` is what the syntax asks for — so #292 tracks rebuilding instead of refusing, along with the wire-from-`kind` rule (#230's real diagnosis) and the stripped-spelling model facts a rebuild needs. Two regression tests, each on a **control pair** so it cannot pass vacuously, and the ordering one verified to fail when its guard is disabled. Generated Rust is never compiled by this suite (#269), so they pin that the emitter is never *asked* to write the ill-typed code rather than the `E0308` itself; their docs say so. Also fixes a `cargo fmt --check` failure that had already turned CI red. --- prebindgen/src/api/core/flat/boundary.ledger | 5 +- prebindgen/src/api/core/flat/ty.rs | 59 ++++ prebindgen/src/api/core/registry/view.rs | 83 ++--- prebindgen/src/api/lang/jnigen/jni/builder.rs | 43 ++- .../api/lang/jnigen/jni/emit/flat_input.rs | 135 +++++--- .../src/api/lang/jnigen/jni/emit/vec_build.rs | 104 +++--- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 49 +-- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 149 ++++++--- prebindgen/src/api/lang/jnigen/jni/fold.rs | 39 ++- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 52 ++- prebindgen/src/api/lang/jnigen/jni/mod.rs | 2 +- .../src/api/lang/jnigen/jni/tests/values.rs | 310 ++++++++++++++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 32 +- prebindgen/src/api/lang/jnigen/mod.rs | 12 +- 14 files changed, 821 insertions(+), 253 deletions(-) diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 83b31b1b..1397156a 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -54,9 +54,8 @@ 4 api/lang/jnigen/jni/builder.rs 4 api/lang/jnigen/jni/emit/convert.rs 2 api/lang/jnigen/jni/emit/delivery.rs -10 api/lang/jnigen/jni/emit/flat_input.rs +7 api/lang/jnigen/jni/emit/flat_input.rs 17 api/lang/jnigen/jni/emit/names.rs -2 api/lang/jnigen/jni/emit/vec_build.rs 11 api/lang/jnigen/jni/emit/wrapper.rs 3 api/lang/jnigen/jni/fold.rs 4 api/lang/jnigen/jni/iface.rs @@ -70,4 +69,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 127 +# total: 122 diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 597a3b2a..303c2cac 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -312,6 +312,40 @@ impl TypeRef { crate::api::core::registry::TypeKey::from_type(&self.origin.syntax) } + /// The [transparent wrapper](TRANSPARENT_WRAPPERS) this type's **spelling** + /// adds over its classification, if any — `Box>` → `Some("Box")`, + /// `Option` → `None`. + /// + /// This exists because [`kind`](Self::kind) and [`syntax`](Self::syntax) + /// answer different questions, and only one of them is about the + /// destination: + /// + /// * `kind` decides what the **destination** sees — the surface type and the + /// wire. `Box>` and `Option` are one optional + /// string to every destination language, which is why the wrapper is + /// erased. + /// * `syntax` decides how the value is **converted** — and Rust does tell + /// them apart. A converter that rebuilds a value must produce the type + /// the source actually spelled. + /// + /// So a consumer that *classifies* should never consult this; a consumer + /// that **reconstructs a Rust value** must, because rebuilding from the + /// classification alone yields the stripped type and handing that to a + /// parameter spelled `Box<..>` is an `E0308` in the generated crate. + /// + /// Only the outermost wrapper is named. That is enough to decide *whether* + /// a spelling was erased — which is the question a reconstruction asks — + /// but a consumer that wants to rebuild a nested `Box>` needs to + /// peel repeatedly with [`peel_transparent`], the same list this reads. + /// + /// Erased says nothing about **rebuildable**: `Box` reconstructs as + /// `Box::new(v)`, while `Cow`'s `Owned`/`Borrowed` choice is not determined + /// by any fact the model holds. Which wrappers an emitter can rebuild is + /// that emitter's policy; this only stops the wrapper from being invisible. + pub fn erased_wrapper(&self) -> Option<&'static str> { + peel_transparent(&self.origin.syntax).map(|(name, _)| name) + } + /// The `Ok` and `Err` sides when this is a `Result`, else `None`. pub fn fallible_parts(&self) -> Option<(&TypeRef, &TypeRef)> { match &self.kind { @@ -320,6 +354,31 @@ impl TypeRef { } } + /// The argument types when this is a callback, else `None` — the reading + /// counterpart of + /// [`extract_fn_trait_args`](super::extract_fn_trait_args). + /// + /// The two are the same question asked of different things, and that is the + /// whole difference. `extract_fn_trait_args` takes an + /// `impl Fn(..) + Send + Sync + 'static` **apart**: it walks the bounds, + /// checks the three markers, and refuses a written return type — it is a + /// classifier, and the one the model itself runs to build + /// [`TypeKind::Callback`]. This reads the result of that classification, + /// already made. A consumer holding a reading has no reason to redo the + /// walk, and every reason not to: a `Vec` of *arguments* has lost + /// which of them the model accepted and how, while each `TypeRef` here + /// carries its own classification and its own spelling. + /// + /// Consequently this answers `None` for a type that merely *looks* like a + /// callback but was refused (a missing `Send`, an `impl Fn() -> u8`): the + /// acceptance already happened, and asking again is how the two drift. + pub fn callback_args(&self) -> Option<&[TypeRef]> { + match &self.kind { + TypeKind::Callback { args } => Some(args), + _ => None, + } + } + /// The extent of this type when it is an array, else `None`. pub fn array_extent(&self) -> Option<&ArrayExtent> { match &self.kind { diff --git a/prebindgen/src/api/core/registry/view.rs b/prebindgen/src/api/core/registry/view.rs index 65ffbb68..ccbae407 100644 --- a/prebindgen/src/api/core/registry/view.rs +++ b/prebindgen/src/api/core/registry/view.rs @@ -4,6 +4,10 @@ use std::collections::HashMap; use super::*; +use crate::api::core::{ + flat::{Flat, TypeRef}, + unfold::{DeconId, DeconSpec, UnfoldPlan}, +}; /// One `(direction, type)` pair that crosses the boundary. /// @@ -22,7 +26,7 @@ pub type Crossing = (Direction, TypeKey); /// `&impl Conversions` and works either side of the boundary. pub trait Conversions { /// The model. - fn flat(&self) -> &crate::api::core::flat::Flat; + fn flat(&self) -> &Flat; /// The reading for `ty` — what the frontend made of it. /// @@ -40,7 +44,7 @@ pub trait Conversions { /// reading. This is the door FROM identity TO the model's answer, and the /// only lookup on this trait that does not already take a `TypeRef` — the /// rest take one precisely because this exists to hand them one (#284). - fn reading(&self, key: &TypeKey) -> Option; + fn reading(&self, key: &TypeKey) -> Option; /// The conversion for `reading` in `dir`, if there is one. /// @@ -49,11 +53,7 @@ pub trait Conversions { /// tokens instead let a spelling nobody classified reach the table, and cost /// a `TypeKey::from_type` on every call for an identity the reading already /// carries. - fn conversion( - &self, - dir: Direction, - reading: &crate::api::core::flat::TypeRef, - ) -> Option<&TypeEntry>; + fn conversion(&self, dir: Direction, reading: &TypeRef) -> Option<&TypeEntry>; /// The reading for a **spelling** — identify, then look up. /// @@ -66,17 +66,17 @@ pub trait Conversions { /// so they cannot be called about a type the registry does not know — which /// is the guarantee, and it survives only while getting a reading from /// tokens is a visible step with a `None` to handle. - fn reading_of(&self, ty: &syn::Type) -> Option { + fn reading_of(&self, ty: &syn::Type) -> Option { self.reading(&TypeKey::from_type(ty)) } /// Wire → rust. - fn input_entry(&self, reading: &crate::api::core::flat::TypeRef) -> Option<&TypeEntry> { + fn input_entry(&self, reading: &TypeRef) -> Option<&TypeEntry> { self.conversion(Direction::Input, reading) } /// Rust → wire. - fn output_entry(&self, reading: &crate::api::core::flat::TypeRef) -> Option<&TypeEntry> { + fn output_entry(&self, reading: &TypeRef) -> Option<&TypeEntry> { self.conversion(Direction::Output, reading) } @@ -85,22 +85,20 @@ pub trait Conversions { /// On the trait because a callback converter needs it while being built, /// and the emitter needs it again afterwards. Plans are applied by /// `prepare`, so they are complete either side of that line. - fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan>; + fn callback_arg_plan(&self, key: &TypeKey) -> Option<&UnfoldPlan>; /// Every callback-argument decomposition, for the emitters that enumerate /// them rather than look one up. - fn callback_arg_plans(&self) -> &HashMap; + fn callback_arg_plans(&self) -> &HashMap; /// The return decomposition of a function, if it has one. - fn unfold_plans(&self) -> &HashMap; + fn unfold_plans(&self) -> &HashMap; /// The error-position decomposition of a fallible function. - fn error_plans(&self) -> &HashMap; + fn error_plans(&self) -> &HashMap; /// The declaration-default decomposition behind each deconstructor. - fn decon_plans( - &self, - ) -> &HashMap; + fn decon_plans(&self) -> &HashMap; /// Every type key that crosses in `dir`. /// @@ -120,34 +118,28 @@ pub trait Conversions { } impl Conversions for Building<'_, M> { - fn flat(&self) -> &crate::api::core::flat::Flat { + fn flat(&self) -> &Flat { &self.registry.flat } - fn reading(&self, key: &TypeKey) -> Option { + fn reading(&self, key: &TypeKey) -> Option { self.registry.reading(key) } - fn conversion( - &self, - dir: Direction, - reading: &crate::api::core::flat::TypeRef, - ) -> Option<&TypeEntry> { + fn conversion(&self, dir: Direction, reading: &TypeRef) -> Option<&TypeEntry> { self.built.get(&(dir, reading.key())) } - fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan> { + fn callback_arg_plan(&self, key: &TypeKey) -> Option<&UnfoldPlan> { self.registry.callback_arg_plans.get(key) } - fn callback_arg_plans(&self) -> &HashMap { + fn callback_arg_plans(&self) -> &HashMap { &self.registry.callback_arg_plans } - fn unfold_plans(&self) -> &HashMap { + fn unfold_plans(&self) -> &HashMap { &self.registry.unfold_plans } - fn error_plans(&self) -> &HashMap { + fn error_plans(&self) -> &HashMap { &self.registry.error_plans } - fn decon_plans( - &self, - ) -> &HashMap { + fn decon_plans(&self) -> &HashMap { &self.registry.decon_plans } fn crossing_keys(&self, dir: Direction) -> Vec { @@ -160,34 +152,28 @@ impl Conversions for Building<'_, M> { } impl Conversions for Registry { - fn flat(&self) -> &crate::api::core::flat::Flat { + fn flat(&self) -> &Flat { &self.flat } - fn reading(&self, key: &TypeKey) -> Option { + fn reading(&self, key: &TypeKey) -> Option { Registry::reading(self, key) } - fn conversion( - &self, - dir: Direction, - reading: &crate::api::core::flat::TypeRef, - ) -> Option<&TypeEntry> { + fn conversion(&self, dir: Direction, reading: &TypeRef) -> Option<&TypeEntry> { self.type_table(dir).get(&reading.key())?.entry.as_ref() } - fn callback_arg_plan(&self, key: &TypeKey) -> Option<&crate::api::core::unfold::UnfoldPlan> { + fn callback_arg_plan(&self, key: &TypeKey) -> Option<&UnfoldPlan> { self.callback_arg_plans.get(key) } - fn callback_arg_plans(&self) -> &HashMap { + fn callback_arg_plans(&self) -> &HashMap { &self.callback_arg_plans } - fn unfold_plans(&self) -> &HashMap { + fn unfold_plans(&self) -> &HashMap { &self.unfold_plans } - fn error_plans(&self) -> &HashMap { + fn error_plans(&self) -> &HashMap { &self.error_plans } - fn decon_plans( - &self, - ) -> &HashMap { + fn decon_plans(&self) -> &HashMap { &self.decon_plans } fn crossing_keys(&self, dir: Direction) -> Vec { @@ -232,15 +218,12 @@ impl<'a, M> Building<'a, M> { /// Shared by [`Registry::origin_module`] and [`Building::origin_module`], so the /// two cannot answer differently. -pub(super) fn origin_module_of( - flat: &crate::api::core::flat::Flat, - ident: &syn::Ident, -) -> Option { +pub(super) fn origin_module_of(flat: &Flat, ident: &syn::Ident) -> Option { let crate_name = flat.element(ident)?.location().crate_name.as_ref()?; syn::parse_str(&crate_name.replace('-', "_")).ok() } -pub(super) fn default_module_of(flat: &crate::api::core::flat::Flat) -> Option { +pub(super) fn default_module_of(flat: &Flat) -> Option { flat.source_modules() .first() .and_then(|m| syn::parse_str(m).ok()) diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 62c4ebbd..f5f3f62f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -7,7 +7,15 @@ //! JNI module; shares the `jni` namespace via `use super::*`. use super::*; -use crate::api::core::registry::Conversions; +// `flat` as a module, not `flat::TypeKind` directly: the bare `TypeKind` in this +// file is jnigen's OWN classifier (`classify.rs`, reached through `use super::*` +// above), and an explicit import beats a glob — importing the model's would +// silently retarget the `TypeKind::Sum` / `TypeKind::DataStruct` matches below. +// One qualifier keeps both names short and says which of the two it is. +use crate::api::core::{ + flat::{self, TypeRef}, + registry::Conversions, +}; impl DeclaredKind { /// The declaring macro's name, for the conflict message. @@ -97,10 +105,35 @@ impl Declarations { /// Kotlin wrapper generator to decide if a parameter needs a `.value` /// projection between the typed enum (Kotlin signature) and the `Int` /// wire (JNI `external fun`). + /// + /// Keyed on the canonical **spelling**, so it answers about the wrapper for + /// a transparently-wrapped type: `Box` is `false` here. + /// [`is_kotlin_enum_reading`](Self::is_kotlin_enum_reading) is the same + /// question asked of the model, and is what a caller holding a reading + /// should use. pub(crate) fn is_kotlin_enum(&self, ty: &syn::Type) -> bool { let key = TypeKey::from_type(ty); self.types.get(&key).is_some_and(|c| c.is_enum_class()) } + + /// Whether this value's core is a type registered via an `EnumClassDecl`, + /// asked of the **reading**: [`enum_probe`] peels the borrow/optional + /// layers off the model, and the name comes off the classification. + /// + /// The name off `TypeKind::Named` rather than the spelling is the whole + /// difference from [`is_kotlin_enum`](Self::is_kotlin_enum). A declaration + /// names a type (`enum_class!(Priority)` keys `Priority`), and the model + /// erases the wrappers no destination language can see — so + /// `Box>` reaches the same declaration `Priority` does, + /// where taking the spelling apart finds `Box` and answers about it. + pub(crate) fn is_kotlin_enum_reading(&self, reading: &TypeRef) -> bool { + let flat::TypeKind::Named { id } = enum_probe(reading).kind() else { + return false; + }; + id.ident() + .and_then(|i| self.types.get(&TypeKey::from_ident(&i))) + .is_some_and(|c| c.is_enum_class()) + } } impl Default for Declarations { @@ -852,7 +885,7 @@ impl Declarations { // model already normalized them. let ret = accessor.ret.borrow_target().unwrap_or(&accessor.ret); assert!( - !matches!(ret.kind(), crate::api::core::flat::TypeKind::Unit), + !matches!(ret.kind(), flat::TypeKind::Unit), "expand_return!({}).fields(fields!({func})): `{func}` returns nothing — a \ value form returns the struct holding this type's fields", key.as_str(), @@ -914,7 +947,7 @@ impl Declarations { registry: &impl Conversions, key: &TypeKey, decl: &FieldsDecl, - st: &crate::api::core::flat::Struct, + st: &flat::Struct, members: &[syn::Ident], name_prefix: &str, depth: usize, @@ -1053,10 +1086,10 @@ impl Declarations { ); // The name is the reading's, not a path taken apart to // re-derive one. - let crate::api::core::flat::TypeKind::Named { id } = probe.kind() else { + let flat::TypeKind::Named { id } = probe.kind() else { panic!("a sum type is a named type") }; - let crate::api::core::flat::Type::Variant(sum) = registry + let flat::Type::Variant(sum) = registry .flat() .declared_type(&id.name) .expect("TypeKind::Sum implies an indexed enum") diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 67078158..8fd585ce 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -2,7 +2,12 @@ //! expressions, and the Rust-side reconstruct. use super::*; -use crate::api::core::registry::Conversions; +// `flat` as a module for `TypeKind`: the bare name in this scope is jnigen's own +// classifier (via `use super::*`), and an explicit import would shadow it. +use crate::api::core::{ + flat::{self, TypeRef}, + registry::Conversions, +}; pub(crate) fn struct_input_body( ext: &Declarations, @@ -739,6 +744,46 @@ pub(crate) struct FlatSumVariant { pub fields: Vec<(syn::Member, usize)>, } +/// Peel `&` then `Option<…>` off the model to reach the value a specialized +/// lowering would **rebuild**, refusing at any layer whose spelling adds a +/// wrapper the classification erased. +/// +/// One function because it is one rule. `kind` decides what the destination +/// sees; the **conversion** follows the syntax, and these lowerings do not +/// decode their parameter — they emit a literal `S { .. }`, wrap it in +/// `Option::Some`, and hand it to the source function. Rebuilding from the +/// classification alone produces the stripped type, so a parameter spelled +/// `Box>` receives an `Option`: `E0308` in the generated crate. +/// +/// Refusing rather than rebuilding is a **gap, not a requirement**: +/// `Box::new(v)` is exactly what the syntax asks for and is trivially +/// emittable. It is left out here because doing it properly means teaching the +/// converters to rebuild each wrapper, and `Cow` cannot be rebuilt from an +/// owned payload at all. A wrapped spelling therefore keeps the general +/// converter path, which is correct if less direct. +/// +/// The wrapper question goes to [`TypeRef::erased_wrapper`] — the model holds +/// both halves, so it is the only thing that can answer it, and no spelling is +/// taken apart here. +fn rebuildable_target(arg: &TypeRef) -> Option<(bool, bool, &TypeRef)> { + if arg.erased_wrapper().is_some() { + return None; + } + let by_ref = arg.borrow_target().is_some(); + let t1 = arg.borrow_target().unwrap_or(arg); + if t1.erased_wrapper().is_some() { + return None; + } + let optional = t1.optional_inner().is_some(); + let inner = t1.optional_inner().unwrap_or(t1); + // The struct is rebuilt BY NAME (`S { .. }`), so its own spelling must name + // it — a `Box` target would need the `Box::new` this emitter never writes. + if inner.erased_wrapper().is_some() { + return None; + } + Some((by_ref, optional, inner)) +} + /// A flattened plan for one struct input parameter. Built once by /// [`build_flat_input_plan`] and consumed by all three codegen sites. pub(crate) struct FlatInputPlan { @@ -751,26 +796,15 @@ pub(crate) struct FlatInputPlan { pub contains_nested: bool, } -/// Extract `S` from an `impl Into + …` parameter type. -pub(crate) fn impl_into_target(ty: &syn::Type) -> Option { - let syn::Type::ImplTrait(it) = ty else { - return None; - }; - for b in &it.bounds { - if let syn::TypeParamBound::Trait(tb) = b { - if let Some(seg) = tb.path.segments.last() { - if seg.ident == "Into" { - if let syn::PathArguments::AngleBracketed(ab) = &seg.arguments { - if let Some(syn::GenericArgument::Type(t)) = ab.args.first() { - return Some(t.clone()); - } - } - } - } - } - } - None -} +// `impl_into_target` lived here: it extracted `S` from an `impl Into + …` +// spelling for `build_flat_input_plan`'s struct-target peel. It is gone because +// that peel now takes a reading, and the model REFUSES `impl Trait` that is not +// the callback form (`UnsupportedTypeReason::DisallowedImplTrait`) — so a +// parameter spelled `impl Into` never becomes a `TypeRef` and never reached +// the call. `cargo check` confirmed it dead rather than the reasoning alone. +// jnigen's actual `impl Into<…>` support is elsewhere: plugin wrapper exts build +// a `ConverterImpl::function` by hand via `Declarations::input_converter_name`, +// which never consults this. /// Peel a leading `&`/`&mut` then an `Option<…>` to expose the inner type used /// for enum/struct detection (`&Priority`, `Option` → `Priority`). @@ -1136,19 +1170,24 @@ pub(crate) fn build_flat_input_plan( ext: &Declarations, registry: &Registry, param_name: &syn::Ident, - arg_ty: &syn::Type, + arg: &TypeRef, ) -> Result, FlatInputError> { - // 1. Resolve the struct target through `&`, `Option<…>`, and `impl Into`. - let (by_ref, t1) = match arg_ty { - syn::Type::Reference(r) => (true, (*r.elem).clone()), - other => (false, other.clone()), + // 1. Resolve the struct target through `&` and `Option<…>` — off the model, + // and refusing any layer whose spelling the rebuild could not satisfy. + let Some((by_ref, optional, inner)) = rebuildable_target(arg) else { + return Ok(None); }; - let (optional, inner) = match option_inner_type(&t1) { - Some(i) => (true, i), - None => (false, t1.clone()), + // `impl Into` is NOT peeled here, and cannot be: the model refuses + // `impl Trait` that is not the callback form (`DisallowedImplTrait`), so a + // parameter spelled that way never becomes a reading and never reaches this + // function. The former `impl_into_target` call was already unreachable from + // every caller — see the sibling helper's doc. + // The name off the classification, not off the last path segment: `Box` + // IS `S` here, and taking the spelling apart would answer about the wrapper. + let flat::TypeKind::Named { id } = inner.kind() else { + return Ok(None); }; - let struct_ty = impl_into_target(&inner).unwrap_or_else(|| inner.clone()); - let Some(name) = bare_path_ident(&struct_ty) else { + let Some(name) = id.ident() else { return Ok(None); }; let Some(st) = registry @@ -1158,7 +1197,7 @@ pub(crate) fn build_flat_input_plan( else { return Ok(None); }; - let key = TypeKey::from_type(&struct_ty); + let key = inner.key(); let Some(cfg) = ext.types.get(&key) else { return Ok(None); }; @@ -1171,10 +1210,8 @@ pub(crate) fn build_flat_input_plan( // surfaces as `"Any"` Dispatch or a foreign source type). The resolved // param's Kotlin type (compared by short name, since metadata carries the // FQN) must equal the struct's data-class name. - let Some(entry) = registry - .reading_of(arg_ty) - .and_then(|tr| registry.input_entry(&tr)) - else { + // The parameter's own reading straight to its entry — no spell-and-look-back. + let Some(entry) = registry.input_entry(arg) else { return Ok(None); }; if entry.metadata.projection.is_some() { @@ -1829,16 +1866,22 @@ pub(crate) fn build_option_scalar_input_plan( ext: &Declarations, registry: &Registry, param_name: &syn::Ident, - arg_ty: &syn::Type, + arg: &TypeRef, ) -> Option { - let inner = option_inner_type(arg_ty)?; + // The optional layer off the model — but the emitter rebuilds a bare + // `Option::Some(v)` and hands it to the source fn, so a spelling the model + // erased a wrapper from could not receive it. Conversion follows the syntax; + // see [`rebuildable_target`], which applies the same rule to the struct path. + if arg.erased_wrapper().is_some() { + return None; + } + let inner = arg.optional_inner()?; // `Option<&T>` is the nullable-borrow / handle path, not a scalar. - if matches!(inner, syn::Type::Reference(_)) { + if inner.borrow_target().is_some() { return None; } - let inner_entry = registry - .reading_of(&inner) - .and_then(|tr| registry.input_entry(&tr))?; + // The layer's own reading straight to its entry — no spell-and-look-back. + let inner_entry = registry.input_entry(inner)?; let value_wire = inner_entry.destination.clone(); // Only the boxed-primitive fallback shape: primitive wire, no niche, // no projection, no composed pre-stages. @@ -1852,7 +1895,13 @@ pub(crate) fn build_option_scalar_input_plan( if !inner_entry.pre_stages.is_empty() { return None; } - let is_enum = ext.is_kotlin_enum(&inner); + // The reading-taking probe: `Option>` now answers TRUE, where + // keying on the spelling asked about `Box < Priority >` and found nothing — + // the #270/#272 family again. The probe also peels an optional, which cannot + // matter here: a nested `Option>` has a BOXED wire, and + // `JniPrim::from_wire` above accepts only the eight `j*` primitives, so it + // has already returned by this line. + let is_enum = ext.is_kotlin_enum_reading(inner); Some(OptionScalarInputPlan { present_ident: format_ident!("{}_present", param_name), value_ident: format_ident!("{}_value", param_name), diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs index 51e40c10..b9c4c588 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs @@ -2,22 +2,18 @@ //! (`New`/`Push`/`Free` helper trio). use super::*; +// `flat` as a module for `TypeKind`: the bare name in this scope is jnigen's own +// classifier (reached through `use super::*`), and an explicit import would win +// over the glob and silently retarget it. +use crate::api::core::flat::{self, RefMode, TypeRef}; -/// Classify a slice/`Vec` **input** param for the "build the Rust-side Vec -/// incrementally" path: an immutable slice `&[T]` (`by_ref = true`, the target -/// borrows the boxed Vec) or a by-value `Vec` (`by_ref = false`, the target -/// moves it out via `mem::take`). `&mut [T]` (mutate-back semantics) and every -/// other shape return `None`, keeping the existing `input_vec` `List` -/// path. (Element flattenability is checked separately by [`vec_build_elem`].) -pub(crate) fn slice_or_vec_elem(arg_ty: &syn::Type) -> Option<(syn::Type, bool)> { - match arg_ty { - syn::Type::Reference(r) if r.mutability.is_none() => match &*r.elem { - syn::Type::Slice(s) => Some(((*s.elem).clone(), true)), - _ => None, - }, - _ => vec_inner_type(arg_ty).map(|t| (t, false)), - } -} +// `slice_or_vec_elem` lived here: it matched `&[T]` / `Vec` off the SPELLING +// and returned the element. `vec_build_elem` was its only caller and now reads +// the same two facts off the model (`sequence_elem`, through `borrow_target`), +// where a `Box>` answers as `Vec` does instead of failing a +// last-path-segment test. Its one non-structural rule — `&mut [T]` is refused, +// because mutate-back semantics keep the `input_vec` path — survives as the +// `RefMode::Shared` guard on that match. /// `Some((element_type, by_ref))` when `arg_ty` is a slice/`Vec` input whose /// element is a **flattenable `data_class`** — i.e. it decomposes into the @@ -32,11 +28,46 @@ pub(crate) fn slice_or_vec_elem(arg_ty: &syn::Type) -> Option<(syn::Type, bool)> pub(crate) fn vec_build_elem( ext: &Declarations, registry: &Registry, - arg_ty: &syn::Type, -) -> Option<(syn::Type, bool)> { - let (elem, by_ref) = slice_or_vec_elem(arg_ty)?; + arg: &TypeRef, +) -> Option<(TypeRef, bool)> { + // The run and its element off the MODEL. `&mut [T]` is still refused — + // mutate-back semantics keep the `input_vec` path — and that is the one + // fact the layer accessors do not carry, so `RefMode` is read directly. + // + // The conversion follows the SYNTAX, and must: this path builds a Rust-side + // `Vec` and hands the source fn a borrow of it (or `mem::take`s it), so + // the referent has to be a form that built value satisfies. `&[T]` + // deref-coerces and `Vec` is the thing itself; `Box>` and + // `Cow<'_, [T]>` classify identically and cannot be rebuilt from the local. + // `decoded_vec_satisfies` in `selector.rs` is the same rule guarding the + // general converter path — asked here of the model, which holds both halves. + // + // BEFORE the peel as well as after, and the order is the whole point: the + // erasure happens OUTSIDE the layer it wraps, so `Box<&Vec>` classifies + // as `Ref` and interpreting `kind` first would replace `arg` with the inner + // sequence — whose spelling is a clean `Vec` — and let the outer `Box` + // through unseen. Every layer is checked on the way down, the way + // `rebuildable_target` does it. + if arg.erased_wrapper().is_some() { + return None; + } + let (run, by_ref) = match arg.kind() { + flat::TypeKind::Ref { mode, inner } if *mode == RefMode::Shared => (&**inner, true), + _ => (arg, false), + }; + // Still needed after the peel: `&Box>` puts the wrapper on the + // referent, where the check above (a `syn::Type::Reference`) cannot see it. + if run.erased_wrapper().is_some() { + return None; + } + let elem = run.sequence_elem()?; + // The element is spelled into `Vec<#elem>` and rebuilt per push, so a + // wrapped element spelling is unbuildable for the same reason. + if elem.erased_wrapper().is_some() { + return None; + } // The element must flatten; the probe ident is irrelevant here. - let plan = build_flat_input_plan(ext, registry, &format_ident!("e"), &elem) + let plan = build_flat_input_plan(ext, registry, &format_ident!("e"), elem) .ok() .flatten()?; // Recursive/optional element decomposition is intentionally outside this @@ -51,7 +82,7 @@ pub(crate) fn vec_build_elem( { return None; } - Some((elem, by_ref)) + Some((elem.clone(), by_ref)) } /// Every distinct flattenable element type `T` that a scanned, declared function @@ -62,22 +93,18 @@ pub(crate) fn vec_build_elem( pub(crate) fn collect_vec_build_elem_types( ext: &Declarations, registry: &Registry, -) -> Vec { +) -> Vec { let declared = ext.declared_functions(); - let mut seen: std::collections::BTreeMap = std::collections::BTreeMap::new(); - for (ident, item_fn) in registry - .flat() - .functions() - .map(|f| (&f.name, &f.origin.syntax)) - { - if !declared.contains(ident) { + let mut seen: std::collections::BTreeMap = std::collections::BTreeMap::new(); + // Over the model's params, which already carry a reading each — the + // `sig.inputs` walk had to re-derive one per argument. + for f in registry.flat().functions() { + if !declared.contains(&f.name) { continue; } - for input in &item_fn.sig.inputs { - if let syn::FnArg::Typed(pt) = input { - if let Some((elem, _)) = vec_build_elem(ext, registry, &pt.ty) { - seen.insert(TypeKey::from_type(&elem).as_str().to_string(), elem); - } + for p in &f.params { + if let Some((elem, _)) = vec_build_elem(ext, registry, &p.ty) { + seen.insert(elem.key().as_str().to_string(), elem); } } } @@ -101,7 +128,7 @@ pub(crate) struct VecBuildHelpers { pub(crate) fn vec_build_helpers( ext: &Declarations, registry: &Registry, - elem: &syn::Type, + elem: &TypeRef, ) -> Option { let plan = build_flat_input_plan(ext, registry, &format_ident!("e"), elem) .ok() @@ -115,7 +142,7 @@ pub(crate) fn vec_build_helpers( { return None; } - let key = TypeKey::from_type(elem); + let key = elem.key(); let kt_fqn = ext .types .get(&key) @@ -168,8 +195,11 @@ pub(crate) fn build_vec_build_helper_items( registry: &Registry, ) -> Vec { let mut named: Vec<(String, syn::Item)> = Vec::new(); - for elem in collect_vec_build_elem_types(ext, registry) { - let Some(h) = vec_build_helpers(ext, registry, &elem) else { + for elem_reading in collect_vec_build_elem_types(ext, registry) { + // Generated Rust spells `origin.syntax`; the reading is what the plan + // and the key are taken from. + let elem = elem_reading.syntax(); + let Some(h) = vec_build_helpers(ext, registry, &elem_reading) else { continue; }; let new_sym = vec_helper_symbol(ext, &h.base, "New"); diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index d1edd6bd..0d674792 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -553,6 +553,8 @@ fn emit_input_param( // Vec the Kotlin `finally` frees). Decode is infallible, like the // by-value-handle consume below. InputKind::VecBuild { elem, by_ref } => { + // Generated Rust spells the reading's own tokens. + let elem = elem.syntax(); let handle_ident = format_ident!("{}_handle", arg_ident); wire_params.push(quote!(#handle_ident: jni::sys::jlong)); if *by_ref { @@ -610,16 +612,20 @@ fn emit_input_param( | InputKind::Handle { .. } | InputKind::Unsigned64 { .. } | InputKind::Plain => { - let entry = registry - .reading_of(arg_ty) - .and_then(|tr| registry.input_entry(&tr)) - .unwrap_or_else(|| { - panic!( - "JniGen::on_function: input type `{}` for `{}` is unresolved", - TypeKey::from_type(arg_ty), - original_ident, - ) - }); + // The leaf's reading — for `ParamForm::Single` it is the very + // reading `param.ty` was spelled from, so this is the same lookup + // without the round trip. The panic now CALLS the shared message + // instead of restating it, which is what `PlanError::message`'s doc + // has always claimed and hand-duplication did not deliver. + let entry = registry.input_entry(&leaf.reading).unwrap_or_else(|| { + panic!( + "{}", + PlanError::Unresolved { + ty: Box::new(leaf.reading.clone()) + } + .message(original_ident) + ) + }); emit_plain_decode(entry, arg_ident, arg_ty, on_err) } } @@ -761,16 +767,19 @@ pub(crate) fn emit_expanded_param( for (leaf, classified) in plan.leaves.iter().zip(leaves) { let leaf_ty = leaf.ty.syntax(); let lookup_entry = || { - registry - .reading_of(leaf_ty) - .and_then(|tr| registry.input_entry(&tr)) - .unwrap_or_else(|| { - panic!( - "JniGen expand: leaf type `{}` (parameter `{}`) is unresolved", - TypeKey::from_type(leaf_ty), - orig_param, - ) - }) + // The leaf's own reading goes straight to the entry: spelling it and + // looking the same reading back up is the round trip #286 removed. + registry.input_entry(&leaf.ty).unwrap_or_else(|| { + // Shared wording, not restated — see the sibling backstop above. + panic!( + "{}", + PlanError::UnresolvedLeaf { + ty: Box::new(leaf.ty.clone()), + param: orig_param.clone(), + } + .message(orig_param) + ) + }) }; let local = format_ident!("__exp_{}", leaf.name); diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index 42023361..65ae13a0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -11,7 +11,7 @@ //! plan to function granularity; the output side follows in a later stage. use super::*; -use crate::api::core::registry::Conversions; +use crate::api::core::{flat::TypeRef, registry::Conversions}; /// The lowered plan for one bound function: one [`PlanParam`] per source /// `syn::Signature` parameter (non-`Typed`/non-`Ident` args — `self`, @@ -68,7 +68,7 @@ pub(crate) struct PlanLeaf { /// The leaf's **reading** — classification and spelling in one value, so /// the two cannot disagree and no consumer has to look the type up. Spell /// with `reading.origin.syntax`. - pub reading: crate::api::core::flat::TypeRef, + pub reading: TypeRef, /// Kotlin parameter name (`kt_param_name(ident)`: camelCase + /// hard-keyword escaping) — shared by the wrapper signature and the /// `external fun` declaration. @@ -110,7 +110,9 @@ pub(crate) enum InputKind { Callback { iface: Option> }, /// `&[T]` / `Vec` of a flattenable data_class: a single `jlong` /// Vec-handle on the wire, built by pushing element leaves. - VecBuild { elem: syn::Type, by_ref: bool }, + /// The element as a **reading**: the vec-helper plan and the element key + /// are both taken from it, and generated Rust spells `elem.syntax()`. + VecBuild { elem: TypeRef, by_ref: bool }, /// Bare `Option` / `Option`: a decoupled /// `(present: jboolean, value: )` pair. OptionScalar(OptionScalarInputPlan), @@ -222,11 +224,32 @@ pub(crate) enum ReturnSurface { #[derive(Debug)] pub(crate) enum PlanError { /// `registry.input_entry` has no converter for a source param type. - Unresolved { ty: TypeKey }, + /// + /// The **reading**, not a key: the registry knew the type well enough to + /// classify it — what it lacks is a converter — so the reading is in hand, + /// and it carries the source position [`Self::message`] points at. + /// + /// **Boxed**, and it has to be. A `TypeRef` holds a `syn::Type` inline and + /// runs to ~264 bytes; a `Result` is sized by its largest variant, so an + /// unboxed reading here would widen every `Result<_, PlanError>` on this + /// path — the *success* return included, which is the one that always + /// happens (`clippy::result_large_err`). An error is rare enough to afford + /// the allocation; the plans it returns beside are not. + Unresolved { ty: Box }, /// No converter for a constructor-expansion leaf type. - UnresolvedLeaf { ty: TypeKey, param: syn::Ident }, - /// `registry.output_entry` has no converter for the output target type. - UnresolvedOutput { ty: TypeKey }, + UnresolvedLeaf { ty: Box, param: syn::Ident }, + /// The output target type is known but `registry.output_entry` has no + /// converter for it — the failure `output_wrapper` fixes. + UnresolvedOutput { ty: Box }, + /// The output target type is not in the registry **at all**, so there is no + /// reading to hold and `output_wrapper` is not the answer. + /// + /// Split from [`Self::UnresolvedOutput`] because the two want opposite + /// advice: one type needs a converter written for it, the other needs to + /// reach the registry first. Collapsed together, the `output_wrapper` + /// message sent the reader to write a converter for a type the resolver + /// would still never ask about. + UnknownOutputType { ty: TypeKey }, /// An unmarked declared data class could not produce a complete recursive /// input plan. Silent `JObject` fallback is forbidden. UnflattenableDataClass(FlatInputError), @@ -236,25 +259,63 @@ pub(crate) enum PlanError { } impl PlanError { + /// Where the offending type was written, when a file wrote it — the + /// suffix [`Self::message`] appends. + /// + /// `has_position` gates it exactly as `resolve.rs` gates + /// `UnresolvedEntry::location`: a composed type and a test's hand-built + /// stream are lowered against `SourceLocation::default`, and printing + /// `:0:0` for them would make a fabricated position look like a real one. + fn location_suffix(&self) -> String { + let reading = match self { + PlanError::Unresolved { ty } + | PlanError::UnresolvedLeaf { ty, .. } + | PlanError::UnresolvedOutput { ty } => ty, + PlanError::UnknownOutputType { .. } + | PlanError::UnflattenableDataClass(_) + | PlanError::JvmParameterLimit { .. } => return String::new(), + }; + let loc = reading.location(); + if loc.has_position() { + format!(" (declared at {loc})") + } else { + String::new() + } + } + /// The historical emission-panic message for this failure, shared by the /// validation boundary and the Rust emitter's backstop panics so the /// wording cannot drift. + /// + /// The base wording is unchanged; a source position is appended when the + /// reading has one, so a backstop that reaches the same failure without a + /// reading still prints a prefix of this. pub fn message(&self, fn_ident: &syn::Ident) -> String { + let at = self.location_suffix(); match self { PlanError::Unresolved { ty } => format!( - "JniGen::on_function: input type `{}` for `{}` is unresolved", - ty, fn_ident, + "JniGen::on_function: input type `{}` for `{}` is unresolved{at}", + ty.key(), + fn_ident, ), PlanError::UnresolvedLeaf { ty, param } => format!( - "JniGen expand: leaf type `{}` (parameter `{}`) is unresolved", - ty, param, + "JniGen expand: leaf type `{}` (parameter `{}`) is unresolved{at}", + ty.key(), + param, ), PlanError::UnresolvedOutput { ty } => format!( "JniGen::on_function: return type `{}` of `{}` has no registered output \ converter — register one via `Declarations::output_wrapper(pat, |…| Some((ty, exc, body)))` \ (exc = `None` for non-throwing, `Some(parse_quote!())` \ - to bind a domain exception)", - ty, fn_ident, + to bind a domain exception){at}", + ty.key(), + fn_ident, + ), + PlanError::UnknownOutputType { ty } => format!( + "JniGen::on_function: return type `{}` of `{}` is not registered — the \ + resolver never saw this type, so no converter can be selected for it. \ + Declare the type (or the function that produces it) before binding `{}`", + ty, fn_ident, fn_ident, ), PlanError::UnflattenableDataClass(error) => { format!("JniGen::on_function `{fn_ident}`: {}", error.message()) @@ -531,28 +592,34 @@ fn classify_leaf( ext: &Declarations, registry: &Registry, ident: &syn::Ident, - reading: &crate::api::core::flat::TypeRef, + reading: &TypeRef, expanded: bool, source_param: &syn::Ident, ) -> Result { - // The reading, so the layer questions cannot miss. What generated Rust must - // spell is `origin.syntax`, unchanged. - let ty = reading.syntax(); + // Every question below is the model's now — the local spelling this function + // opened with has no users left. let optional = reading.optional_inner().is_some(); - let as_enum_value = ext.is_kotlin_enum(&enum_probe_type(ty)); + // The enum probe off the reading — the layers it peels are the model's own + // (`&`, `Option`), so there is nothing to re-spell and nothing to look up. + let as_enum_value = ext.is_kotlin_enum_reading(reading); let kt_name = kt_param_name(&ident.to_string()); // `impl Fn(args)` first: typed entirely from the interface spec — the // erased entry exists but its metadata carries no surface type. - if let Some(args) = extract_fn_trait_args(ty) { - let iface = ext.iface_spec(registry, &SpecKey::callback(&args)); + if let Some(args) = reading.callback_args() { + // `SpecKey` is a memo key and holds `TypeKey`s, so the args reach it as + // spellings either way — but as each arg reading's OWN spelling now, + // rather than one re-extracted from the parameter's bounds. + // `a_callback_identity_is_the_same_from_the_reading_or_the_syntax` + // pins that the two routes are one memo identity. + let arg_tys: Vec = args.iter().map(|a| a.syntax().clone()).collect(); + let iface = ext.iface_spec(registry, &SpecKey::callback(&arg_tys)); return Ok(PlanLeaf { reading: reading.clone(), kt_name, kt_public: None, kt_meta: registry - .reading_of(ty) - .and_then(|tr| registry.input_entry(&tr)) + .input_entry(reading) .and_then(|e| e.metadata.kotlin_name.clone()), optional, as_enum_value, @@ -562,29 +629,30 @@ fn classify_leaf( // Every non-callback leaf requires a resolved input entry — the same // hard boundary the Rust emitter has always enforced. - let Some(entry) = registry - .reading_of(ty) - .and_then(|tr| registry.input_entry(&tr)) - else { - let key = TypeKey::from_type(ty); + let Some(entry) = registry.input_entry(reading) else { + // The reading itself, so the diagnostic can say where the type was + // written. Reaching here means the type IS classified and merely has no + // converter, which is why there is something to carry. return Err(if expanded { PlanError::UnresolvedLeaf { - ty: key, + ty: Box::new(reading.clone()), param: source_param.clone(), } } else { - PlanError::Unresolved { ty: key } + PlanError::Unresolved { + ty: Box::new(reading.clone()), + } }); }; - let flat_plan = build_flat_input_plan(ext, registry, ident, ty) + let flat_plan = build_flat_input_plan(ext, registry, ident, reading) .map_err(PlanError::UnflattenableDataClass)?; let kind = if let Some((elem, by_ref)) = (!expanded) - .then(|| vec_build_elem(ext, registry, ty)) + .then(|| vec_build_elem(ext, registry, reading)) .flatten() { InputKind::VecBuild { elem, by_ref } - } else if let Some(sp) = build_option_scalar_input_plan(ext, registry, ident, ty) { + } else if let Some(sp) = build_option_scalar_input_plan(ext, registry, ident, reading) { InputKind::OptionScalar(sp) } else if let Some(plan) = flat_plan { InputKind::FlattenStruct(plan) @@ -694,14 +762,21 @@ fn build_output( .expect("Return delivery carries convert_out_ty"), None => ok_ty.unwrap_or(return_ty), }; - let Some(entry) = registry - .reading_of(&target_ty) - .and_then(|tr| registry.output_entry(&tr)) - else { - return Err(PlanError::UnresolvedOutput { + // Two failures, told apart. `target_ty` is composed here (`convert_out_ty`, + // or the `Result` peeled to its `Ok`), so unlike the input side there may + // genuinely be no reading — and "not registered" wants different advice + // from "registered, no converter". Collapsed into one `and_then`, both got + // the `output_wrapper` message and the first one got it wrong. + let Some(target) = registry.reading_of(&target_ty) else { + return Err(PlanError::UnknownOutputType { ty: TypeKey::from_type(&target_ty), }); }; + let Some(entry) = registry.output_entry(&target) else { + return Err(PlanError::UnresolvedOutput { + ty: Box::new(target), + }); + }; let wire_ty = entry.destination.clone(); // The Kotlin surface classifies the DECLARED return — `convert_out_ty` diff --git a/prebindgen/src/api/lang/jnigen/jni/fold.rs b/prebindgen/src/api/lang/jnigen/jni/fold.rs index 9299a017..54153eec 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fold.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fold.rs @@ -5,12 +5,41 @@ //! via `use super::*`. use super::*; +use crate::api::core::flat::TypeRef; -/// Peel a leading `&`/`&mut` and an `Option<…>` layer to expose the inner type -/// used for enum detection. So `&Priority`, `Priority`, and `Option` -/// all probe as `Priority` — letting nullable enum params (`Option`) wire -/// as `Int?` + `?.value` just like a non-null enum wires as `Int` + `.value`, -/// instead of leaking the enum object to the (boxed-int-expecting) Rust converter. +/// Peel the layers that never change whether a value's core is a Kotlin enum, +/// **off the model**: a borrow and an optional, in any nesting. So `&Priority`, +/// `Priority`, `Option` and `Option<&Priority>` all probe as +/// `Priority` — letting nullable enum params (`Option`) wire as `Int?` + +/// `?.value` just like a non-null enum wires as `Int` + `.value`, instead of +/// leaking the enum object to the (boxed-int-expecting) Rust converter. +/// +/// A **run is not peeled**. `Vec` is a `List`, not an enum, +/// so this is deliberately not [`TypeRef::layer_stack`], which strips the +/// sequence layer too. +/// +/// Borrowing rather than composing is not a shortcut: every layer of a reading +/// already holds the next as a `TypeRef` of its own, so there is nothing to +/// mint — which is also why this needs no registry. What it returns spells +/// itself (`origin.syntax`) and classifies itself (`kind`), and the two cannot +/// disagree. +pub(crate) fn enum_probe(reading: &TypeRef) -> &TypeRef { + let mut cur = reading; + while let Some(inner) = cur.borrow_target().or_else(|| cur.optional_inner()) { + cur = inner; + } + cur +} + +/// [`enum_probe`] over a bare spelling, for the one caller that does not have a +/// reading to peel: [`unfold_leaf_kt`](super::render::unfold_leaf_kt), whose +/// `out_ty` arrives as `syn::Type` through `leaf_iface_param` from +/// `UnfoldPlan::element` and `LeafDesc::Whole`. Both must become `TypeRef`s +/// before this can go — an element-walking change, not a helper swap. +/// +/// It answers about the WRAPPER where the model answers about the type: +/// `Box` probes as `Box` here and as `Priority` there. +/// Prefer [`enum_probe`] wherever a reading is in hand. pub(crate) fn enum_probe_type(ty: &syn::Type) -> syn::Type { let stripped = match ty { syn::Type::Reference(r) => (*r.elem).clone(), diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index c62e552c..1e46d539 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -793,18 +793,15 @@ impl Declarations { // nothing is looked up (#275). let field_ty = field.ty.syntax(); let where_ = || format!("sealed_class!({}) payload `{variant}.{prop}`", sum_name); - let out = registry - .reading_of(field_ty) - .and_then(|tr| registry.output_entry(&tr)) - .unwrap_or_else(|| { - panic!( - "{}: `{}` has no resolved OUTPUT converter, so the Kotlin surface for it \ + let out = registry.output_entry(&field.ty).unwrap_or_else(|| { + panic!( + "{}: `{}` has no resolved OUTPUT converter, so the Kotlin surface for it \ cannot be derived — register converters for the payload type before \ declaring the sealed class", - where_(), - field_ty.to_token_stream(), - ) - }); + where_(), + field_ty.to_token_stream(), + ) + }); if let Some(h) = out.metadata.projection.clone() { let leaf = projection_leaf_kt(self, &h).unwrap_or_else(|| { @@ -836,10 +833,7 @@ impl Declarations { // boxed value, a present flag, a niche) rather than in the type name. // Comparing the rendered types would reject that legitimate shape — // which is what an `Option` payload does. - if let Some(inp) = registry - .reading_of(field_ty) - .and_then(|tr| registry.input_entry(&tr)) - { + if let Some(inp) = registry.input_entry(&field.ty) { if let (Some(in_ty), (Some(a), Some(b))) = ( inp.metadata.kotlin_name.clone(), ( @@ -1079,19 +1073,17 @@ impl Declarations { .collect(); for ident in &declared_idents { { - let Some(item_fn) = registry - .flat() - .function(&ident) - .map(|func| &func.origin.syntax) - else { + // The ELEMENT, so the callback params come off the model's own + // classification rather than a second walk of the bounds. + let Some(func) = registry.flat().function(&ident) else { continue; }; - for input in &item_fn.sig.inputs { - let syn::FnArg::Typed(pt) = input else { - continue; - }; - if let Some(cb_args) = extract_fn_trait_args(&pt.ty) { - uses.insert(SpecKey::callback(&cb_args)); + let item_fn = &func.origin.syntax; + for p in &func.params { + if let Some(cb_args) = p.ty.callback_args() { + let arg_tys: Vec = + cb_args.iter().map(|a| a.syntax().clone()).collect(); + uses.insert(SpecKey::callback(&arg_tys)); } } if let Some(plan) = registry @@ -1525,12 +1517,12 @@ impl Declarations { // it `Int` and the wrap has to name the enum class itself — read off the // same output-converter metadata `factory_field` reads for an enum // struct field. - if self.is_kotlin_enum(&enum_probe_type(leaf.out_ty.syntax())) { - let inner = option_inner_type(leaf.out_ty.syntax()) - .unwrap_or_else(|| leaf.out_ty.syntax().clone()); + if self.is_kotlin_enum_reading(&leaf.out_ty) { + // The `Option` layer peeled off the model, so the entry lookup takes + // the layer's own reading instead of a spelling to look back up. + let inner = leaf.out_ty.optional_inner().unwrap_or(&leaf.out_ty); let name = registry - .reading_of(&inner) - .and_then(|tr| registry.output_entry(&tr)) + .output_entry(inner) .and_then(|e| e.metadata.kotlin_name.clone()) .and_then(|t| t.leaf_name().map(str::to_string)) .unwrap_or_else(|| { diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index afd63a09..b66092f3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -52,7 +52,7 @@ pub(crate) use crate::api::{ domain::ScalarValue, niches::{NicheSlot, Niches}, prebindgen::{ConverterImpl, Prebindgen, Stage}, - registry::{extract_fn_trait_args, Direction, Registry, TypeKey}, + registry::{Direction, Registry, TypeKey}, types_util::{bare_path_ident, option_inner_type, vec_inner_type}, }, gen::kotlin::WriteKotlinError, diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs index 9fe7d992..c02aae58 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs @@ -1610,3 +1610,313 @@ fn a_borrowed_transparent_sequence_wrapper_is_not_decoded_as_a_vec() { "the refusal must name the wrapper spelling the binding cannot convert:\n{msg}" ); } + +/// The enum probe answers about the **type**, not about the wrapper around it. +/// +/// `is_kotlin_enum_reading` peels with [`enum_probe`], which walks the model's +/// own layers, and then keys on `TypeKind::Named` — so every spelling of "a +/// `Priority`, held some way" reaches the `enum_class!(Priority)` declaration. +/// The spelling-keyed `is_kotlin_enum` cannot: `Box` canonicalizes to +/// `Box < Priority >`, which no declaration ever registered, and the answer is +/// `false` about a type that IS a Kotlin enum. Both are asserted here, because +/// the difference between them is the reason the reading-taking one exists. +/// +/// A **run is not peeled**, and that is the half a `layer_stack`-based probe +/// would get wrong: `Vec` is a `List` on the Kotlin side, so +/// treating it as an enum would wire a list to a `.value` discriminant. +/// +/// `Probe` is deliberately undeclared — jnigen is opt-in, so it emits nothing, +/// and it exists only to give the model a field per spelling to classify. +#[test] +fn the_enum_probe_sees_through_wrappers_a_spelling_key_misses() { + use crate::api::core::flat; + + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum Priority { + Low = 1, + High = 2, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct Probe { + pub plain: Priority, + pub borrowed: Box, + pub optional: Option, + pub boxed_optional: Box>, + pub optional_borrow: Option>, + pub run: Vec, + pub unrelated: i64, + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let gen = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package(crate::package!().class(crate::enum_class!(Priority))) + .build_with(registry) + .expect("resolve"); + let (ext, registry) = (gen.declarations(), gen.registry()); + + let flat::Type::Struct(probe) = registry.flat().declared_type("Probe").expect("indexed") else { + panic!("Probe is a struct"); + }; + let field = |name: &str| { + &probe + .fields + .iter() + .find(|f| f.name.as_ref().is_some_and(|n| n == name)) + .unwrap_or_else(|| panic!("field `{name}`")) + .ty + }; + + for name in [ + "plain", + "borrowed", + "optional", + "boxed_optional", + "optional_borrow", + ] { + let reading = field(name); + assert!( + ext.is_kotlin_enum_reading(reading), + "`{name}` holds a declared Kotlin enum, however it is wrapped — the \ + probe peels the model's layers, so it must say so" + ); + } + + for name in ["run", "unrelated"] { + assert!( + !ext.is_kotlin_enum_reading(field(name)), + "`{name}` is not an enum value: a run of enums is a `List`, and the \ + probe must not peel the sequence layer to reach the element" + ); + } + + // The difference from the spelling-keyed probe, pinned: a transparent + // wrapper is invisible to the model and decisive for a canonical key. + assert!( + !ext.is_kotlin_enum(field("borrowed").syntax()), + "if the spelling key ever started seeing through `Box`, this test would \ + stop distinguishing the two probes" + ); + assert!(ext.is_kotlin_enum(field("plain").syntax())); +} + +/// A **transparently-wrapped** parameter spelling does not take a specialized +/// lowering that would rebuild the unwrapped type. +/// +/// The model erases `Box`/`Cow` ([`TRANSPARENT_WRAPPERS`]), so +/// `Box>` classifies as `Optional` exactly as `Option` does — +/// and reading the layers off the model is what the reading-based probes were +/// changed to do. But `build_option_scalar_input_plan` does not *decode* the +/// parameter, it **rebuilds** it: the emitter writes a literal +/// `Option::Some(v)` / `Option::None` and hands that to the source function. +/// Handing a bare `Option` to a parameter spelled `Box>` is +/// an `E0308` in the generated crate. +/// +/// So the selection asks the spelling too ([`rebuilt_value_satisfies`]) and +/// declines, exactly as `decoded_vec_satisfies` makes the general converter path +/// decline `&Box>` (see +/// `a_borrowed_transparent_sequence_wrapper_is_not_decoded_as_a_vec`). +/// +/// **What this pins is the refusal**, and it is asserted on the *pair* so it +/// cannot pass vacuously: the bare twin must still take the decoupled +/// `(present, value)` wire, and the wrapped one must not. The generated Rust is +/// never compiled by this suite (#269), so the `E0308` itself is out of reach — +/// the reachable property is that the emitter is never asked to write it. +#[test] +fn a_transparently_wrapped_option_does_not_take_the_present_value_pair() { + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum Mode { + A, + B, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_bare(mode: Option) { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_boxed(mode: Box>) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package(crate::package!().class(crate::enum_class!(Mode))) + .package( + crate::package!("cfg") + .fun(crate::fun!(z_bare)) + .fun(crate::fun!(z_boxed)), + ); + + let dir = unique_test_dir("jnigen_wrapped_optscalar"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + + // The wrapped spelling may legitimately fail to resolve a converter of its + // own — that is a refusal too, and equally not an `E0308`. Only a build that + // SUCCEEDS can be asked what it emitted. + let Ok(gen) = jni.build_with(registry) else { + return; + }; + let kdir = dir.join("kotlin"); + let paths = gen.write_kotlin(&kdir).expect("write_kotlin"); + let kotlin: String = paths + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + let kc: String = kotlin.split_whitespace().collect(); + + // The control: the bare twin still takes the decoupled pair, so a refusal + // below is about the wrapper and not about the fixture failing to reach the + // specialized path at all. + assert!( + kc.contains("zBare(modePresent:Boolean,modeValue:Int"), + "the bare `Option` must still cross as (present, value) — \ + otherwise this test proves nothing about the wrapped one:\n{kotlin}" + ); + + // The finding: `Box>` must NOT, because the emitter would + // rebuild a bare `Option` for a parameter that is not one. + assert!( + !kc.contains("zBoxed(modePresent"), + "`Box>` took the present/value lowering, which rebuilds a \ + bare `Option` and hands it to a fn expecting `Box>` \ + — an E0308 in the generated crate:\n{kotlin}" + ); + // What it takes instead: the ordinary boxed-`Int?` optional wire, whose + // converter is selected by `selector.rs` — the path that carries its own + // spelling guards. + assert!(kc.contains("zBoxed(mode:Int?"), "{kotlin}"); +} + +/// The transparent-wrapper guard runs **before** the model's layers are +/// interpreted, not after. +/// +/// An erasure sits *outside* the layer it wraps, so `Box<&Vec>` classifies +/// as `TypeKind::Ref` — the `Box` is gone from `kind` and survives only in the +/// spelling. A guard that reads `kind` first replaces the argument with the +/// inner sequence reading, whose own spelling is a clean `Vec`, and the +/// outer wrapper is never seen: the Vec-build plan is selected, its emitter +/// hands the source fn a `&[Foo]` built from the transient Rust-side `Vec`, and +/// the parameter still spells `Box<&Vec>`. That is the same `E0308` class +/// [`a_transparently_wrapped_option_does_not_take_the_present_value_pair`] +/// covers, reached by peeling in the wrong order. +/// +/// So this pins the **ordering**, which the shape-by-shape tests cannot: every +/// layer is checked on the way down, and the outermost is checked first. +#[test] +fn an_outer_wrapper_around_a_reference_is_seen_before_the_layers_are_read() { + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct Foo { + pub id: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn put_bare(v: &[Foo]) { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn put_wrapped(v: Box<&Vec>) { + unimplemented!() + } + )), + loc.clone(), + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!("foo") + .class(crate::data_class!(Foo)) + .fun(crate::fun!(put_bare)) + .fun(crate::fun!(put_wrapped)), + ); + + let dir = unique_test_dir("jnigen_outer_wrapper_ref"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + + // The wrapped spelling may legitimately resolve no converter of its own — + // that is a refusal too, and equally not an `E0308`. + let Ok(gen) = jni.build_with(registry) else { + return; + }; + let kdir = dir.join("kotlin"); + let paths = gen.write_kotlin(&kdir).expect("write_kotlin"); + let kotlin: String = paths + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + let kc: String = kotlin.split_whitespace().collect(); + + // The control: the bare `&[Foo]` twin still takes the Vec-build handle path, + // so a refusal below is about the wrapper and not about the fixture failing + // to reach the specialized path at all. + assert!( + kc.contains("fooVecNew"), + "the bare `&[Foo]` must still take the Vec-build path — otherwise this \ + test proves nothing about the wrapped one:\n{kotlin}" + ); + assert!( + kc.contains("val__vec_v=JNINative.fooVecNew(v.size)"), + "{kotlin}" + ); + + // The finding: `Box<&Vec>` must not reach the Vec-build call site. Its + // wrapper is invisible to `kind` (which says `Ref`), so only a check made + // before the layers are read can catch it. + // Split on the WRAPPER, not on `externalfunputWrapped(` in `JNINative` — + // the extern block is followed by the shared `fooVecNew` declarations, so a + // looser split would read them as this function's body and pass falsely. + let wrapped_body = kc + .split("publicfunputWrapped(") + .nth(1) + .map(|s| s.split("publicfun").next().unwrap_or(s).to_string()) + .unwrap_or_default(); + assert!( + !wrapped_body.contains("fooVecNew"), + "`Box<&Vec>` took the Vec-build path, which hands the source fn a \ + `&[Foo]` built from a transient Vec while the parameter spells \ + `Box<&Vec>` — an E0308 in the generated crate:\n{kotlin}" + ); +} diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index b01302c6..4701596f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -1378,7 +1378,6 @@ impl Declarations { } }; for f in registry.flat().functions() { - let item_fn = &f.origin.syntax; // `Vec` / `Option>` return. The model's `ret` already // normalizes an elided return to `()`, so there is no arm for it. { @@ -1389,16 +1388,15 @@ impl Declarations { consider(peel_leading_ref(&elem)); } } - // `impl Fn(&[T])` / `impl Fn([T])` callback arg. - for input in &item_fn.sig.inputs { - let syn::FnArg::Typed(pt) = input else { - continue; - }; - let Some(args) = crate::api::core::registry::extract_fn_trait_args(&pt.ty) else { + // `impl Fn(&[T])` / `impl Fn([T])` callback arg. Over the model's + // params, whose readings already say which ones ARE callbacks — + // walking `sig.inputs` re-extracted that from the bounds. + for p in &f.params { + let Some(args) = p.ty.callback_args() else { continue; }; for arg in args { - if let syn::Type::Slice(s) = &peel_leading_ref(&arg) { + if let syn::Type::Slice(s) = &peel_leading_ref(arg.syntax()) { consider(peel_leading_ref(&s.elem)); } } @@ -1533,13 +1531,12 @@ impl Prebindgen for Declarations { // something that must not exist. Reject them here, where the message // can say what is actually unsupported and what to write instead. for ident in self.declared_functions() { - let Some(item_fn) = binding - .flat() - .function(&ident) - .map(|func| &func.origin.syntax) - else { + // The ELEMENT, not just its syntax: check (3) below asks its params + // which are callbacks, which is the model's answer, not the tokens'. + let Some(func) = binding.flat().function(&ident) else { continue; }; + let item_fn = &func.origin.syntax; // (1) A sum in the `Ok` position of a fallible return. A sum is // delivered DECOMPOSED through a builder callback, and the // `Result` lane has no builder: a `Result` return deliberately @@ -1617,15 +1614,12 @@ impl Prebindgen for Declarations { // (`impl Fn(&[E])`): the element fold would need the sum's // folder-appender singleton, which is emitted per `Vec` RETURN // position, so the shape resolves to nothing. - for input in &item_fn.sig.inputs { - let syn::FnArg::Typed(pt) = input else { - continue; - }; - let Some(args) = extract_fn_trait_args(&pt.ty) else { + for p in &func.params { + let Some(args) = p.ty.callback_args() else { continue; }; for arg in args { - let after_ref = match &arg { + let after_ref = match arg.syntax() { syn::Type::Reference(r) => (*r.elem).clone(), other => other.clone(), }; diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index 8fa56307..3c52b5fd 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -117,13 +117,19 @@ mod spelling_census { // The L4 "layer questions" remainder — #229. Not migrated here because // it is a separate consumer and bundling it would make one review of // both impossible. - ("jni/emit/flat_input.rs", 20), + ("jni/emit/flat_input.rs", 18), ("jni/emit/struct_out.rs", 2), - ("jni/emit/vec_build.rs", 1), ("jni/emit/wrapper.rs", 2), + // + // vec_build.rs is absent, and off the boundary ledger too: its element + // peel reads `sequence_elem`/`borrow_target` off the model now. + // + // fold.rs's one call is inside `enum_probe_type`, the spelling twin of + // `enum_probe` kept for `unfold_leaf_kt`'s `syn::Type` callers. ("jni/fold.rs", 1), ("jni/iface.rs", 2), - ("jni/kotlin_emit.rs", 1), + // kotlin_emit.rs is off the census: `sum_ctor_arg`'s enum payload peels + // its `Option` off the leaf's own reading. ("jni/trait_impl.rs", 4), // Down from 2: the nullability decisions now ask the model. The one // left probes for an enum through its layers. From 8d7904354ad6e0efde7a1b1d1568b67da80ccb9d Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 20:42:52 +0200 Subject: [PATCH 37/52] core: the model says what it erased, and one rebuild stops guessing (#293) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TypeKind` erases `Box`/`Cow`, which is right: `Box>` is one optional to every destination language. But conversion follows the SYNTAX, and the two facts a rebuild needs were not on the model. `TypeRef::erased_wrappers()` and `stripped_syntax()`, both derived from the spelling rather than stored — `lower_type` keeps discarding the wrapper, and nothing new can disagree with `syntax`. The stripped spelling is defined by its invariant, not by its loop: it is the spelling whose own lowering yields exactly this `kind`, so the peel runs to a fixed point. `erased_wrapper()` becomes the head of the list. An erasure sits OUTSIDE the layer it wraps, so both answer for one layer's spelling only; the tests pin that with the pair `Box<&Vec>` / `&Box>`, each invisible to the other's vantage point. The audit found one live miscompilation. Builder delivery binds the returned value and matches it against `Option`'s patterns, which match ergonomics does not see through a `Box` — every other peel site classifies, which is the erasure working. `read_through_erased_wrappers` undoes them at the single point the value enters the delivery. Its fixture is in perftest-flat, whose binding covertest compiles: verified by disabling the fix and watching `E0308`, and round-tripped on the JVM. `Box`'s read op drops its parens — every consumer splices into a `let` initializer, where converters happen to `#[allow(unused_parens)]` and wrapper externs do not. Refs #292 (item 1), #229 (L4/L5). --- docs/language-integration.md | 49 +++++++ examples/covertest-kotlin/build.rs | 6 + examples/covertest-kotlin/kotlin/REPORT.md | 2 + .../generated/io/prebindgen/covertest.kt | 2 + .../io/prebindgen/covertest/model.kt | 34 +++++ .../kotlin/io/prebindgen/covertest/Test.kt | 26 ++++ .../src/generated_bindings.rs | 134 +++++++++++++++++- examples/perftest-flat/src/ext.rs | 18 +++ .../src/api/core/flat/tests/acceptance.rs | 107 ++++++++++++++ prebindgen/src/api/core/flat/ty.rs | 72 +++++++++- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 33 ++++- .../api/lang/jnigen/jni/tests/value_form.rs | 12 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 39 ++++- 13 files changed, 522 insertions(+), 12 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 3725cf7a..c808bd89 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -65,6 +65,7 @@ classification stays small and genuinely neutral: | `Foo<'a, T>` | `TypeRef::origin.syntax` | generated Rust only | | "it is a `Foo`" | `TypeKind::Named` | every adapter | | `[u8; TAG_LEN]` — spelling / number / const identity | `TypeRef::origin.syntax` / `ArrayExtent::value` / `ExtentSource::Const` | C header / Kotlin / both | +| the `Box` in `Box>`, and the `Option` under it | `TypeRef::erased_wrappers()` / `stripped_syntax()` — derived from the syntax, not stored | an emitter that **rebuilds or destructures** a Rust value | | where an item came from | `Origin::location` — **absent for a synthesized one** | diagnostics | ### The rule @@ -451,6 +452,48 @@ The long pole — 97 sites, down from 106 because #248 took `jni/builder` from 1 - [ ] `prim_array_of` reads `ArrayExtent` instead of re-matching `Type::Array` - [ ] Generated Rust and Kotlin byte-identical +#### What L4 taught: an erasure sits outside the layer it wraps + +The model erases `Box` and `Cow`, and that erasure is right — `Box>` +is one optional to every destination. But **conversion follows the syntax**, and +the two facts a rebuild needs were not on the model: what was taken off, and what +is left under it. #292 added them as derived readings, `TypeRef::erased_wrappers()` +and `stripped_syntax()`, defined by an invariant rather than by a loop — the +stripped spelling is *the one whose own lowering yields exactly this `kind`*, so +the peel runs to a fixed point (`Box>` classifies as `T`, and one strip +leaves a `Box` that does not match). + +The rule, which outlives the stage: + +> **`kind` is precisely the thing the wrapper is missing from, so interpreting +> `kind` before checking for a wrapper always discards one.** + +`Box<&Vec>` classifies as `Ref`; peel that first and the wrapper is gone from +everywhere a consumer will look. `&Box>` hides it on the referent, where a +question asked of the outer `syn::Type::Reference` cannot see it. Neither check +subsumes the other, so a walk must ask at **every layer, on the way down** — +which is also why the wrapper is a *list*, gathered as the walk descends. + +The audit that came with it found the population is two, and only one has to ask: +a site that **classifies** must never consult the wrapper — that is the erasure +working, and every cbindgen site and all but one jnigen site are of that kind. A +site that **binds a source value and destructures or rebuilds it** must. There +was exactly one unguarded instance, and it was a live miscompilation: builder +delivery bound the returned value and matched it against `Option`'s patterns, +which match ergonomics does not see through a `Box`. Fixed at the single point +the value enters the delivery, not at each of the four matches downstream. + +Two things it taught about evidence: + +* **#290's guards were hand-maintained and wrong twice in one PR.** "Are all the + peel sites guarded?" is answered by inspection until the model carries the + facts and one shared helper consumes them. +* **The suite could not see the defect.** 737 `contains(..)` assertions pass on + Rust that does not compile (#269). The fixture that proves this one is in + `perftest-flat`, whose generated binding covertest `include!`s and **builds** — + the only place in the tree where an `E0308` is a test failure. It was verified + by disabling the fix and watching the build break. + ### L5 — close the seam The public contract stops being `syn`, which is what stops the population from @@ -467,6 +510,12 @@ growing back. `on_enum` split into `on_variant` + `on_enum` along the model's own distinction. Done as #275's first half rather than waiting for this stage, because it was the last thing keeping the spelling accessors alive +- [x] **What a spelling adds over its classification is the model's answer, not + a peel each adapter writes** (#292): `erased_wrappers()` / `stripped_syntax()`. + Belongs here because the alternative was every rebuilding emitter taking a + `syn::Type` apart for itself, which is the population growing back — the + completion criterion below already forbids reconstructing a spelling from a + classification, and this is the fact that makes obeying it possible - [ ] `Prebindgen::post_process_item(&mut syn::Item)` — the hook that let qualification live in an adapter in the first place - [ ] `ConverterImpl::function` / `TypeEntry::function` as `syn::ItemFn`; diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index 0ddb8d8d..8af3a495 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -522,6 +522,12 @@ fn main() { // with the wrong number of dereferences fails the build (#270). .fun(fun!(boxed_note_echo)) .fun(fun!(plain_note_echo)) + // The same wrapper over a DECOMPOSED return (#292). `Summary` + // has an output expansion, so this return takes no converter to + // name the spelling for it — the extern binds the value and + // matches it, and a `Box` match ergonomics cannot see through + // is an `E0308` that only compiling this crate catches. + .fun(fun!(boxed_latest)) .fun(fun!(ledger_new)) .fun(fun!(archive_set_reading)) .fun(fun!(archive_reading)) diff --git a/examples/covertest-kotlin/kotlin/REPORT.md b/examples/covertest-kotlin/kotlin/REPORT.md index ba557452..b743cfcd 100644 --- a/examples/covertest-kotlin/kotlin/REPORT.md +++ b/examples/covertest-kotlin/kotlin/REPORT.md @@ -63,6 +63,8 @@ Base package: `io.prebindgen.covertest` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) - `blob_value_new` — `fun blobValueNew(secs: Long, id: ByteArray, chunks: List, onError: JniErrorHandler): BlobValue` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) +- `boxed_latest` — `fun boxedLatest(a: SummaryVault, onError: JniErrorHandler, build: SummaryBuilder): R?` + - shaped by: return `Summary` decomposed → [count, total] (Callback delivery) - `boxed_note_echo` — `fun boxedNoteEcho(note: String?, onError: JniErrorHandler): String?` - `cache_config_weight` — `fun cacheConfigWeight(cache: CacheConfig?, onError: JniErrorHandler): Int` - `celsius_double` — `fun celsiusDouble(c: Int, onError: JniErrorHandler): Int` diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt index 451fc850..b2a2537e 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt @@ -894,6 +894,8 @@ internal object CovNative { errorSink: Any, ): Any? + external fun boxedLatest(a: Long, build: Any, errorSink: Any): Any? + external fun boxedNoteEcho(note: String?, errorSink: Any): String? external fun cacheConfigWeight( diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt index 96b6d299..cb8a3fb9 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt @@ -12,6 +12,7 @@ import io.prebindgen.covertest.Payload import io.prebindgen.covertest.Ranked import io.prebindgen.covertest.__u64FolderRawHolder import io.prebindgen.covertest.analytics.Summary +import io.prebindgen.covertest.analytics.SummaryBuilder import io.prebindgen.covertest.analytics.SummaryVault import io.prebindgen.covertest.asRaw import io.prebindgen.covertest.u64Callback @@ -1623,6 +1624,39 @@ public fun plainNoteEcho(note: String?, onError: JniErrorHandler): Stri return __ret } +/** + * A transparent wrapper over a **decomposed** return — the shape `boxed_note_echo` + * does not reach. + * + * `boxed_note_echo`'s return takes an output *converter*, which is selected for + * the spelling and therefore names `Box>` itself. This one has + * no converter at all: `Summary` carries a declared output expansion, so the + * extern **binds the returned value and matches it** to deliver the leaves to a + * builder. Match ergonomics does not see through a `Box`, so the emitter has to + * move the value out of the wrappers the classification erased before it can + * destructure — the defect #292's audit found, and one this crate compiles. + * + * The unwrapped twin is [`archive_latest`], which crosses as the same + * `Summary?`. + * + * The Rust `Summary` result is delivered decomposed: the builder callback receives (`count`, `total`). + */ +@Suppress("UNCHECKED_CAST") +public fun boxedLatest( + a: SummaryVault, + onError: JniErrorHandler, + build: SummaryBuilder, +): R? { + if (a.isClosed()) return onError.run("Operation on a closed native handle.") + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = withSortedHandleLocks(a) { + val a_ptr = a.ptr + CovNative.boxedLatest(a_ptr, build, __bcap) + } + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret as R? +} + /** * Build a [`Ledger`]; `n` selects which of the two slots are filled (bit 0 = * `filed`, bit 1 = `archived`), so a caller can drive every arm of the diff --git a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt index 4f335570..5789625f 100644 --- a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt +++ b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt @@ -44,6 +44,9 @@ import io.prebindgen.covertest.model.Unsigned import io.prebindgen.covertest.model.annotatedNew import io.prebindgen.covertest.model.arraysEcho import io.prebindgen.covertest.model.blobValueEcho +import io.prebindgen.covertest.model.boxedLatest +import io.prebindgen.covertest.model.boxedNoteEcho +import io.prebindgen.covertest.model.plainNoteEcho import io.prebindgen.covertest.model.blobValueNew import io.prebindgen.covertest.model.annotatedAlternateValue import io.prebindgen.covertest.model.celsiusDouble @@ -1425,6 +1428,29 @@ fun main() { a.close() } + // ── transparent wrappers: the spelling changes, the crossing must not ─── + // The model erases `Box`/`Cow`, so a wrapped spelling and its unwrapped + // twin are ONE type to Kotlin. Compiling this crate already proves the + // generated Rust is well-typed; these assert the surfaces are the same and + // the values actually make the round trip. + section("transparent wrapper crossings") { + // Converted return: the converter is selected for the spelling, so it + // names `Box>` itself. + for (note in listOf("wrapped", null)) { + check(boxedNoteEcho(note, boom) == note) + check(plainNoteEcho(note, boom) == note) + } + + // DECOMPOSED return: no converter names the spelling — the extern binds + // the value and matches it, so the `Box` has to come off first (#292). + // Same delivery as `archiveLatest`, one wrapper apart. + val a: SummaryVault = archiveNew(boom) + check(boxedLatest(a, boom) { count, total -> count to total } == null) + archiveStore(a, 0, 5L, 100.0, null, boom) + check(boxedLatest(a, boom) { count, total -> count to total } == 5L to 100.0) + a.close() + } + // ── Vec fold + Option input + plain String return ──── section("Vec storageLabels + Option input + String return") { val s = storageNew(boom) diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index c1a730b9..43592ebe 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -662,7 +662,7 @@ pub(crate) unsafe fn Box_Box_Option_String_to_JString_299999e0<'a>( v: Box>>, ) -> ::core::result::Result, __JniErr> { Ok({ - let v: Option = (*(*v)); + let v: Option = **v; { match v { Some(value) => String_to_JString_c7f3ca43(env, value)?, @@ -684,6 +684,33 @@ pub(crate) unsafe fn Box_Box_Option_String_to_JString_299999e0<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn Box_Option_Summary_to_jlong_75560ba9<'a>( + env: &mut jni::JNIEnv<'a>, + v: Box>, +) -> ::core::result::Result { + Ok({ + let v: Option = *v; + { + match v { + Some(value) => Summary_to_jlong_3cb103b9(env, value)?, + None => 0i64, + } + } + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn Box_String_to_JString_027f6250<'a>( env: &mut jni::JNIEnv<'a>, v: Box, @@ -12708,6 +12735,111 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_blobValueNew<'a> } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedLatest<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + a: jni::sys::jlong, + __builder: jni::objects::JObject<'a>, + __error_sink: jni::objects::JObject<'a>, +) -> jni::objects::JObject<'a> { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let a = match jlong_to_Archive_cd73502c(&mut env, &a) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return jni::objects::JObject::null().into(); + } + }; + #[allow(non_upper_case_globals)] + static __CB_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __CB_FQN: &str = "io/prebindgen/covertest/analytics/SummaryBuilder"; + const __CB_DESCR: &str = "(JD)Ljava/lang/Object;"; + let __out = *perftest_flat::boxed_latest(&a); + match __out { + ::core::option::Option::Some(__inner) => { + let __obj0: jni::sys::jvalue = { + let __enc0 = match i64_to_jlong_fbf9a9bc( + &mut env, + perftest_flat::summary_count(&__inner), + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return jni::objects::JObject::null().into(); + } + }; + jni::sys::jvalue { j: __enc0 } + }; + let __obj1: jni::sys::jvalue = { + let __enc1 = match f64_to_jdouble_9e4a8f70( + &mut env, + perftest_flat::summary_total(&__inner), + ) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return jni::objects::JObject::null().into(); + } + }; + jni::sys::jvalue { d: __enc1 } + }; + match __CB_MID + .call_object( + &mut env, + __CB_FQN, + "run", + __CB_DESCR, + &__builder, + &[__obj0, __obj1], + ) + { + ::core::result::Result::Ok(__o) => __o, + ::core::result::Result::Err(__e) => { + let _ = env.exception_describe(); + let __e2 = <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()); + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e2.to_string(), + ); + jni::objects::JObject::null().into() + } + } + } + ::core::option::Option::None => jni::objects::JObject::null().into(), + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedNoteEcho<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index a34bb57e..4d424507 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -1618,6 +1618,24 @@ pub fn plain_note_echo(note: Option) -> Option { note } +/// A transparent wrapper over a **decomposed** return — the shape `boxed_note_echo` +/// does not reach. +/// +/// `boxed_note_echo`'s return takes an output *converter*, which is selected for +/// the spelling and therefore names `Box>` itself. This one has +/// no converter at all: `Summary` carries a declared output expansion, so the +/// extern **binds the returned value and matches it** to deliver the leaves to a +/// builder. Match ergonomics does not see through a `Box`, so the emitter has to +/// move the value out of the wrappers the classification erased before it can +/// destructure — the defect #292's audit found, and one this crate compiles. +/// +/// The unwrapped twin is [`archive_latest`], which crosses as the same +/// `Summary?`. +#[prebindgen] +pub fn boxed_latest(a: &Archive) -> Box> { + Box::new(a.latest.clone()) +} + /// Deliver a [`Ledger`] to a callback, so both conditional decompositions cross /// in ONE call — including the sum (`Report::outcome`) each one carries, whose /// `match` belongs inside the arm that binds the report. diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index c9268ad6..17001a00 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -110,6 +110,113 @@ fn a_box_classifies_as_what_it_wraps() { assert_eq!(tokens(&inner.origin.syntax), "Box < String >"); } +/// The erasure, stated as something the model can be **asked**: what was taken +/// off, and what is left under it. +/// +/// The invariant is not "the loop peels until it stops" — it is that +/// [`TypeRef::stripped_syntax`] is *the spelling whose own lowering yields +/// exactly this type's `kind`*. That is what makes it a safe base for a +/// reconstruction, and it is why the peel must run to a **fixed point**: +/// `Box>` classifies as `T`, so one strip leaves a `Box` that does +/// not match. +#[test] +fn the_stripped_spelling_is_the_one_that_lowers_to_this_kind() { + // The property, asserted as a property: strip, lower again, get the same + // classification. A one-layer implementation fails the nested rows. + for spelling in [ + quote::quote!(Box>), + quote::quote!(Box>), + quote::quote!(Box>>>), + quote::quote!(Box>), + quote::quote!(Cow<'_, str>), + // The control: nothing erased, so stripping is the identity and the + // comparison cannot pass by both sides being trivially equal elsewhere. + quote::quote!(Option), + ] { + let ty = lower(spelling).expect("in the language"); + let stripped = ty.stripped_syntax(); + assert_eq!( + format!("{:?}", kind(quote::quote!(#stripped))), + format!("{:?}", ty.kind), + "`{}` strips to `{}`, which must classify identically", + tokens(&ty.origin.syntax), + tokens(&stripped), + ); + } + + // The wrappers themselves, outermost first — the list a rebuild applies in + // reverse. `Box>` is two DIFFERENT operations, which is why one + // name and a count would not do. + let wrappers = + |t: proc_macro2::TokenStream| lower(t).expect("in the language").erased_wrappers(); + assert_eq!(wrappers(quote::quote!(Box>)), ["Box", "Box"]); + assert_eq!(wrappers(quote::quote!(Box>)), ["Box", "Cow"]); + assert_eq!(wrappers(quote::quote!(Option)), [] as [&str; 0]); + + // Unwrapped, the stripped spelling is the spelling — token-identical, not + // merely equivalent, since it is what generated Rust would emit. + let plain = lower(quote::quote!(Option)).expect("in the language"); + assert_eq!( + tokens(&plain.stripped_syntax()), + tokens(&plain.origin.syntax) + ); + assert_eq!( + tokens( + &lower(quote::quote!(Box>>)) + .expect("in the language") + .stripped_syntax() + ), + "Option < Sample >" + ); +} + +/// **An erasure sits outside the layer it wraps**, so the question has to be +/// asked on the way *down* — at every layer — and never once at the top. +/// +/// This is the pair that pins it, and each row is invisible to the other's +/// vantage point: `Box<&Vec>` classifies as `Ref`, so a consumer that +/// interprets `kind` first is left holding a clean `Vec` with the `Box` +/// unreachable; `&Box>` puts the wrapper on the referent, where a +/// question asked of a `syn::Type::Reference` cannot see it. +#[test] +fn a_wrapper_is_found_only_at_the_layer_that_spells_it() { + let outside = lower(quote::quote!(Box<&Vec>)).expect("in the language"); + assert_eq!(outside.erased_wrappers(), ["Box"]); + let referent = outside.borrow_target().expect("a borrow"); + assert_eq!( + referent.erased_wrappers(), + [] as [&str; 0], + "peeling `kind` first reaches a clean `Vec` — the `Box` is only \ + visible before the peel" + ); + + let inside = lower(quote::quote!(&Box>)).expect("in the language"); + assert_eq!( + inside.erased_wrappers(), + [] as [&str; 0], + "a reference cannot be peeled as a transparent wrapper" + ); + assert_eq!( + inside.borrow_target().expect("a borrow").erased_wrappers(), + ["Box"], + "the wrapper is on the referent, after the peel" + ); + + // Both classify the same shape, which is the whole reason neither check + // alone is enough: the difference between them lives in a spelling, and the + // classification is exactly the thing it is missing from. + for ty in [&outside, &inside] { + let TypeKind::Ref { mode, inner } = &ty.kind else { + panic!("a borrow"); + }; + assert_eq!(*mode, RefMode::Shared); + let TypeKind::Sequence(elem) = &inner.kind else { + panic!("a run"); + }; + assert!(matches!(elem.kind, TypeKind::Named { .. })); + } +} + /// A builtin must be spelled BARE **after normalization**: the real std path /// reduces and classifies, while a path-qualified lookalike is a foreign type /// that merely shares the name, and collapsing it would silently retype the diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 303c2cac..6abc6c21 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -334,16 +334,80 @@ impl TypeRef { /// parameter spelled `Box<..>` is an `E0308` in the generated crate. /// /// Only the outermost wrapper is named. That is enough to decide *whether* - /// a spelling was erased — which is the question a reconstruction asks — - /// but a consumer that wants to rebuild a nested `Box>` needs to - /// peel repeatedly with [`peel_transparent`], the same list this reads. + /// a spelling was erased — which is the question a **refusal** asks — but a + /// consumer that rebuilds a nested `Box>` needs every layer, and + /// asks [`erased_wrappers`](Self::erased_wrappers) for the whole list plus + /// [`stripped_syntax`](Self::stripped_syntax) for what sits under it. /// /// Erased says nothing about **rebuildable**: `Box` reconstructs as /// `Box::new(v)`, while `Cow`'s `Owned`/`Borrowed` choice is not determined /// by any fact the model holds. Which wrappers an emitter can rebuild is /// that emitter's policy; this only stops the wrapper from being invisible. pub fn erased_wrapper(&self) -> Option<&'static str> { - peel_transparent(&self.origin.syntax).map(|(name, _)| name) + self.erased_wrappers().into_iter().next() + } + + /// Every [transparent wrapper](TRANSPARENT_WRAPPERS) this type's **spelling** + /// adds over its classification, outermost first — `Box>` → + /// `["Box", "Box"]`, `Box>` → `["Box", "Cow"]`, an unwrapped + /// spelling → `[]`. + /// + /// The list [`erased_wrapper`](Self::erased_wrapper) names the head of. A + /// consumer deciding *whether* to refuse needs only that head; one that + /// **rebuilds** needs all of them, because it has to apply an operation per + /// layer — and `Box>` is two different operations, not one + /// repeated. + /// + /// # This answers for one layer's spelling + /// + /// **An erasure sits outside the layer it wraps**, so this is a question + /// that has to be asked on the way *down*, at every layer, and never once at + /// the top: + /// + /// | Spelling | here | on [`borrow_target`](Self::borrow_target) | + /// |---|---|---| + /// | `Box<&Vec>` | `["Box"]` | `[]` — `kind` is `Ref`, and peeling it first drops the `Box` | + /// | `&Box>` | `[]` — a `syn::Type::Reference` cannot be peeled | `["Box"]` | + /// + /// A rebuild therefore collects wrappers **as it descends**: by the time it + /// reaches the leaf they are gone from `kind`, which is precisely the thing + /// they are missing from. + pub fn erased_wrappers(&self) -> Vec<&'static str> { + let mut names = Vec::new(); + let mut ty = std::borrow::Cow::Borrowed(&self.origin.syntax); + while let Some((name, inner)) = peel_transparent(&ty) { + names.push(name); + ty = std::borrow::Cow::Owned(inner); + } + names + } + + /// This type's spelling with every [transparent + /// wrapper](TRANSPARENT_WRAPPERS) removed — `Box>>` → + /// `Option`, an unwrapped spelling → itself. + /// + /// The spelling a reconstruction builds *before* it puts the wrappers back: + /// rebuilding from [`kind`](Self::kind) alone yields this, so an emitter + /// that hands it to a parameter spelled `Box<..>` writes an `E0308`. Paired + /// with [`erased_wrappers`](Self::erased_wrappers), which says exactly what + /// has to go back on. + /// + /// **The invariant, which is what defines this rather than the loop that + /// computes it**: it is the spelling whose own lowering yields exactly this + /// type's `kind`. So the peel runs to a **fixed point** — `Box>` + /// classifies as `T`, and stripping one layer leaves a `Box` that does + /// not match. + /// + /// Per-layer, for the reason [`erased_wrappers`](Self::erased_wrappers) + /// tabulates: this strips what stands over *this* node's classification, and + /// a wrapper under a borrow or inside an `Option` belongs to that inner + /// node's own spelling. + pub fn stripped_syntax(&self) -> syn::Type { + let mut ty = self.origin.syntax.clone(); + while let Some((_, inner)) = peel_transparent(&ty) { + ty = inner; + } + ty } /// The `Ok` and `Err` sides when this is a `Result`, else `None`. diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index 0d674792..e8f41827 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -2,7 +2,10 @@ //! params, and the expanded-param path. use super::*; -use crate::api::core::{registry::Conversions, types_util::result_ok_type}; +use crate::api::{ + core::{registry::Conversions, types_util::result_ok_type}, + lang::jnigen::jni::trait_impl::read_through_erased_wrappers, +}; pub(crate) fn emit_jni_function_wrapper( ext: &Declarations, @@ -352,6 +355,34 @@ pub(crate) fn emit_jni_function_wrapper_with_callee( // Decompose/Optional: a single `__builder` callback. let uplan = unfold_plan.expect("Unfold output ⇒ unfold plan present"); builder_param = Some(unfold_builder_param(u.iterable_fold)); + // The delivery **binds** the returned value and matches it against the + // canonical shape its `kind` names (`Option`, then a run). Conversion + // follows the SYNTAX, and this position takes no converter — nothing + // between the source call and the match re-spells anything — so the + // wrappers the classification erased have to come off here, at the + // emitter's own binding, or the match is an `E0308` on a spelling the + // model deliberately reads as optional (#292). + // + // The value delivered is the `Ok` side when the error plan applied the + // `?`, and the return itself otherwise — the wrappers questioned are + // those over whatever `call_expr` actually yields. + let delivered = match error_plan { + Some(_) => f.ret.fallible_parts().map_or(&f.ret, |(ok, _)| ok), + None => &f.ret, + }; + let call_expr = + read_through_erased_wrappers(delivered, call_expr.clone()).unwrap_or_else(|| { + panic!( + "`{original_ident}` returns `{}`, whose leaves are delivered to a builder: \ + the value has to be moved out of `{}` to be decomposed, and that wrapper \ + does not permit it (a `Cow` payload cannot be moved through `Deref`). \ + Reserved rather than refused for good: rebuilding through every \ + transparent wrapper is #292 item 3 — until then, spell the return \ + without it.", + delivered.syntax().to_token_stream(), + delivered.erased_wrappers().join("<"), + ) + }); emit_unfold_delivery( ext, registry, diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index 26bd5e5f..eec09938 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -2500,7 +2500,7 @@ fn an_owned_string_crosses_the_same_however_rust_spells_it() { /// Layers are counted, too: `Box>>` is `Optional`, and one /// dereference leaves `Box>`. /// -/// Both shapes used to RESOLVE and emit `let v: Option = (*v);` — the +/// Both shapes used to RESOLVE and emit `let v: Option = *v;` — the /// worst outcome available, because resolution succeeding is what tells the /// binding its type is supported. Failing to resolve names the type; emitting /// unbuildable Rust names nothing (#270 review). @@ -2556,21 +2556,23 @@ fn a_transparent_wrapper_is_bridged_only_where_it_can_be() { } }; - // One box: bridged with one dereference. + // One box: bridged with one dereference. Unparenthesized — the bind is a + // `let` initializer, where a wrapping paren is `unused_parens`, and + // generated code runs through the consumer's own lints (#292). let one = build(syn::parse_quote!(Box>)).expect("a single box is bridgeable"); let oc: String = one.split_whitespace().collect(); assert!( - oc.contains("letv:Option=(*v);"), + oc.contains("letv:Option=*v;"), "one layer, one dereference:\n{one}" ); // Two boxes: bridged with TWO. This is the case a single deref got wrong, - // silently — `(*v)` on `Box>` is still a `Box<_>`. + // silently — one `*` on `Box>` still leaves a `Box<_>`. let two = build(syn::parse_quote!(Box>>)).expect("nested boxes are bridgeable"); let tc: String = two.split_whitespace().collect(); assert!( - tc.contains("letv:Option=(*(*v));"), + tc.contains("letv:Option=**v;"), "two layers, two dereferences:\n{two}" ); diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 4701596f..eeb338bc 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -678,6 +678,12 @@ struct WrapperOps { /// Move the inner value **out**. `None` when the representation does not /// permit it — a `Cow` payload cannot be moved through `Deref` (`E0507`), /// and neither can an `Rc`'s. + /// + /// Emitted **unparenthesized**: every consumer splices the result into a + /// `let` initializer, where a wrapping paren is `unused_parens` — and + /// generated code runs through the consumer's own lints, where that is a + /// denial. A consumer that splices into a tighter position (a method + /// receiver, a field base) parenthesizes at its own site. read: Option TokenStream>, /// Build it **from** the inner value. `None` when not supported. build: Option TokenStream>, @@ -688,7 +694,7 @@ const WRAPPER_OPS: &[WrapperOps] = &[ WrapperOps { name: "Box", // `*b` moves out of a box, and `Box::new` puts it back. - read: Some(|e| quote!((*#e))), + read: Some(|e| quote!(*#e)), build: Some(|e| quote!(::std::boxed::Box::new(#e))), }, WrapperOps { @@ -750,6 +756,37 @@ fn build_from_canonical( Some(e) } +/// Move a value the **source** produced out of the transparent wrappers its +/// spelling adds over its classification, so an emitter that binds it holds the +/// canonical shape — `Box>` → `(*e)`, an unwrapped spelling → `e` +/// unchanged. +/// +/// The counterpart of [`bind_as_option`](super::emit::bind_as_option) for an +/// **owned** position. A type-ascribed `let` is a coercion site and serves any +/// representation, but coercion applies to *references*: a value whose payload +/// downstream moves has to be moved out of the wrapper instead, which is what +/// [`WrapperOps::read`] does and what only some wrappers permit. +/// +/// `None` when a layer cannot be read through (`Cow`, whose payload cannot be +/// moved out by `Deref`) — the caller then has an unrepresentable crossing to +/// report, and must not emit the match anyway. +/// +/// **This answers for one layer's spelling.** It undoes the wrappers standing +/// over `ty`'s own classification; a wrapper *inside* — the `Box` of +/// `Option>>` — belongs to the inner reading and is that layer's +/// question, per [`TypeRef::erased_wrappers`](crate::api::core::flat::TypeRef::erased_wrappers). +pub(crate) fn read_through_erased_wrappers( + ty: &crate::api::core::flat::TypeRef, + e: TokenStream, +) -> Option { + let mut out = e; + // Outermost first, which is the order they have to come off in. + for name in ty.erased_wrappers() { + out = (wrapper_ops(name)?.read?)(out); + } + Some(out) +} + /// Whether the source wrote the canonical spelling itself — no wrapper to undo. /// /// Required by the converters that do **not** produce the spelled type by From a972bb0660b5eaeab4757921885cfd2b243d66f4 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 22:45:13 +0200 Subject: [PATCH 38/52] jnigen: rebuild the spelling instead of refusing it (#294) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #292 item 3, and #289 with it — the two are one change because #289 alone breaks the build: reading a field's layer off the model is what makes the emitter rebuild an `Option` for a slot ascribed `Box>`. `build_through_erased_wrappers` is the input dual of #293's reader, on the same `WRAPPER_OPS` rows, applied innermost-out. The three specialized input lowerings descend instead of refusing, collecting each layer's wrappers on the way down — an erasure sits outside the layer it wraps. A layer's wrappers are applied only where that layer exists; applying them unconditionally double-wraps when two layers are the same reading. `Cow` keeps `build: None` as POLICY, not impossibility: `Cow::Owned(v)` is well-typed, but always-Owned pays a copy per call and removes the borrow path the source asked for, observably. Two findings the fixtures forced out: * **A wrapper silently cost a parameter its lowering.** The data-class declaration was keyed by the wrapped spelling, so `Box` found no `Payload` declaration and fell to the general converter — no error, no diff. Declarations are keyed by `stripped_key()` now; conversions keep `key()`. * **A wrapper over a terminal had no converter at all.** `input_transparent_bridge` delegates to the stripped spelling and re-wraps, tried last so no existing route changes. Refused with stated reasons: `Box<&T>` (a converter yields an owned value), `&Box>` (needs a per-call clone), `Vec>` elements (helper-trio name collision — see the follow-up, this one is soft). #289: `build_flat_struct_node` takes `flat::Struct` and peels its fields off the model. Both censuses move DOWN — spelling helpers 18 → 9, ledger 127 → 126 — the first in the #284 chain to do so, because it retires callers rather than re-typing signatures. Review catch, fixed in `eb9df58`: the wrap refactor had hoisted an optional node's field decodes out of its presence gate, so a null object's inert placeholders were decoded — a required handle field's pointer `0` reads as a closed handle and `null` became an error instead of `None`. `Holder` is the fixture that shows it. Every wrap verified by disabling it and reading the error naming its shape. Against the merge base the generated bindings have zero genuinely-removed lines. Closes #289. --- examples/covertest-kotlin/build.rs | 26 + examples/covertest-kotlin/kotlin/REPORT.md | 9 + .../generated/io/prebindgen/covertest.kt | 88 ++ .../io/prebindgen/covertest/model.kt | 136 +++ .../kotlin/io/prebindgen/covertest/Test.kt | 42 + .../src/generated_bindings.rs | 1006 ++++++++++++++++- examples/perftest-flat/src/ext.rs | 132 +++ prebindgen/src/api/core/flat/boundary.ledger | 4 +- prebindgen/src/api/core/flat/ty.rs | 22 + .../api/lang/jnigen/jni/emit/flat_input.rs | 423 +++++-- .../src/api/lang/jnigen/jni/emit/vec_build.rs | 49 +- .../src/api/lang/jnigen/jni/emit/wrapper.rs | 95 +- .../src/api/lang/jnigen/jni/selector.rs | 5 +- .../src/api/lang/jnigen/jni/tests/values.rs | 75 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 197 ++++ prebindgen/src/api/lang/jnigen/mod.rs | 9 +- prebindgen/src/api/test_util.rs | 29 + 17 files changed, 2139 insertions(+), 208 deletions(-) diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index 8af3a495..be7c3306 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -206,6 +206,17 @@ fn main() { .method(fun!(payload_label_len)), ), ) + // `Option` where `Holder` has a REQUIRED handle field: the + // absent case passes pointer 0 for it, so the field decodes must stay + // inside the presence gate or `null` becomes a binding error instead of + // `None` (PR#294 review). + .package(package!().class(data_class!(Holder))) + // A data class whose FIELDS carry transparent wrappers (#289 + #292): + // `boxed: Box>` must cross exactly as `plain: Option` + // does — the decoupled `(present, value)` pair — with the `Box` put back + // on the Rust side. Peeling the field by path segment answered "not + // optional" and boxed it instead. + .package(package!().class(data_class!(WrappedFields))) // ── Subpackage `model`: enum + value class + nested data class ────── .package( package!("model") @@ -522,6 +533,21 @@ fn main() { // with the wrong number of dereferences fails the build (#270). .fun(fun!(boxed_note_echo)) .fun(fun!(plain_note_echo)) + // Transparent wrappers on the INPUT side (#292 item 3), one per + // specialized lowering. These rebuild their parameter rather + // than decoding it, so the erased wrapper has to go back on + // before the value reaches the signature — and each layer is + // applied at a different point in the construction, which is why + // one shape cannot cover them all. Compiling this crate is the + // check: a missing `Box::new` is an `E0308`, invisible to any + // text assertion. + .fun(fun!(wrapped_fields_sum)) + .fun(fun!(holder_tag_or)) + .fun(fun!(boxed_payload_id)) + .fun(fun!(boxed_opt_payload_id)) + .fun(fun!(boxed_opt_priority_weight)) + .fun(fun!(boxed_elem_id_sum)) + .fun(fun!(boxed_run_id_sum)) // The same wrapper over a DECOMPOSED return (#292). `Summary` // has an output expansion, so this return takes no converter to // name the spelling for it — the extern binds the value and diff --git a/examples/covertest-kotlin/kotlin/REPORT.md b/examples/covertest-kotlin/kotlin/REPORT.md index b743cfcd..2c636424 100644 --- a/examples/covertest-kotlin/kotlin/REPORT.md +++ b/examples/covertest-kotlin/kotlin/REPORT.md @@ -63,9 +63,14 @@ Base package: `io.prebindgen.covertest` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) - `blob_value_new` — `fun blobValueNew(secs: Long, id: ByteArray, chunks: List, onError: JniErrorHandler): BlobValue` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) +- `boxed_elem_id_sum` — `fun boxedElemIdSum(ps: List, onError: JniErrorHandler): Long` - `boxed_latest` — `fun boxedLatest(a: SummaryVault, onError: JniErrorHandler, build: SummaryBuilder): R?` - shaped by: return `Summary` decomposed → [count, total] (Callback delivery) - `boxed_note_echo` — `fun boxedNoteEcho(note: String?, onError: JniErrorHandler): String?` +- `boxed_opt_payload_id` — `fun boxedOptPayloadId(p: Payload?, onError: JniErrorHandler): Long` +- `boxed_opt_priority_weight` — `fun boxedOptPriorityWeight(p: Priority?, onError: JniErrorHandler): Long` +- `boxed_payload_id` — `fun boxedPayloadId(p: Payload, onError: JniErrorHandler): Long` +- `boxed_run_id_sum` — `fun boxedRunIdSum(ps: List, onError: JniErrorHandler): Long` - `cache_config_weight` — `fun cacheConfigWeight(cache: CacheConfig?, onError: JniErrorHandler): Int` - `celsius_double` — `fun celsiusDouble(c: Int, onError: JniErrorHandler): Int` - `duration_boundary_echo` — `fun durationBoundaryEcho(value: DurationBoundary, onError: JniErrorHandler): DurationBoundary` @@ -76,6 +81,7 @@ Base package: `io.prebindgen.covertest` - `hold_echo` — `fun holdEcho(h: Hold, onError: JniErrorHandler): Hold` - shaped by: return `Hold` decomposed → [tag, for_v0] (Callback delivery) - `hold_policy_echo` — `fun holdPolicyEcho(p: HoldPolicy, onError: JniErrorHandler): HoldPolicy` +- `holder_tag_or` — `fun holderTagOr(h: Holder?, fallback: Long, onError: JniErrorHandler): Long` - `label_reverse` — `fun labelReverse(l: String, onError: JniErrorHandler): String` - `label_series_echo` — `fun labelSeriesEcho(labels: List, onError: JniErrorHandler>): List` - `ledger_each` — `fun ledgerEach(n: Long, sink: LedgerCallback, onError: JniErrorHandler)` @@ -117,6 +123,7 @@ Base package: `io.prebindgen.covertest` - shaped by: return `Unsigned` decomposed → [byte, short, int, long, maybeLong] (Callback delivery) - `unsigned_series` — `fun unsignedSeries(onError: JniErrorHandler>): List` - shaped by: return `u64` decomposed → [] (Callback delivery) +- `wrapped_fields_sum` — `fun wrappedFieldsSum(w: WrappedFields, onError: JniErrorHandler): Long` ## package `io.prebindgen.covertest.storage` @@ -192,6 +199,7 @@ Base package: `io.prebindgen.covertest` - `EscapeProbe`: ptr_class → `io.prebindgen.covertest.esc_pkg.Esc_Probe` (wire `jni :: sys :: jlong`) - `Hold`: sealed_class → `io.prebindgen.covertest.model.Hold` (wire `?`) - `HoldPolicy`: data_class → `io.prebindgen.covertest.model.HoldPolicy` (wire `jni :: objects :: JObject`) +- `Holder`: data_class → `io.prebindgen.covertest.Holder` (wire `jni :: objects :: JObject`) - `Lookup`: sealed_class → `io.prebindgen.covertest.model.Lookup` (wire `?`) - `Marker`: sealed_class → `io.prebindgen.covertest.model.Marker` (wire `?`) - `ObjectBoundary`: data_class → `io.prebindgen.covertest.model.ObjectBoundary` (wire `jni :: objects :: JObject`, input `JObject` opt-in) @@ -218,6 +226,7 @@ Base package: `io.prebindgen.covertest` - `Summary`: ptr_class → `io.prebindgen.covertest.analytics.Summary` (wire `jni :: sys :: jlong`) - `Tagged`: data_class → `io.prebindgen.covertest.model.Tagged` (wire `jni :: objects :: JObject`) - `Unsigned`: data_class → `io.prebindgen.covertest.model.Unsigned` (wire `jni :: objects :: JObject`) +- `WrappedFields`: data_class → `io.prebindgen.covertest.WrappedFields` (wire `jni :: objects :: JObject`) ## conversions diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt index b2a2537e..f2b1a914 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt @@ -157,6 +157,31 @@ internal inline fun withSortedHandleLocks( return synchronized(x) { synchronized(y) { synchronized(z) { body() } } } } +/** + * An `Option` whose data class has a **required handle** field. + * + * The shape that proves an optional node's field decodes stay **inside** its + * presence gate. When the Kotlin object is null every leaf carries an inert + * placeholder, and a handle leaf's placeholder is pointer `0` — which the + * direct-handle decode reads as a closed handle, signals a binding error for, + * and returns from. Hoisting the decodes out of the gate therefore turns + * `null` into an error instead of `None`, and no fixture whose fields all + * decode successfully can tell the difference. + * + * `Summary` is consumed by value here, as a handle field is: the `Some` case + * hands over ownership, and the `None` case must never touch the slot. + */ +public data class Holder(val tag: Long, val summary: Summary) : AutoCloseable { + override fun close() { + summary.close() + } + + public companion object { + @JvmStatic + public fun fromParts(tag: Long, summary: Long): Holder = Holder(tag, Summary(summary)) + } +} + public interface PayloadApi { val id: Long @@ -209,6 +234,27 @@ public data class Payload(override val id: Long, override val seq: Int, override } } +/** + * A data class whose **fields** carry transparent wrappers. + * + * This is what #289 changes and why it could not land alone. The field walk + * used to peel with `option_inner_type`, which reads the last path segment: a + * field spelled `Box>` answered "not optional" and crossed as one + * boxed `java.lang.Long`. The model says `Optional`, so it now takes the + * decoupled `(present, value)` pair its bare twin does — and the emitter has to + * put the `Box` back when it rebuilds, or the migration turns a working boxed + * crossing into an `E0308`. + * + * `plain` is the control: the two fields must produce the same wire, since the + * model says they are the same type. + */ +public data class WrappedFields(val id: Long, val boxed: Long?, val plain: Long?) { + public companion object { + @JvmStatic + public fun fromParts(id: Long, boxed: Long?, plain: Long?): WrappedFields = WrappedFields(id, boxed, plain) + } +} + /** Typed handle for a native Zenoh `PayloadHandler`. */ public class PayloadHandler(initialPtr: Long) : NativeHandle(initialPtr) { @Synchronized @@ -894,10 +940,35 @@ internal object CovNative { errorSink: Any, ): Any? + external fun boxedElemIdSum(ps: List, errorSink: Any): Long + external fun boxedLatest(a: Long, build: Any, errorSink: Any): Any? external fun boxedNoteEcho(note: String?, errorSink: Any): String? + external fun boxedOptPayloadId( + pPresent: Boolean, + pId: Long, + pSeq: Int, + pValue: Double, + pFlag: Boolean, + pLabel: String?, + errorSink: Any, + ): Long + + external fun boxedOptPriorityWeight(pPresent: Boolean, pValue: Int, errorSink: Any): Long + + external fun boxedPayloadId( + pId: Long, + pSeq: Int, + pValue: Double, + pFlag: Boolean, + pLabel: String?, + errorSink: Any, + ): Long + + external fun boxedRunIdSum(ps: Long, errorSink: Any): Long + external fun cacheConfigWeight( cachePresent: Boolean, cacheRepliesPriority: Int, @@ -930,6 +1001,14 @@ internal object CovNative { errorSink: Any, ): HoldPolicy + external fun holderTagOr( + hPresent: Boolean, + hTag: Long, + hSummary: Long, + fallback: Long, + errorSink: Any, + ): Long + external fun labelReverse(l: String, errorSink: Any): String external fun labelSeriesEcho(labels: List, errorSink: Any): List @@ -1249,6 +1328,15 @@ internal object CovNative { external fun unsignedSeries(acc: Any?, fold: Any, errorSink: Any): Any? + external fun wrappedFieldsSum( + wId: Long, + wBoxedPresent: Boolean, + wBoxedValue: Long, + wPlainPresent: Boolean, + wPlainValue: Long, + errorSink: Any, + ): Long + external fun constGetCoverMagic(errorSink: Any): Long external fun constGetCoverTag(errorSink: Any): String diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt index cb8a3fb9..6388940d 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt @@ -3,6 +3,7 @@ package io.prebindgen.covertest.model import io.prebindgen.covertest.CovNative import io.prebindgen.covertest.DurationCallback +import io.prebindgen.covertest.Holder import io.prebindgen.covertest.JniErrorHandler import io.prebindgen.covertest.JniErrorHandlerCapture import io.prebindgen.covertest.LedgerBuilder @@ -10,6 +11,7 @@ import io.prebindgen.covertest.LedgerCallback import io.prebindgen.covertest.NativeHandle import io.prebindgen.covertest.Payload import io.prebindgen.covertest.Ranked +import io.prebindgen.covertest.WrappedFields import io.prebindgen.covertest.__u64FolderRawHolder import io.prebindgen.covertest.analytics.Summary import io.prebindgen.covertest.analytics.SummaryBuilder @@ -1624,6 +1626,140 @@ public fun plainNoteEcho(note: String?, onError: JniErrorHandler): Stri return __ret } +/** Round-trip a [`WrappedFields`] so both field spellings cross in one call. */ +public fun wrappedFieldsSum(w: WrappedFields, onError: JniErrorHandler): Long { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.wrappedFieldsSum( + w.id, + w.boxed != null, + w.boxed ?: 0L, + w.plain != null, + w.plain ?: 0L, + __bcap, + ) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + +/** + * `tag` when the holder is present, `fallback` when it is absent — so the + * absent arm is observable as a **value** rather than as an error. + */ +public fun holderTagOr(h: Holder?, fallback: Long, onError: JniErrorHandler): Long { + if (h?.summary?.isClosed() == true) return onError.run("Operation on a closed native handle.") + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = run { + val __locks = ArrayList() + h?.summary?.let { __locks.add(it) } + withSortedHandleLocks(__locks) { + val hSummary_ptr = h?.summary?.ptr ?: 0L + try { + CovNative.holderTagOr(h != null, h?.tag ?: 0L, hSummary_ptr, fallback, __bcap) + } finally { + h?.summary?.markConsumed() + } + } + } + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + +/** + * Transparent wrappers on the **input** side, one per specialized lowering. + * + * These lowerings do not *decode* their parameter, they **rebuild** it — a + * literal `Payload { .. }`, an `Option::Some(v)`, a `Vec` pushed element by + * element — so the wrappers the classification erased have to go back on before + * the value reaches the signature. Rebuilding from the classification alone + * hands an `Option` to a parameter spelled `Box>`: + * `E0308` (#292 item 3, which replaced #290's refusals). + * + * Declared here rather than only in unit tests for the reason + * [`boxed_note_echo`] is: this crate's generated binding is `include!`d and + * **compiled**, so a missing or misplaced `Box::new` fails the build. Each has + * an unwrapped twin already declared — the surfaces must come out identical, + * since the model says the two spellings are one type. + * + * The layers are covered separately because each is applied at a different + * point in the construction, and only a shape that exercises one can show it: + * the core wrap goes inside the present gate, and the optional wrap around it. + * + * **`Box<&Payload>` is deliberately absent.** The flatten lowering could build + * it — it owns a local and `Box::new(&local)` is well-typed — but a declared + * parameter also needs a general converter entry, and a converter *produces* an + * owned value: there is nothing for a `Box<&T>` to borrow from that outlives + * the call (`E0106` on the generated signature). So the shape is refused at + * resolution, by the converter's nature rather than the wrapper's. + */ +public fun boxedPayloadId(p: Payload, onError: JniErrorHandler): Long { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.boxedPayloadId(p.id, p.seq, p.value, p.flag, p.label, __bcap) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + +/** + * The optional layer over the same rebuild — the wrap goes **around** the + * present gate, where the core wrap goes inside it. + */ +public fun boxedOptPayloadId(p: Payload?, onError: JniErrorHandler): Long { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.boxedOptPayloadId( + p != null, + p?.id ?: 0L, + p?.seq ?: 0, + p?.value ?: 0.0, + p?.flag ?: false, + p?.label, + __bcap, + ) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + +/** + * The option-scalar lowering (`(present, value)` raw pair) under a wrapper — + * the rebuilt `Option` is re-wrapped before it reaches the signature. + */ +public fun boxedOptPriorityWeight(p: Priority?, onError: JniErrorHandler): Long { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.boxedOptPriorityWeight(p != null, p?.value ?: 0, __bcap) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + +/** + * A wrapped **element** in the Vec-build path: the storage is `Vec>` + * and each push wraps its own literal. + */ +public fun boxedElemIdSum(ps: List, onError: JniErrorHandler): Long { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.boxedElemIdSum(ps, __bcap) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + +/** + * A wrapped **run**, by value: `mem::take` yields the owned `Vec`, so the + * `Box` costs nothing. The borrowed twin (`&Box>`) is deliberately + * **not** declared — interposing a `Box` between the caller's Vec and the + * callee would require copying it, so that shape keeps the ordinary path. + */ +public fun boxedRunIdSum(ps: List, onError: JniErrorHandler): Long { + val __bcap = JniErrorHandlerCapture.acquire() + val __vec_ps = CovNative.payloadVecNew(ps.size) + val __ret = try { + for (__e in ps) { + CovNative.payloadVecPush(__vec_ps, __e.id, __e.seq, __e.value, __e.flag, __e.label) + } + CovNative.boxedRunIdSum(__vec_ps, __bcap) + } finally { + CovNative.payloadVecFree(__vec_ps) + } + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret +} + /** * A transparent wrapper over a **decomposed** return — the shape `boxed_note_echo` * does not reach. diff --git a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt index 5789625f..14f32315 100644 --- a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt +++ b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt @@ -44,7 +44,16 @@ import io.prebindgen.covertest.model.Unsigned import io.prebindgen.covertest.model.annotatedNew import io.prebindgen.covertest.model.arraysEcho import io.prebindgen.covertest.model.blobValueEcho +import io.prebindgen.covertest.Holder +import io.prebindgen.covertest.WrappedFields +import io.prebindgen.covertest.model.boxedElemIdSum import io.prebindgen.covertest.model.boxedLatest +import io.prebindgen.covertest.model.boxedOptPayloadId +import io.prebindgen.covertest.model.boxedOptPriorityWeight +import io.prebindgen.covertest.model.boxedPayloadId +import io.prebindgen.covertest.model.boxedRunIdSum +import io.prebindgen.covertest.model.holderTagOr +import io.prebindgen.covertest.model.wrappedFieldsSum import io.prebindgen.covertest.model.boxedNoteEcho import io.prebindgen.covertest.model.plainNoteEcho import io.prebindgen.covertest.model.blobValueNew @@ -1449,6 +1458,39 @@ fun main() { archiveStore(a, 0, 5L, 100.0, null, boom) check(boxedLatest(a, boom) { count, total -> count to total } == 5L to 100.0) a.close() + + // INPUT side (#292 item 3). These lowerings REBUILD their parameter, so + // the wrapper has to go back on before the value reaches the signature. + // Every surface below is the unwrapped one — a wrapper must not cost a + // parameter its lowering, and must not show up in Kotlin either. + val p = payload(7L, 1, 2.0, true, "w") + check(boxedPayloadId(p, boom) == 7L) // core wrap + check(boxedOptPayloadId(p, boom) == 7L) // core + optional wrap + check(boxedOptPayloadId(null, boom) == -1L) // …and the absent arm + check(boxedOptPriorityWeight(Priority.HIGH, boom) == 10L) // option-scalar + check(boxedOptPriorityWeight(null, boom) == -1L) + val many = listOf(payload(1L, 0, 0.0, false, null), payload(2L, 0, 0.0, false, null)) + check(boxedElemIdSum(many, boom) == 3L) // wrapped element + check(boxedRunIdSum(many, boom) == 3L) // wrapped run, by value + + // FIELDS (#289). `boxed: Box>` and `plain: Option` + // are one type to the model, so both cross as `Long?` on the decoupled + // `(present, value)` pair — the boxed one used to be read by path + // segment as "not optional" and crossed as one boxed object. + check(wrappedFieldsSum(WrappedFields(1L, 2L, 4L), boom) == 7L) + check(wrappedFieldsSum(WrappedFields(1L, null, 4L), boom) == 5L) + check(wrappedFieldsSum(WrappedFields(1L, 2L, null), boom) == 3L) + check(wrappedFieldsSum(WrappedFields(1L, null, null), boom) == 1L) + + // An absent `Option` must deliver `None`, not an error. Its + // leaves are inert placeholders when the object is null, and a required + // HANDLE field's placeholder is pointer 0 — which the direct-handle + // decode reads as a closed handle. So this is the shape that proves the + // field decodes stay inside the presence gate; a fixture whose fields + // all decode successfully cannot tell the two orders apart. + check(holderTagOr(null, -9L, boom) == -9L) + val held = Summary.of(4L, 8.0, boom) + check(holderTagOr(Holder(3L, held), -9L, boom) == 7L) // 3 + count(4) } // ── Vec fold + Option input + plain String return ──── diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index 43592ebe..d4a9abcf 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -711,6 +711,39 @@ pub(crate) unsafe fn Box_Option_Summary_to_jlong_75560ba9<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn Box_Option_i64_to_JObject_cf5a3724<'a>( + env: &mut jni::JNIEnv<'a>, + v: Box>, +) -> ::core::result::Result, __JniErr> { + Ok({ + let v: Option = *v; + { + match v { + Some(value) => { + let __raw: jni::sys::jlong = i64_to_jlong_fbf9a9bc(env, value)?; + ::prebindgen::lang::box_jlong(env, __raw) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option box: {}", e)))? + } + None => jni::objects::JObject::null(), + } + } + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn Box_String_to_JString_027f6250<'a>( env: &mut jni::JNIEnv<'a>, v: Box, @@ -1001,6 +1034,46 @@ pub(crate) unsafe fn HoldPolicy_to_JObject_d2a5bcc4<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn Holder_to_JObject_c36a9705<'a>( + env: &mut jni::JNIEnv<'a>, + v: perftest_flat::Holder, +) -> ::core::result::Result, __JniErr> { + Ok({ + let ___tag: jni::sys::jlong = i64_to_jlong_fbf9a9bc(env, v.tag.clone())?; + let ___summary: jni::sys::jlong = Summary_to_jlong_3cb103b9( + env, + v.summary.clone(), + )?; + let __obj = env + .call_static_method( + "io/prebindgen/covertest/Holder", + "fromParts", + "(JJ)Lio/prebindgen/covertest/Holder;", + &[ + jni::objects::JValue::from(___tag), + jni::objects::JValue::from(___summary), + ], + ) + .and_then(|__v| __v.l()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("encode struct via fromParts: {}", e)))?; + __obj + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn JBooleanArray_to_bool_3_3f960c58<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JBooleanArray<'v>, @@ -1548,6 +1621,166 @@ pub(crate) unsafe fn JObject_to_BlobValue_89b5dab7<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn JObject_to_Box_Option_Payload_8d993ebb<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result>, __JniErr> { + Ok({ + let __v: ::core::option::Option = { + if v.is_null() { None } else { Some(JObject_to_Payload_98f64326(env, v)?) } + }; + ::std::boxed::Box::new(__v) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn JObject_to_Box_Option_Priority_cb1cb2b5<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result>, __JniErr> { + Ok({ + let __v: ::core::option::Option = { + if !v.is_null() { + let __unboxed: jni::sys::jint = env + .call_method(&v, "intValue", "()I", &[]) + .and_then(|val| val.i()) + .map(|__x| __x as jni::sys::jint) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option unbox: {}", e)))?; + Some(jint_to_Priority_447102d2(env, &__unboxed)?) + } else { + None + } + }; + ::std::boxed::Box::new(__v) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn JObject_to_Box_Option_i64_cf5a3724<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result>, __JniErr> { + Ok({ + let __v: ::core::option::Option = { + if !v.is_null() { + let __unboxed: jni::sys::jlong = env + .call_method(&v, "longValue", "()J", &[]) + .and_then(|val| val.j()) + .map(|__x| __x as jni::sys::jlong) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Option unbox: {}", e)))?; + Some(jlong_to_i64_fbf9a9bc(env, &__unboxed)?) + } else { + None + } + }; + ::std::boxed::Box::new(__v) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn JObject_to_Box_Payload_0d2d19da<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result, __JniErr> { + Ok({ + let __inner = JObject_to_Payload_98f64326(env, v)?; + ::std::boxed::Box::new(__inner) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn JObject_to_Box_Vec_Payload_ca25c6a1<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result>, __JniErr> { + Ok({ + let __list = jni::objects::JList::from_env(env, v) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Vec<_>: list-from-env: {}", e)))?; + let mut __it = __list + .iter(env) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Vec<_>: list-iter: {}", e)))?; + let mut __out: Vec = Vec::new(); + while let Some(__obj) = __it + .next(env) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Vec<_>: list-next: {}", e)))? + { + let __elem_wire: jni::objects::JObject = __obj.into(); + let __elem: perftest_flat::Payload = JObject_to_Payload_98f64326( + env, + &__elem_wire, + )?; + __out.push(__elem); + } + ::std::boxed::Box::new(__out) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn JObject_to_CacheConfig_db89a97c<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, @@ -1754,6 +1987,62 @@ pub(crate) unsafe fn JObject_to_Hold_5f85caaf<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn JObject_to_Holder_c36a9705<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result { + Ok({ + let __tag_raw: jni::sys::jlong = env + .get_field(v, "tag", "J") + .and_then(|val| val.j()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Holder.tag: {}", e)))? as _; + let tag = jlong_to_i64_fbf9a9bc(env, &__tag_raw)?; + let __summary_jobj: jni::objects::JObject = env + .get_field(v, "summary", "Lio/prebindgen/covertest/analytics/Summary;") + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Holder.summary: {}", e)))?; + let __summary_raw: jni::sys::jlong = if __summary_jobj.is_null() { + 0 + } else { + env.call_method(&__summary_jobj, "peek", "()J", &[]) + .and_then(|val| val.j()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Holder.summary: {}", e)))? + }; + if __summary_raw == 0 || (__summary_raw & 1) == 1 { + return ::core::result::Result::Err( + <__JniErr as ::core::convert::From< + String, + >>::from("Operation on a closed native handle.".to_string()), + ); + } + let summary: perftest_flat::Summary = unsafe { + *std::boxed::Box::from_raw(__summary_raw as *mut perftest_flat::Summary) + }; + perftest_flat::Holder { + tag, + summary, + } + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn JObject_to_Lookup_94ada15e<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, @@ -2422,6 +2711,30 @@ pub(crate) unsafe fn JObject_to_Option_Hold_230d7f9b<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn JObject_to_Option_Holder_ca758c1f<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result, __JniErr> { + Ok({ + let __v: ::core::option::Option = { + if v.is_null() { None } else { Some(JObject_to_Holder_c36a9705(env, v)?) } + }; + __v + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn JObject_to_Option_Payload_97036642<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, @@ -3067,6 +3380,50 @@ pub(crate) unsafe fn JObject_to_Unsigned_7e3cc618<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn JObject_to_Vec_Box_Payload_ae68babe<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result>, __JniErr> { + Ok({ + let __list = jni::objects::JList::from_env(env, v) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Vec<_>: list-from-env: {}", e)))?; + let mut __it = __list + .iter(env) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Vec<_>: list-iter: {}", e)))?; + let mut __out: Vec> = Vec::new(); + while let Some(__obj) = __it + .next(env) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("Vec<_>: list-next: {}", e)))? + { + let __elem_wire: jni::objects::JObject = __obj.into(); + let __elem: Box = JObject_to_Box_Payload_0d2d19da( + env, + &__elem_wire, + )?; + __out.push(__elem); + } + __out + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn JObject_to_Vec_Label_3fdf860d<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::objects::JObject<'v>, @@ -3184,7 +3541,53 @@ pub(crate) unsafe fn JObject_to_Vec_Vec_u8_43404875<'env, 'v>( let __elem: Vec = JByteArray_to_Vec_u8_7936d5de(env, &__elem_wire)?; __out.push(__elem); } - __out + __out + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] +pub(crate) unsafe fn JObject_to_WrappedFields_f14f08c1<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::objects::JObject<'v>, +) -> ::core::result::Result { + Ok({ + let __id_raw: jni::sys::jlong = env + .get_field(v, "id", "J") + .and_then(|val| val.j()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("WrappedFields.id: {}", e)))? as _; + let id = jlong_to_i64_fbf9a9bc(env, &__id_raw)?; + let __boxed_raw: jni::objects::JObject = env + .get_field(v, "boxed", "Ljava/lang/Object;") + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("WrappedFields.boxed: {}", e)))?; + let boxed = JObject_to_Box_Option_i64_cf5a3724(env, &__boxed_raw)?; + let __plain_raw: jni::objects::JObject = env + .get_field(v, "plain", "Ljava/lang/Long;") + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("WrappedFields.plain: {}", e)))?; + let plain = JObject_to_Option_i64_2ba9a5ed(env, &__plain_raw)?; + perftest_flat::WrappedFields { + id, + boxed, + plain, + } }) } #[allow( @@ -9054,6 +9457,51 @@ pub(crate) unsafe fn Vec_u8_to_JByteArray_7936d5de<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn WrappedFields_to_JObject_f14f08c1<'a>( + env: &mut jni::JNIEnv<'a>, + v: perftest_flat::WrappedFields, +) -> ::core::result::Result, __JniErr> { + Ok({ + let ___id: jni::sys::jlong = i64_to_jlong_fbf9a9bc(env, v.id.clone())?; + let ___boxed: jni::objects::JObject = Box_Option_i64_to_JObject_cf5a3724( + env, + v.boxed.clone(), + )?; + let ___plain: jni::objects::JObject = Option_i64_to_JObject_2ba9a5ed( + env, + v.plain.clone(), + )?; + let __obj = env + .call_static_method( + "io/prebindgen/covertest/WrappedFields", + "fromParts", + "(JLjava/lang/Long;Ljava/lang/Long;)Lio/prebindgen/covertest/WrappedFields;", + &[ + jni::objects::JValue::from(___id), + jni::objects::JValue::Object(&___boxed), + jni::objects::JValue::Object(&___plain), + ], + ) + .and_then(|__v| __v.l()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("encode struct via fromParts: {}", e)))?; + __obj + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn bool_3_to_JBooleanArray_3f960c58<'a>( env: &mut jni::JNIEnv<'a>, v: [bool; 3], @@ -12735,6 +13183,48 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_blobValueNew<'a> } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedElemIdSum<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + ps: jni::objects::JObject<'a>, + __error_sink: jni::objects::JObject<'a>, +) -> jni::sys::jlong { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let ps = match JObject_to_Vec_Box_Payload_ae68babe(&mut env, &ps) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __out = perftest_flat::boxed_elem_id_sum(ps); + match i64_to_jlong_fbf9a9bc(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + 0 as jni::sys::jlong + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedLatest<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, @@ -12835,22 +13325,240 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedLatest<'a>( } } } - ::core::option::Option::None => jni::objects::JObject::null().into(), + ::core::option::Option::None => jni::objects::JObject::null().into(), + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedNoteEcho<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + note: jni::objects::JString<'a>, + __error_sink: jni::objects::JObject<'a>, +) -> jni::objects::JString<'a> { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let note = match JString_to_Box_Option_String_caeff346(&mut env, ¬e) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return jni::objects::JObject::null().into(); + } + }; + let __out = perftest_flat::boxed_note_echo(note); + match Box_Box_Option_String_to_JString_299999e0(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + jni::objects::JObject::null().into() + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedOptPayloadId<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + p_present: jni::sys::jboolean, + p_id: jni::sys::jlong, + p_seq: jni::sys::jint, + p_value: jni::sys::jdouble, + p_flag: jni::sys::jboolean, + p_label: jni::objects::JString<'a>, + __error_sink: jni::objects::JObject<'a>, +) -> jni::sys::jlong { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let __flat_p = ::std::boxed::Box::new( + if p_present != 0u8 { + let __flat_p_id = match jlong_to_i64_fbf9a9bc(&mut env, &p_id) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p_seq = match jint_to_i32_a3e3b6ef(&mut env, &p_seq) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p_value = match jdouble_to_f64_9e4a8f70(&mut env, &p_value) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p_flag = match jboolean_to_bool_31306d98(&mut env, &p_flag) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p_label = match JString_to_Option_Box_String_071e4c8c( + &mut env, + &p_label, + ) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + ::core::option::Option::Some(perftest_flat::Payload { + id: __flat_p_id, + seq: __flat_p_seq, + value: __flat_p_value, + flag: __flat_p_flag, + label: __flat_p_label, + }) + } else { + ::core::option::Option::None + }, + ); + let p = __flat_p; + let __out = perftest_flat::boxed_opt_payload_id(p); + match i64_to_jlong_fbf9a9bc(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + 0 as jni::sys::jlong + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedOptPriorityWeight< + 'a, +>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + p_present: jni::sys::jboolean, + p_value: jni::sys::jint, + __error_sink: jni::objects::JObject<'a>, +) -> jni::sys::jlong { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let p = ::std::boxed::Box::new( + if p_present != 0u8 { + let __p_val = match jint_to_Priority_447102d2(&mut env, &p_value) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + ::core::option::Option::Some(__p_val) + } else { + ::core::option::Option::None + }, + ); + let __out = perftest_flat::boxed_opt_priority_weight(p); + match i64_to_jlong_fbf9a9bc(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + 0 as jni::sys::jlong + } } } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] -pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedNoteEcho<'a>( +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedPayloadId<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, - note: jni::objects::JString<'a>, + p_id: jni::sys::jlong, + p_seq: jni::sys::jint, + p_value: jni::sys::jdouble, + p_flag: jni::sys::jboolean, + p_label: jni::objects::JString<'a>, __error_sink: jni::objects::JObject<'a>, -) -> jni::objects::JString<'a> { +) -> jni::sys::jlong { #[allow(non_upper_case_globals)] static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; - let note = match JString_to_Box_Option_String_caeff346(&mut env, ¬e) { + let __flat_p_id = match jlong_to_i64_fbf9a9bc(&mut env, &p_id) { ::core::result::Result::Ok(__v) => __v, ::core::result::Result::Err(__e) => { signal_binding_error( @@ -12861,11 +13569,78 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedNoteEcho<'a __SINK_DESCR, &__e.to_string(), ); - return jni::objects::JObject::null().into(); + return 0 as jni::sys::jlong; } }; - let __out = perftest_flat::boxed_note_echo(note); - match Box_Box_Option_String_to_JString_299999e0(&mut env, __out) { + let __flat_p_seq = match jint_to_i32_a3e3b6ef(&mut env, &p_seq) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p_value = match jdouble_to_f64_9e4a8f70(&mut env, &p_value) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p_flag = match jboolean_to_bool_31306d98(&mut env, &p_flag) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p_label = match JString_to_Option_Box_String_071e4c8c( + &mut env, + &p_label, + ) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_p = ::std::boxed::Box::new(perftest_flat::Payload { + id: __flat_p_id, + seq: __flat_p_seq, + value: __flat_p_value, + flag: __flat_p_flag, + label: __flat_p_label, + }); + let p = __flat_p; + let __out = perftest_flat::boxed_payload_id(p); + match i64_to_jlong_fbf9a9bc(&mut env, __out) { ::core::result::Result::Ok(__w) => __w, ::core::result::Result::Err(__e) => { signal_binding_error( @@ -12876,7 +13651,38 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedNoteEcho<'a __SINK_DESCR, &__e.to_string(), ); - jni::objects::JObject::null().into() + 0 as jni::sys::jlong + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedRunIdSum<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + ps_handle: jni::sys::jlong, + __error_sink: jni::objects::JObject<'a>, +) -> jni::sys::jlong { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let ps = ::std::boxed::Box::new(unsafe { + ::core::mem::take(&mut *(ps_handle as *mut Vec)) + }); + let __out = perftest_flat::boxed_run_id_sum(ps); + match i64_to_jlong_fbf9a9bc(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + 0 as jni::sys::jlong } } } @@ -13578,6 +14384,88 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_holdPolicyEcho<' } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_holderTagOr<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + h_present: jni::sys::jboolean, + h_tag: jni::sys::jlong, + h_summary: jni::sys::jlong, + fallback: jni::sys::jlong, + __error_sink: jni::objects::JObject<'a>, +) -> jni::sys::jlong { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let __flat_h = if h_present != 0u8 { + let __flat_h_tag = match jlong_to_i64_fbf9a9bc(&mut env, &h_tag) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + if h_summary == 0 || (h_summary & 1) == 1 { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + "Operation on a closed native handle.", + ); + return 0 as jni::sys::jlong; + } + let __flat_h_summary: perftest_flat::Summary = unsafe { + *::std::boxed::Box::from_raw(h_summary as *mut perftest_flat::Summary) + }; + ::core::option::Option::Some(perftest_flat::Holder { + tag: __flat_h_tag, + summary: __flat_h_summary, + }) + } else { + ::core::option::Option::None + }; + let h = __flat_h; + let fallback = match jlong_to_i64_fbf9a9bc(&mut env, &fallback) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __out = perftest_flat::holder_tag_or(h, fallback); + match i64_to_jlong_fbf9a9bc(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + 0 as jni::sys::jlong + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_labelReverse<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, @@ -21920,6 +22808,104 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_unsignedSeries<' } __acc } +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_wrappedFieldsSum<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + w_id: jni::sys::jlong, + w_boxed_present: jni::sys::jboolean, + w_boxed_value: jni::sys::jlong, + w_plain_present: jni::sys::jboolean, + w_plain_value: jni::sys::jlong, + __error_sink: jni::objects::JObject<'a>, +) -> jni::sys::jlong { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let __flat_w_id = match jlong_to_i64_fbf9a9bc(&mut env, &w_id) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_w_boxed = ::std::boxed::Box::new( + if w_boxed_present != 0u8 { + let __flat_w_boxed_value = match jlong_to_i64_fbf9a9bc( + &mut env, + &w_boxed_value, + ) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + ::core::option::Option::Some(__flat_w_boxed_value) + } else { + ::core::option::Option::None + }, + ); + let __flat_w_plain = if w_plain_present != 0u8 { + let __flat_w_plain_value = match jlong_to_i64_fbf9a9bc( + &mut env, + &w_plain_value, + ) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + ::core::option::Option::Some(__flat_w_plain_value) + } else { + ::core::option::Option::None + }; + let __flat_w = perftest_flat::WrappedFields { + id: __flat_w_id, + boxed: __flat_w_boxed, + plain: __flat_w_plain, + }; + let w = __flat_w; + let __out = perftest_flat::wrapped_fields_sum(w); + match i64_to_jlong_fbf9a9bc(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + 0 as jni::sys::jlong + } + } +} /// The storage capacity limit advertised to bindings (a primitive const). pub const COVER_MAGIC: i64 = perftest_flat::COVER_MAGIC; #[no_mangle] diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index 4d424507..788a48d1 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -1636,6 +1636,138 @@ pub fn boxed_latest(a: &Archive) -> Box> { Box::new(a.latest.clone()) } +/// An `Option` whose data class has a **required handle** field. +/// +/// The shape that proves an optional node's field decodes stay **inside** its +/// presence gate. When the Kotlin object is null every leaf carries an inert +/// placeholder, and a handle leaf's placeholder is pointer `0` — which the +/// direct-handle decode reads as a closed handle, signals a binding error for, +/// and returns from. Hoisting the decodes out of the gate therefore turns +/// `null` into an error instead of `None`, and no fixture whose fields all +/// decode successfully can tell the difference. +/// +/// `Summary` is consumed by value here, as a handle field is: the `Some` case +/// hands over ownership, and the `None` case must never touch the slot. +#[prebindgen] +pub struct Holder { + pub tag: i64, + pub summary: Summary, +} + +/// `tag` when the holder is present, `fallback` when it is absent — so the +/// absent arm is observable as a **value** rather than as an error. +#[prebindgen] +pub fn holder_tag_or(h: Option, fallback: i64) -> i64 { + match h { + Some(h) => h.tag + h.summary.count, + None => fallback, + } +} + +/// A data class whose **fields** carry transparent wrappers. +/// +/// This is what #289 changes and why it could not land alone. The field walk +/// used to peel with `option_inner_type`, which reads the last path segment: a +/// field spelled `Box>` answered "not optional" and crossed as one +/// boxed `java.lang.Long`. The model says `Optional`, so it now takes the +/// decoupled `(present, value)` pair its bare twin does — and the emitter has to +/// put the `Box` back when it rebuilds, or the migration turns a working boxed +/// crossing into an `E0308`. +/// +/// `plain` is the control: the two fields must produce the same wire, since the +/// model says they are the same type. +#[prebindgen] +pub struct WrappedFields { + pub id: i64, + pub boxed: Box>, + pub plain: Option, +} + +/// Round-trip a [`WrappedFields`] so both field spellings cross in one call. +#[prebindgen] +pub fn wrapped_fields_sum(w: WrappedFields) -> i64 { + w.id + w.boxed.unwrap_or(0) + w.plain.unwrap_or(0) +} + +/// Transparent wrappers on the **input** side, one per specialized lowering. +/// +/// These lowerings do not *decode* their parameter, they **rebuild** it — a +/// literal `Payload { .. }`, an `Option::Some(v)`, a `Vec` pushed element by +/// element — so the wrappers the classification erased have to go back on before +/// the value reaches the signature. Rebuilding from the classification alone +/// hands an `Option` to a parameter spelled `Box>`: +/// `E0308` (#292 item 3, which replaced #290's refusals). +/// +/// Declared here rather than only in unit tests for the reason +/// [`boxed_note_echo`] is: this crate's generated binding is `include!`d and +/// **compiled**, so a missing or misplaced `Box::new` fails the build. Each has +/// an unwrapped twin already declared — the surfaces must come out identical, +/// since the model says the two spellings are one type. +/// +/// The layers are covered separately because each is applied at a different +/// point in the construction, and only a shape that exercises one can show it: +/// the core wrap goes inside the present gate, and the optional wrap around it. +/// +/// **`Box<&Payload>` is deliberately absent.** The flatten lowering could build +/// it — it owns a local and `Box::new(&local)` is well-typed — but a declared +/// parameter also needs a general converter entry, and a converter *produces* an +/// owned value: there is nothing for a `Box<&T>` to borrow from that outlives +/// the call (`E0106` on the generated signature). So the shape is refused at +/// resolution, by the converter's nature rather than the wrapper's. +#[prebindgen] +// A boxed parameter IS the point here — clippy is right that the `Box` buys +// nothing, and that is what makes it a fixture: the binding must cross it as +// the unwrapped type and put the wrapper back. +#[allow(clippy::boxed_local)] +pub fn boxed_payload_id(p: Box) -> i64 { + p.id +} + +/// The optional layer over the same rebuild — the wrap goes **around** the +/// present gate, where the core wrap goes inside it. +#[prebindgen] +// A boxed parameter IS the point here — clippy is right that the `Box` buys +// nothing, and that is what makes it a fixture: the binding must cross it as +// the unwrapped type and put the wrapper back. +#[allow(clippy::boxed_local)] +pub fn boxed_opt_payload_id(p: Box>) -> i64 { + p.as_ref().as_ref().map(|p| p.id).unwrap_or(-1) +} + +/// The option-scalar lowering (`(present, value)` raw pair) under a wrapper — +/// the rebuilt `Option` is re-wrapped before it reaches the signature. +#[prebindgen] +// A boxed parameter IS the point here — clippy is right that the `Box` buys +// nothing, and that is what makes it a fixture: the binding must cross it as +// the unwrapped type and put the wrapper back. +#[allow(clippy::boxed_local)] +pub fn boxed_opt_priority_weight(p: Box>) -> i64 { + match *p { + Some(v) => priority_weight(v) as i64, + None => -1, + } +} + +/// A wrapped **element** in the Vec-build path: the storage is `Vec>` +/// and each push wraps its own literal. +#[prebindgen] +pub fn boxed_elem_id_sum(ps: Vec>) -> i64 { + ps.iter().map(|p| p.id).sum() +} + +/// A wrapped **run**, by value: `mem::take` yields the owned `Vec`, so the +/// `Box` costs nothing. The borrowed twin (`&Box>`) is deliberately +/// **not** declared — interposing a `Box` between the caller's Vec and the +/// callee would require copying it, so that shape keeps the ordinary path. +#[prebindgen] +// A boxed parameter IS the point here — clippy is right that the `Box` buys +// nothing, and that is what makes it a fixture: the binding must cross it as +// the unwrapped type and put the wrapper back. +#[allow(clippy::boxed_local)] +pub fn boxed_run_id_sum(ps: Box>) -> i64 { + ps.iter().map(|p| p.id).sum() +} + /// Deliver a [`Ledger`] to a callback, so both conditional decompositions cross /// in ONE call — including the sum (`Report::outcome`) each one carries, whose /// `match` belongs inside the arm that binds the report. diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 1397156a..e263c69b 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -54,7 +54,7 @@ 4 api/lang/jnigen/jni/builder.rs 4 api/lang/jnigen/jni/emit/convert.rs 2 api/lang/jnigen/jni/emit/delivery.rs -7 api/lang/jnigen/jni/emit/flat_input.rs +5 api/lang/jnigen/jni/emit/flat_input.rs 17 api/lang/jnigen/jni/emit/names.rs 11 api/lang/jnigen/jni/emit/wrapper.rs 3 api/lang/jnigen/jni/fold.rs @@ -69,4 +69,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 122 +# total: 120 diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 6abc6c21..61fddc6e 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -382,6 +382,28 @@ impl TypeRef { names } + /// This type's identity as a table key with every transparent wrapper + /// removed — the key of [`stripped_syntax`](Self::stripped_syntax). + /// + /// What [`key`](Self::key) answers for a **spelling**, this answers for the + /// **type**. The two are different questions and both are legitimate: + /// + /// * a *conversion* is keyed by `key`, because `Box>` and + /// `Option` genuinely need different converter bodies — one has to put + /// a `Box` back and the other must not; + /// * a **declaration** is keyed by this, because a declaration says what a + /// type *is* to the destination language, and a wrapper the model erases + /// cannot change that. A `Box` parameter is a `Payload` to + /// Kotlin, so it must find `Payload`'s data-class declaration — keying it + /// by spelling finds nothing and silently costs the parameter its + /// lowering. + /// + /// Use this wherever the lookup is against declarations the binding author + /// wrote, and `key` wherever it is against something derived per spelling. + pub fn stripped_key(&self) -> crate::api::core::registry::TypeKey { + crate::api::core::registry::TypeKey::from_type(&self.stripped_syntax()) + } + /// This type's spelling with every [transparent /// wrapper](TRANSPARENT_WRAPPERS) removed — `Box>>` → /// `Option`, an unwrapped spelling → itself. diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 8fd585ce..c5d4ad6d 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -4,9 +4,12 @@ use super::*; // `flat` as a module for `TypeKind`: the bare name in this scope is jnigen's own // classifier (via `use super::*`), and an explicit import would shadow it. -use crate::api::core::{ - flat::{self, TypeRef}, - registry::Conversions, +use crate::api::{ + core::{ + flat::{self, TypeRef}, + registry::Conversions, + }, + lang::jnigen::jni::trait_impl::{build_through_erased_wrappers, build_through_wrappers}, }; pub(crate) fn struct_input_body( @@ -712,6 +715,11 @@ pub(crate) enum FlatFieldNode { direct_handle: bool, optional_handle: bool, rust_ty: Box, + /// The transparent wrappers this field's spelling adds over its + /// classification, outermost first — put back wherever the decode + /// **rebuilds** the value (an `Option::Some`/`None` literal) rather than + /// running the field's own converter, which already yields the spelling. + wrappers: Vec<&'static str>, }, Nested { field: syn::Ident, @@ -733,6 +741,11 @@ pub(crate) enum FlatFieldNode { /// Variants in declaration order; index == tag. variants: Vec, rust_ty: Box, + /// The transparent wrappers this field's spelling adds over its + /// classification, outermost first — put back wherever the decode + /// **rebuilds** the value (an `Option::Some`/`None` literal) rather than + /// running the field's own converter, which already yields the spelling. + wrappers: Vec<&'static str>, }, } @@ -744,44 +757,119 @@ pub(crate) struct FlatSumVariant { pub fields: Vec<(syn::Member, usize)>, } -/// Peel `&` then `Option<…>` off the model to reach the value a specialized -/// lowering would **rebuild**, refusing at any layer whose spelling adds a -/// wrapper the classification erased. -/// -/// One function because it is one rule. `kind` decides what the destination -/// sees; the **conversion** follows the syntax, and these lowerings do not -/// decode their parameter — they emit a literal `S { .. }`, wrap it in -/// `Option::Some`, and hand it to the source function. Rebuilding from the -/// classification alone produces the stripped type, so a parameter spelled -/// `Box>` receives an `Option`: `E0308` in the generated crate. +/// The three layers a specialized struct lowering descends through, each paired +/// with the reading whose spelling it must satisfy. /// -/// Refusing rather than rebuilding is a **gap, not a requirement**: -/// `Box::new(v)` is exactly what the syntax asks for and is trivially -/// emittable. It is left out here because doing it properly means teaching the -/// converters to rebuild each wrapper, and `Cow` cannot be rebuilt from an -/// owned payload at all. A wrapped spelling therefore keeps the general -/// converter path, which is correct if less direct. +/// `kind` decides what the destination sees; the **conversion** follows the +/// syntax, and this lowering does not decode its parameter — it emits a literal +/// `S { .. }`, wraps it in `Option::Some`, and hands it to the source function. +/// Rebuilding from the classification alone produces the *stripped* type, so a +/// parameter spelled `Box>` would receive an `Option`: `E0308` in +/// the generated crate. /// -/// The wrapper question goes to [`TypeRef::erased_wrapper`] — the model holds -/// both halves, so it is the only thing that can answer it, and no spelling is -/// taken apart here. -fn rebuildable_target(arg: &TypeRef) -> Option<(bool, bool, &TypeRef)> { - if arg.erased_wrapper().is_some() { - return None; +/// So each layer keeps its own reading, and the emitter puts that layer's +/// wrappers back as it builds outward — see [`RebuildTarget::wrap_core`] and its +/// siblings. Collected on the way **down** because an erasure sits *outside* the +/// layer it wraps: `Box<&S>` classifies as `Ref`, and reading `kind` first would +/// leave the `Box` unreachable. +pub(crate) struct RebuildTarget { + /// Wrappers over the borrow, if there is one — the `Box` of `Box<&S>`. + arg: Vec<&'static str>, + /// Wrappers over the `Option` — the `Box` of `Box>`. + under_borrow: Vec<&'static str>, + /// Wrappers over the `S { .. }` literal — the `Box` of `Option>`. + core: Vec<&'static str>, + /// `true` when the source fn takes `&Struct`. + pub by_ref: bool, + /// `true` when the value is `Option`-wrapped. + pub optional: bool, +} + +impl RebuildTarget { + /// Put back the wrappers standing over the `S { .. }` literal — + /// `Option>` wraps here, not at [`Self::wrap_optional`]. + pub fn wrap_core(&self, e: TokenStream) -> TokenStream { + Self::wrap(&self.core, e) + } + + /// Put back the wrappers over the `Option<..>` — the `Box` of + /// `Box>`. + /// + /// A no-op when the parameter is not optional, for the same reason + /// [`Self::wrap_arg`] is one when it is not a borrow: with no `Option` to + /// peel, `under_borrow` and `core` are the *same reading*, and wrapping at + /// both would apply one layer twice. + /// + /// Stated once as the rule the three share: **a layer's wrappers are + /// applied only where that layer exists**, and the innermost always applies. + pub fn wrap_optional(&self, e: TokenStream) -> TokenStream { + if !self.optional { + return e; + } + Self::wrap(&self.under_borrow, e) + } + + /// Put back the wrappers over the **borrow** — the `Box` of `Box<&S>`, + /// which goes on after the call site has added its `&`. + /// + /// A no-op when the parameter is not a borrow, and that is not an + /// optimisation: with no `&` to peel, `arg` and `under_borrow` are the *same + /// reading*, so wrapping at both would apply one layer twice — + /// `Box::new(Box::new(v))` for a `Box>` parameter. + pub fn wrap_arg(&self, e: TokenStream) -> TokenStream { + if !self.by_ref { + return e; + } + Self::wrap(&self.arg, e) } + + /// Every wrap goes through the one helper, and every layer was proved + /// buildable by [`rebuildable_target`] before a plan existed — so a `None` + /// here would mean the descent and the emission disagree about the same + /// reading, which is a bug in this file rather than an unsupported source. + fn wrap(names: &[&'static str], e: TokenStream) -> TokenStream { + build_through_wrappers(names, e) + .expect("every layer was checked buildable when the plan was built") + } +} + +/// Descend `&` then `Option<…>` off the model to the struct a specialized +/// lowering will **rebuild**, keeping each layer's reading so its spelling can +/// be restored. +/// +/// One function because it is one rule, and the layers are checked **on the way +/// down**: an erasure sits outside the layer it wraps, so `Box<&S>` classifies +/// as `Ref` and interpreting `kind` before asking would discard the `Box`. +/// +/// The only refusal left is a wrapper the adapter cannot **build** — `Cow`, by +/// policy rather than by impossibility (see its `WRAPPER_OPS` row). A wrapped +/// spelling that declines here keeps the general converter path, which is +/// correct if less direct. +fn rebuildable_target(arg: &TypeRef) -> Option<(RebuildTarget, &TypeRef)> { + // A probe per layer: "can this spelling be rebuilt at all", asked before the + // peel that would hide it. The token is irrelevant — only the `Option` is. + let buildable = |t: &TypeRef| build_through_erased_wrappers(t, quote!(__probe)).map(|_| ()); + buildable(arg)?; let by_ref = arg.borrow_target().is_some(); let t1 = arg.borrow_target().unwrap_or(arg); - if t1.erased_wrapper().is_some() { - return None; - } + buildable(t1)?; let optional = t1.optional_inner().is_some(); let inner = t1.optional_inner().unwrap_or(t1); - // The struct is rebuilt BY NAME (`S { .. }`), so its own spelling must name - // it — a `Box` target would need the `Box::new` this emitter never writes. - if inner.erased_wrapper().is_some() { - return None; - } - Some((by_ref, optional, inner)) + // The struct is rebuilt BY NAME (`S { .. }`), and its own spelling may add a + // wrapper over that name — `Box` gets its `Box::new` at `wrap_core`. + buildable(inner)?; + // Only the wrapper LISTS are kept: they are all a rebuild uses, and a + // `TypeRef` apiece would put ~800 bytes into every `InputKind`. + Some(( + RebuildTarget { + arg: arg.erased_wrappers(), + under_borrow: t1.erased_wrappers(), + core: inner.erased_wrappers(), + by_ref, + optional, + }, + inner, + )) } /// A flattened plan for one struct input parameter. Built once by @@ -794,6 +882,9 @@ pub(crate) struct FlatInputPlan { /// Vec/slice element lowering deliberately retains its previous /// non-recursive ABI; callers use this bit to decline recursive plans. pub contains_nested: bool, + /// The layer readings the rebuild has to satisfy — carried rather than + /// re-derived at the emission sites, so the descent is stated once. + pub target: RebuildTarget, } // `impl_into_target` lived here: it extracted `S` from an `impl Into + …` @@ -805,16 +896,11 @@ pub(crate) struct FlatInputPlan { // jnigen's actual `impl Into<…>` support is elsewhere: plugin wrapper exts build // a `ConverterImpl::function` by hand via `Declarations::input_converter_name`, // which never consults this. - -/// Peel a leading `&`/`&mut` then an `Option<…>` to expose the inner type used -/// for enum/struct detection (`&Priority`, `Option` → `Priority`). -pub(crate) fn flat_probe_inner(ty: &syn::Type) -> syn::Type { - let stripped = match ty { - syn::Type::Reference(r) => (*r.elem).clone(), - other => other.clone(), - }; - option_inner_type(&stripped).unwrap_or(stripped) -} +// `flat_probe_inner` lived here: it peeled `&` then `Option` off a SPELLING to +// reach the type an enum probe should ask about. Its last caller now asks +// `is_kotlin_enum_reading`, whose `enum_probe` peels the same two layers off the +// model — so `Box` probes as `Priority` where this answered about the +// wrapper (#289). /// Kotlin literal that fills a leaf slot when its `Option` parent is /// absent (the `present` flag tells Rust to ignore it). `None` for nullable @@ -926,9 +1012,10 @@ fn build_flat_sum_field( native_prefix: &str, field_ref: &str, nullable_access: bool, - rust_ty: &syn::Type, + field_reading: &TypeRef, leaves: &mut Vec, ) -> Option { + let rust_ty = field_reading.syntax(); use crate::api::core::types_util::SumSpec; let ident = bare_path_ident(sum_ty)?; @@ -1073,6 +1160,7 @@ fn build_flat_sum_field( let module = ext.fn_module(registry, &ident); Some(FlatFieldNode::Sum { + wrappers: field_reading.erased_wrappers(), field, tag_leaf, present_leaf, @@ -1173,10 +1261,11 @@ pub(crate) fn build_flat_input_plan( arg: &TypeRef, ) -> Result, FlatInputError> { // 1. Resolve the struct target through `&` and `Option<…>` — off the model, - // and refusing any layer whose spelling the rebuild could not satisfy. - let Some((by_ref, optional, inner)) = rebuildable_target(arg) else { + // keeping each layer's reading so the rebuild can restore its spelling. + let Some((target, inner)) = rebuildable_target(arg) else { return Ok(None); }; + let (by_ref, optional) = (target.by_ref, target.optional); // `impl Into` is NOT peeled here, and cannot be: the model refuses // `impl Trait` that is not the callback form (`DisallowedImplTrait`), so a // parameter spelled that way never becomes a reading and never reaches this @@ -1190,14 +1279,18 @@ pub(crate) fn build_flat_input_plan( let Some(name) = id.ident() else { return Ok(None); }; - let Some(st) = registry - .flat() - .struct_type(&name) - .map(|st| &st.origin.syntax) - else { + // The ELEMENT, not the item it was parsed from: its fields already carry + // readings, which is the whole of #289. + let Some(st) = registry.flat().struct_type(&name) else { return Ok(None); }; - let key = inner.key(); + // The DECLARATION is keyed by the type, not by the spelling: a + // `Box` parameter is a `Payload` to Kotlin and must find + // `Payload`'s data-class declaration. Keying by spelling looked up + // `Box < Payload >`, found nothing, and silently dropped the parameter to + // the general converter — the flatten lowering was unreachable for every + // wrapped core. + let key = inner.stripped_key(); let Some(cfg) = ext.types.get(&key) else { return Ok(None); }; @@ -1254,14 +1347,26 @@ pub(crate) fn build_flat_input_plan( root, by_ref, contains_nested, + target, })) } #[allow(clippy::too_many_arguments)] +/// Takes the **element**, not the `syn::ItemStruct` it was parsed from (#289): +/// `flat::Field::ty` is already a `TypeRef`, so every peel below is the model's +/// answer rather than a last-path-segment test on tokens that had a reading one +/// level up. +/// +/// That matters here and not only on principle. `option_inner_type` reads the +/// last path segment, so a field spelled `Box>` answered "not +/// optional" and crossed as one boxed object; the model says `Optional` and it +/// takes the decoupled `(present, value)` pair like its bare twin. The emitter +/// then has to put the `Box` back — which is why this migration could not land +/// before the rebuild did. fn build_flat_struct_node( ext: &Declarations, registry: &Registry, - st: &syn::ItemStruct, + st: &flat::Struct, optional: bool, native_prefix: &str, access_prefix: &str, @@ -1270,7 +1375,7 @@ fn build_flat_struct_node( stack: &mut Vec, leaves: &mut Vec, ) -> Result { - let node_key = TypeKey::from_ident(&st.ident); + let node_key = TypeKey::from_ident(&st.name); if stack.contains(&node_key) { return Err(flat_error( root, @@ -1285,13 +1390,6 @@ fn build_flat_struct_node( "recursive flattening exceeds depth 16", )); } - let syn::Fields::Named(named) = &st.fields else { - return Err(flat_error( - root, - native_prefix, - "only named-field structs can flatten", - )); - }; stack.push(node_key); let present_ident = if optional { let native = format!("{native_prefix}_present"); @@ -1301,9 +1399,17 @@ fn build_flat_struct_node( None }; let mut fields = Vec::new(); - for field in &named.named { - let Some(fident) = field.ident.clone() else { - return Err(flat_error(root, native_prefix, "unnamed field")); + for field in &st.fields { + // A positional field has no name to derive a Kotlin property from, which + // is what "only named-field structs can flatten" used to say one level + // up. Said per field now, because the element models a field list rather + // than a `syn::Fields` shape. + let Some(fident) = field.name.clone() else { + return Err(flat_error( + root, + native_prefix, + "only named-field structs can flatten", + )); }; let fcamel = mangle_kotlin_ident(&snake_to_camel(&fident.to_string())); let child_native = format!("{native_prefix}_{}", fident); @@ -1312,12 +1418,17 @@ fn build_flat_struct_node( } else { format!("{access_prefix}.{fcamel}") }; - let nested_ty = option_inner_type(&field.ty).unwrap_or_else(|| field.ty.clone()); + // The optional layer off the MODEL, asked once and reused: every site + // below that wants "is this field optional" reads this, so they cannot + // disagree with each other the way seven independent path-segment tests + // could (#273). + let field_optional = field.ty.optional_inner().is_some(); + let nested = field.ty.optional_inner().unwrap_or(&field.ty); + let nested_ty = nested.syntax().clone(); // A data-carrying enum flattens into a tag plus one group per variant. // `None` means some payload is not leaf-shaped — fall through and let // it cross as one object through its own converter. if matches!(ext.type_kind(registry, &nested_ty), TypeKind::Sum) { - let field_optional = option_inner_type(&field.ty).is_some(); if let Some(node) = build_flat_sum_field( ext, registry, @@ -1340,11 +1451,11 @@ fn build_flat_struct_node( } = ext.type_kind(registry, &nested_ty) { if cfg.name_spec.is_some() && !cfg.special_decl() && !cfg.jobject_input { - let child_optional = option_inner_type(&field.ty).is_some(); + let child_optional = field_optional; let node = build_flat_struct_node( ext, registry, - &child.origin.syntax, + child, child_optional, &child_native, &field_ref, @@ -1362,28 +1473,22 @@ fn build_flat_struct_node( } let path = child_native.clone(); - let Some(fentry) = registry - .reading_of(&field.ty) - .and_then(|tr| registry.input_entry(&tr)) - else { + // The field's own reading straight to its entry — the `reading_of` hop + // only ever recovered what the field already carried. + let Some(fentry) = registry.input_entry(&field.ty) else { return Err(flat_error( root, &path, - format!( - "field type `{}` has no input converter", - TypeKey::from_type(&field.ty) - ), + format!("field type `{}` has no input converter", field.ty.key()), )); }; // Nullable primitive/enum with no niche: keep the allocation-free // `(present, value)` representation at every recursion depth. - if let Some(inner_ty) = option_inner_type(&field.ty) { - if !matches!(inner_ty, syn::Type::Reference(_)) { - if let Some(inner) = registry - .reading_of(&inner_ty) - .and_then(|tr| registry.input_entry(&tr)) - { + if let Some(inner_reading) = field.ty.optional_inner() { + let inner_ty = inner_reading.syntax().clone(); + if inner_reading.borrow_target().is_none() { + if let Some(inner) = registry.input_entry(inner_reading) { if let Some(prim) = JniPrim::from_wire(&inner.destination) { if inner.niches.clone().carve().is_none() && inner.metadata.projection.is_none() @@ -1414,7 +1519,8 @@ fn build_flat_struct_node( present_leaf: Some(present_index), direct_handle: false, optional_handle: false, - rust_ty: Box::new(field.ty.clone()), + rust_ty: Box::new(field.ty.syntax().clone()), + wrappers: field.ty.erased_wrappers(), }); continue; } @@ -1431,7 +1537,7 @@ fn build_flat_struct_node( // provides a niche already have a primitive destination and stay // a single leaf below. if proj.kind == ProjectionKind::Unsigned64 { - if let Some(inner_ty) = option_inner_type(&field.ty) { + if let Some(inner_ty) = field.ty.optional_inner().map(|t| t.syntax().clone()) { if JniPrim::from_wire(&fentry.destination).is_none() { let inner = registry .reading_of(&inner_ty) @@ -1466,7 +1572,8 @@ fn build_flat_struct_node( present_leaf: Some(present_index), direct_handle: false, optional_handle: false, - rust_ty: Box::new(field.ty.clone()), + rust_ty: Box::new(field.ty.syntax().clone()), + wrappers: field.ty.erased_wrappers(), }); continue; } @@ -1481,7 +1588,7 @@ fn build_flat_struct_node( "collections of handles retain their collection boundary", )); } - let optional_handle = option_inner_type(&field.ty).is_some(); + let optional_handle = field_optional; let value_index = push_handle_leaf( leaves, &child_native, @@ -1495,12 +1602,13 @@ fn build_flat_struct_node( present_leaf: None, direct_handle: true, optional_handle, - rust_ty: Box::new(field.ty.clone()), + rust_ty: Box::new(field.ty.syntax().clone()), + wrappers: field.ty.erased_wrappers(), }); continue; } ProjectionKind::Unsigned64 => { - let is_opt = option_inner_type(&field.ty).is_some(); + let is_opt = field_optional; let access = if is_opt || nullable_context { let sentinel = proj .niche_sentinels @@ -1525,19 +1633,23 @@ fn build_flat_struct_node( present_leaf: None, direct_handle: false, optional_handle: false, - rust_ty: Box::new(field.ty.clone()), + rust_ty: Box::new(field.ty.syntax().clone()), + wrappers: field.ty.erased_wrappers(), }); continue; } } } - let field_is_option = option_inner_type(&field.ty).is_some(); + let field_is_option = field_optional; // The enum branch is self-contained: when it coalesces (`?.value ?: 0`) // it already yields a non-null `Int`, so block (B) below must not append // a second default (which produced the dead `?: 0 ?: 0`, issue #144). let mut enum_coalesced = false; - let mut access = if ext.is_kotlin_enum(&flat_probe_inner(&field.ty)) { + // The enum probe off the MODEL (`enum_probe` peels the same `&`/`Option` + // layers `flat_probe_inner` peeled off tokens), so a `Box` + // field answers as a `Priority` does. + let mut access = if ext.is_kotlin_enum_reading(&field.ty) { if field_is_option || nullable_context { enum_coalesced = true; format!("{field_ref}?.value ?: 0") @@ -1568,13 +1680,14 @@ fn build_flat_struct_node( present_leaf: None, direct_handle: false, optional_handle: false, - rust_ty: Box::new(field.ty.clone()), + rust_ty: Box::new(field.ty.syntax().clone()), + wrappers: field.ty.erased_wrappers(), }); } stack.pop(); Ok(FlatStructNode { - struct_module: struct_module_path(ext, registry, &st.ident), - struct_ident: st.ident.clone(), + struct_module: struct_module_path(ext, registry, &st.name), + struct_ident: st.name.clone(), binding: format_ident!("__flat_{native_prefix}"), optional, present_ident, @@ -1593,18 +1706,21 @@ pub(crate) fn render_flat_input_decode( arg_ident: &syn::Ident, on_err: &TokenStream, ) -> (TokenStream, TokenStream) { - let reconstruct = render_flat_struct_node(plan, &plan.root, on_err); + let reconstruct = render_flat_struct_node(plan, &plan.root, Some(&plan.target), on_err); let root_binding = &plan.root.binding; let prelude = quote! { #reconstruct let #arg_ident = #root_binding; }; - let call_arg = if plan.by_ref { + // The borrow, then the wrappers standing OVER it — `Box<&S>` is + // `Box::new(&arg)`, in that order, because the erasure sits outside the + // layer it wraps and the `&` is that layer. + let borrowed = if plan.by_ref { quote!(&#arg_ident) } else { quote!(#arg_ident) }; - (prelude, call_arg) + (prelude, plan.target.wrap_arg(borrowed)) } fn render_entry_decode( @@ -1653,9 +1769,14 @@ fn render_entry_decode( body } +/// `target` is `Some` for the parameter's ROOT node, whose spelling may add +/// transparent wrappers the rebuild has to restore, and `None` for a nested one +/// — a nested struct is reached through a field, and a field's own wrappers are +/// applied where that field is decoded. fn render_flat_struct_node( plan: &FlatInputPlan, node: &FlatStructNode, + target: Option<&RebuildTarget>, on_err: &TokenStream, ) -> TokenStream { let mut decodes = TokenStream::new(); @@ -1663,7 +1784,7 @@ fn render_flat_struct_node( for field in &node.fields { match field { FlatFieldNode::Nested { field, node: child } => { - decodes.extend(render_flat_struct_node(plan, child, on_err)); + decodes.extend(render_flat_struct_node(plan, child, None, on_err)); let child_binding = &child.binding; inits.push(quote!(#field: #child_binding)); } @@ -1671,6 +1792,7 @@ fn render_flat_struct_node( // variant. ONLY that arm's leaves are converted — the inert // groups carry wire defaults nobody reads. FlatFieldNode::Sum { + wrappers, field, tag_leaf, present_leaf, @@ -1724,21 +1846,33 @@ fn render_flat_struct_node( } } }; + // The rebuilt value, then the wrappers this FIELD's spelling + // adds — the slot is ascribed `#rust_ty`, so a `Box>` + // field needs its `Box` back. Only the rebuilding arms wrap: the + // fall-through below runs the field's own converter, which + // already yields the spelling. + let wrap = |e: TokenStream| { + build_through_wrappers(wrappers, e) + .expect("a field spelling the plan accepted is buildable") + }; if let Some(p) = present_leaf { let present = &plan.leaves[*p].native_ident; - decodes.extend(quote! { - let #tmp: #rust_ty = if #present != 0u8 { + let gated = wrap(quote! { + if #present != 0u8 { ::core::option::Option::Some(#build) } else { ::core::option::Option::None - }; + } }); + decodes.extend(quote! { let #tmp: #rust_ty = #gated; }); } else { - decodes.extend(quote! { let #tmp: #rust_ty = #build; }); + let built = wrap(build); + decodes.extend(quote! { let #tmp: #rust_ty = #built; }); } inits.push(quote!(#field: #tmp)); } FlatFieldNode::Value { + wrappers, field, value_leaf, present_leaf, @@ -1749,11 +1883,15 @@ fn render_flat_struct_node( let leaf = &plan.leaves[*value_leaf]; let wire = &leaf.native_ident; let tmp = format_ident!("{}_{}", node.binding, field); + let wrap = |e: TokenStream| { + build_through_wrappers(wrappers, e) + .expect("a field spelling the plan accepted is buildable") + }; if *direct_handle { let target = option_inner_type(rust_ty).unwrap_or_else(|| (**rust_ty).clone()); if *optional_handle { - decodes.extend(quote! { - let #tmp: #rust_ty = if #wire == 0 { + let gated = wrap(quote! { + if #wire == 0 { ::core::option::Option::None } else { if (#wire & 1) == 1 { @@ -1763,8 +1901,9 @@ fn render_flat_struct_node( ::core::option::Option::Some(unsafe { *::std::boxed::Box::from_raw(#wire as *mut #target) }) - }; + } }); + decodes.extend(quote! { let #tmp: #rust_ty = #gated; }); } else { decodes.extend(quote! { if #wire == 0 || (#wire & 1) == 1 { @@ -1785,14 +1924,15 @@ fn render_flat_struct_node( let present = &plan.leaves[*present_index].native_ident; let inner_tmp = format_ident!("{}_value", tmp); let decode = render_entry_decode(entry, wire, &inner_tmp, on_err); - decodes.extend(quote! { - let #tmp = if #present != 0u8 { + let gated = wrap(quote! { + if #present != 0u8 { #decode ::core::option::Option::Some(#inner_tmp) } else { ::core::option::Option::None - }; + } }); + decodes.extend(quote! { let #tmp = #gated; }); } else { decodes.extend(render_entry_decode(entry, wire, &tmp, on_err)); } @@ -1804,21 +1944,50 @@ fn render_flat_struct_node( let module = &node.struct_module; let sid = &node.struct_ident; let binding = &node.binding; - let built = quote!(#module::#sid { #(#inits),* }); + // The struct literal, then the wrappers the CORE spelling adds over it — + // `Option>` gets its `Box::new` here, inside the present gate, not + // around it. `None` for a nested node, whose own layers are its field's + // question rather than the parameter's. + let built = match target { + Some(t) => t.wrap_core(quote!(#module::#sid { #(#inits),* })), + None => quote!(#module::#sid { #(#inits),* }), + }; + // …and the wrappers over the `Option` (or over the bare value) go around + // the whole gate — the `Box` of `Box>`. + let outer = |e: TokenStream| match target { + Some(t) => t.wrap_optional(e), + None => e, + }; if node.optional { let present = node.present_ident.as_ref().expect("optional node has gate"); - quote! { - let #binding = if #present != 0u8 { + // `#decodes` belongs **inside** the true arm, and that is a correctness + // requirement rather than a tidiness one: when the Kotlin object is null + // its leaves carry inert placeholders, and decoding them is not + // side-effect-free. A required handle field arrives as pointer `0`, so + // an unconditional direct-handle decode calls `signal_binding_error` and + // returns instead of delivering `None`; an enum with no discriminant `0` + // and a fallible custom converter fail on their placeholders the same + // way. + // + // The wrapper goes around the whole conditional, which is what the + // `Option` layer wraps — so `outer` applies to the `if`, never between + // it and the decodes. + let gate = outer(quote! { + if #present != 0u8 { #decodes ::core::option::Option::Some(#built) } else { ::core::option::Option::None - }; + } + }); + quote! { + let #binding = #gate; } } else { + let value = outer(built); quote! { #decodes - let #binding = #built; + let #binding = #value; } } } @@ -1852,6 +2021,17 @@ pub(crate) struct OptionScalarInputPlan { pub value_kt_type: String, /// Kotlin zero literal filling the value leaf when the option is absent. pub value_kt_zero: String, + /// The transparent wrappers the parameter's spelling adds over `Optional`, + /// outermost first — what the emitter puts back. + /// + /// This plan **rebuilds** its parameter — the emitter writes a literal + /// `Option::Some(v)` / `Option::None` and hands it to the source fn — so a + /// parameter spelled `Box>` must receive a `Box`, not the bare + /// `Option` the classification names. Carried rather than re-derived at the + /// two emission sites, which would be the same rule stated twice; the list + /// rather than the reading, because that is all a rebuild uses and this + /// plan sits in `InputKind`, whose size every variant pays. + pub arg_wrappers: Vec<&'static str>, /// `true` when the inner is an `enum_class` — the call site reads `?.value`. pub is_enum: bool, } @@ -1868,13 +2048,13 @@ pub(crate) fn build_option_scalar_input_plan( param_name: &syn::Ident, arg: &TypeRef, ) -> Option { - // The optional layer off the model — but the emitter rebuilds a bare - // `Option::Some(v)` and hands it to the source fn, so a spelling the model - // erased a wrapper from could not receive it. Conversion follows the syntax; - // see [`rebuildable_target`], which applies the same rule to the struct path. - if arg.erased_wrapper().is_some() { - return None; - } + // The wrappers this spelling adds over `Optional` have to be BUILDABLE, not + // absent: the emitter rebuilds a bare `Option::Some(v)` and hands it to the + // source fn, so a parameter spelled `Box>` receives a `Box`. + // Asked here, before the peel, because an erasure sits outside the layer it + // wraps — and asked as "can I build it" rather than "is there one", so the + // only refusal left is a wrapper `WRAPPER_OPS` declines (`Cow`). + build_through_erased_wrappers(arg, quote!(__probe))?; let inner = arg.optional_inner()?; // `Option<&T>` is the nullable-borrow / handle path, not a scalar. if inner.borrow_target().is_some() { @@ -1912,6 +2092,7 @@ pub(crate) fn build_option_scalar_input_plan( value_kt_type: prim.kotlin_type().to_string(), value_kt_zero: prim.kotlin_zero().to_string(), is_enum, + arg_wrappers: arg.erased_wrappers(), }) } diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs index b9c4c588..0d2447f6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs @@ -5,7 +5,10 @@ use super::*; // `flat` as a module for `TypeKind`: the bare name in this scope is jnigen's own // classifier (reached through `use super::*`), and an explicit import would win // over the glob and silently retarget it. -use crate::api::core::flat::{self, RefMode, TypeRef}; +use crate::api::{ + core::flat::{self, RefMode, TypeRef}, + lang::jnigen::jni::trait_impl::build_through_erased_wrappers, +}; // `slice_or_vec_elem` lived here: it matched `&[T]` / `Vec` off the SPELLING // and returned the element. `vec_build_elem` was its only caller and now reads @@ -48,22 +51,45 @@ pub(crate) fn vec_build_elem( // sequence — whose spelling is a clean `Vec` — and let the outer `Box` // through unseen. Every layer is checked on the way down, the way // `rebuildable_target` does it. - if arg.erased_wrapper().is_some() { - return None; - } let (run, by_ref) = match arg.kind() { flat::TypeKind::Ref { mode, inner } if *mode == RefMode::Shared => (&**inner, true), _ => (arg, false), }; - // Still needed after the peel: `&Box>` puts the wrapper on the - // referent, where the check above (a `syn::Type::Reference`) cannot see it. - if run.erased_wrapper().is_some() { - return None; + // A wrapper over the RUN is buildable only on the by-value path, and the + // reason is a cost rather than a type error. By value the local is owned, so + // `Box>` is `Box::new(mem::take(..))` — free. Borrowed, the local is + // a borrow of the Vec the Kotlin side owns, and there is no way to put a + // `Box` between that borrow and the callee without **copying** the run + // (`&Box::new(v.clone())`) — which needs a `T: Clone` nothing here + // guarantees, and silently adds a per-call copy to a path whose entire point + // is not having one. + // + // Definitive, not deferred: the borrowed shape keeps the `input_vec` path, + // which is correct. If a binding ever wants the copy, it is a decision to + // make on purpose, at the declaration. + let wrapped_run = !arg.erased_wrappers().is_empty() || !run.erased_wrappers().is_empty(); + if wrapped_run { + if by_ref { + return None; + } + // By value: both layers must be buildable (`Cow` still declines). + build_through_erased_wrappers(arg, quote!(__probe))?; + build_through_erased_wrappers(run, quote!(__probe))?; } let elem = run.sequence_elem()?; - // The element is spelled into `Vec<#elem>` and rebuilt per push, so a - // wrapped element spelling is unbuildable for the same reason. - if elem.erased_wrapper().is_some() { + // A wrapped ELEMENT keeps the general converter path, and the obstruction is + // naming rather than typing. The helper trio stores a `Vec<#elem>`, so + // `Vec` and `Vec>` are two different storages needing + // two trios — but the trio's base name is derived from the element's + // **Kotlin class** (`Payload` → `payloadVec`), which the two share, so both + // would emit `payloadVecNew`/`Push`/`Free` and collide. + // + // Definitive as long as the name comes from the Kotlin class: the + // alternative is spelling a Rust wrapper into a JNI symbol, which is the + // representation leak this whole layer exists to prevent. The general + // converter serves the shape correctly (see `input_transparent_bridge`), + // just without the per-element push loop. + if !elem.erased_wrappers().is_empty() { return None; } // The element must flatten; the probe ident is irrelevant here. @@ -256,6 +282,7 @@ pub(crate) fn build_vec_build_helper_items( } let module = &h.plan.root.struct_module; let sid = &h.plan.root.struct_ident; + named.push(( push_sym.clone(), syn::parse_quote!( diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs index e8f41827..f2bf527a 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/wrapper.rs @@ -4,7 +4,9 @@ use super::*; use crate::api::{ core::{registry::Conversions, types_util::result_ok_type}, - lang::jnigen::jni::trait_impl::read_through_erased_wrappers, + lang::jnigen::jni::trait_impl::{ + build_through_erased_wrappers, build_through_wrappers, read_through_erased_wrappers, + }, }; pub(crate) fn emit_jni_function_wrapper( @@ -558,19 +560,30 @@ fn emit_input_param( wire_params.push(quote!(#vid: #vwire)); let conv = &sp.inner_conv; let tmp = format_ident!("__{}_val", arg_ident); + // The rebuilt `Option`, then the wrappers the parameter's spelling + // adds over it — `Box>` gets its `Box` back here, because + // nothing between this and the source call re-spells the value. + // The plan only exists when the build resolves, so this cannot fail. + let built = build_through_wrappers( + &sp.arg_wrappers, + quote! { + if #pid != 0u8 { + let #tmp = match #conv(&mut env, &#vid) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, &__e.to_string()); + return #on_err; + } + }; + ::core::option::Option::Some(#tmp) + } else { + ::core::option::Option::None + } + }, + ) + .expect("an option-scalar plan is built only for a buildable spelling"); prelude.push(quote! { - let #arg_ident = if #pid != 0u8 { - let #tmp = match #conv(&mut env, &#vid) { - ::core::result::Result::Ok(__v) => __v, - ::core::result::Result::Err(__e) => { - signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, &__e.to_string()); - return #on_err; - } - }; - ::core::option::Option::Some(#tmp) - } else { - ::core::option::Option::None - }; + let #arg_ident = #built; }); (wire_params, prelude, quote!(#arg_ident)) } @@ -589,15 +602,27 @@ fn emit_input_param( let handle_ident = format_ident!("{}_handle", arg_ident); wire_params.push(quote!(#handle_ident: jni::sys::jlong)); if *by_ref { + // `vec_build_elem` refuses a wrapped run on this path, so the + // borrow is the parameter's own spelling and there is nothing + // to put back. prelude.push(quote!( let #arg_ident: &[#elem] = unsafe { &*(#handle_ident as *const Vec<#elem>) }; )); } else { - prelude.push(quote!( - let #arg_ident: Vec<#elem> = - unsafe { ::core::mem::take(&mut *(#handle_ident as *mut Vec<#elem>)) }; - )); + // By value the local is owned, so the run's wrappers go back on + // for free — `Box>` is `Box::new(mem::take(..))`. The + // ascription is dropped rather than restated: the wrapped + // spelling is what the expression now produces, and naming it + // here would be the same fact written twice. + let taken = build_through_erased_wrappers( + &leaf.reading, + quote!(unsafe { + ::core::mem::take(&mut *(#handle_ident as *mut Vec<#elem>)) + }), + ) + .expect("vec_build_elem accepted this run spelling"); + prelude.push(quote!(let #arg_ident = #taken;)); } (wire_params, prelude, quote!(#arg_ident)) } @@ -843,19 +868,31 @@ pub(crate) fn emit_expanded_param( let inner_conv = &sp.inner_conv; wire_params.push(quote!(#present_ident: jni::sys::jboolean)); wire_params.push(quote!(#value_ident: #value_wire)); + // The local is ascribed the leaf's own SPELLING (`leaf_ty`), so the + // rebuilt `Option` has to be wrapped back up to match it — the same + // rule as the parameter path above, and the reason the ascription + // can stay as written rather than being weakened to the stripped + // type. + let built = build_through_wrappers( + &sp.arg_wrappers, + quote! { + if #present_ident != 0u8 { + let __v = match #inner_conv(&mut env, &#value_ident) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, &__e.to_string()); + return #on_err; + } + }; + ::core::option::Option::Some(__v) + } else { + ::core::option::Option::None + } + }, + ) + .expect("an option-scalar plan is built only for a buildable spelling"); prelude.push(quote!( - let #local: #leaf_ty = if #present_ident != 0u8 { - let __v = match #inner_conv(&mut env, &#value_ident) { - ::core::result::Result::Ok(__v) => __v, - ::core::result::Result::Err(__e) => { - signal_binding_error(&mut env, &__error_sink, &__SINK_MID, __SINK_FQN, __SINK_DESCR, &__e.to_string()); - return #on_err; - } - }; - ::core::option::Option::Some(__v) - } else { - ::core::option::Option::None - }; + let #local: #leaf_ty = #built; )); leaf_locals.push(local); continue; diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index 9250211a..b6f18c63 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -168,7 +168,10 @@ impl Declarations { return Some(c); } } - None + // 4. Last resort: the spelling differs from something convertible only + // by the wrappers the model erased. Nothing that resolves above + // reaches here, so this adds routes rather than changing them. + self.input_transparent_bridge(ty, registry) } /// Select the output converter for `ty`: terminals, user wrappers, then diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs index c02aae58..6cac249c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs @@ -1711,30 +1711,27 @@ fn the_enum_probe_sees_through_wrappers_a_spelling_key_misses() { assert!(ext.is_kotlin_enum(field("plain").syntax())); } -/// A **transparently-wrapped** parameter spelling does not take a specialized -/// lowering that would rebuild the unwrapped type. +/// A **transparently-wrapped** parameter takes the same specialized lowering as +/// its bare twin, and the emitter puts the wrapper back. /// /// The model erases `Box`/`Cow` ([`TRANSPARENT_WRAPPERS`]), so -/// `Box>` classifies as `Optional` exactly as `Option` does — -/// and reading the layers off the model is what the reading-based probes were -/// changed to do. But `build_option_scalar_input_plan` does not *decode* the -/// parameter, it **rebuilds** it: the emitter writes a literal -/// `Option::Some(v)` / `Option::None` and hands that to the source function. -/// Handing a bare `Option` to a parameter spelled `Box>` is -/// an `E0308` in the generated crate. +/// `Box>` classifies as `Optional` exactly as `Option` does. +/// But `build_option_scalar_input_plan` does not *decode* the parameter, it +/// **rebuilds** it: the emitter writes a literal `Option::Some(v)` / +/// `Option::None` and hands that to the source function, and handing a bare +/// `Option` to a parameter spelled `Box>` is an `E0308`. /// -/// So the selection asks the spelling too ([`rebuilt_value_satisfies`]) and -/// declines, exactly as `decoded_vec_satisfies` makes the general converter path -/// decline `&Box>` (see -/// `a_borrowed_transparent_sequence_wrapper_is_not_decoded_as_a_vec`). +/// #290 closed that by **declining** the wrapped spelling. #292 item 3 replaced +/// the refusal with the rebuild — `Box::new(..)` is exactly what the syntax +/// asks for — so what this pins flipped: the wrapped parameter must now reach +/// the decoupled `(present, value)` wire, *and* the Rust side must re-wrap. /// -/// **What this pins is the refusal**, and it is asserted on the *pair* so it -/// cannot pass vacuously: the bare twin must still take the decoupled -/// `(present, value)` wire, and the wrapped one must not. The generated Rust is -/// never compiled by this suite (#269), so the `E0308` itself is out of reach — -/// the reachable property is that the emitter is never asked to write it. +/// Asserted on the **pair** in both artifacts so it cannot pass vacuously: the +/// bare twin must take the same Kotlin surface (or the wrapped one proves +/// nothing) and must **not** get a `Box::new` (or the wrap assertion would hold +/// for an emitter that wrapped everything). #[test] -fn a_transparently_wrapped_option_does_not_take_the_present_value_pair() { +fn a_transparently_wrapped_option_takes_the_present_value_pair_and_is_rebuilt() { let loc = myflat_loc(); let items: Vec<(syn::Item, SourceLocation)> = vec![ ( @@ -1802,18 +1799,36 @@ fn a_transparently_wrapped_option_does_not_take_the_present_value_pair() { otherwise this test proves nothing about the wrapped one:\n{kotlin}" ); - // The finding: `Box>` must NOT, because the emitter would - // rebuild a bare `Option` for a parameter that is not one. + // …and so does the wrapped one. The two spellings are one type to Kotlin, + // so an identical surface is the whole claim — a wrapper must not cost a + // parameter its lowering. assert!( - !kc.contains("zBoxed(modePresent"), - "`Box>` took the present/value lowering, which rebuilds a \ - bare `Option` and hands it to a fn expecting `Box>` \ - — an E0308 in the generated crate:\n{kotlin}" + kc.contains("zBoxed(modePresent:Boolean,modeValue:Int"), + "`Box>` must take the same present/value lowering as its \ + bare twin — the model erases the `Box`, and the emitter puts it back \ + rather than declining the shape:\n{kotlin}" + ); + + // The Rust half, which is what makes taking that lowering legal: the + // rebuilt `Option` is wrapped back up before it reaches the source fn. + // Without this the extern hands an `Option` to a parameter spelled + // `Box>` — `E0308`, and no Kotlin assertion could see it. + let rust = std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust")) + .expect("read rust"); + let rc: String = rust.split_whitespace().collect(); + assert!( + rc.contains("letmode=::std::boxed::Box::new(if"), + "the rebuilt `Option` must be re-wrapped for the spelling:\n{rust}" + ); + // The control on the Rust side too: exactly ONE of the two externs wraps, + // so the assertion above is about the spelling and not an unconditional + // `Box` the emitter adds to everything. + assert_eq!( + rc.matches("::std::boxed::Box::new(if").count(), + 1, + "only the wrapped spelling gets a `Box::new`; the bare twin builds the \ + `Option` and passes it as is:\n{rust}" ); - // What it takes instead: the ordinary boxed-`Int?` optional wire, whose - // converter is selected by `selector.rs` — the path that carries its own - // spelling guards. - assert!(kc.contains("zBoxed(mode:Int?"), "{kotlin}"); } /// The transparent-wrapper guard runs **before** the model's layers are @@ -1826,7 +1841,7 @@ fn a_transparently_wrapped_option_does_not_take_the_present_value_pair() { /// outer wrapper is never seen: the Vec-build plan is selected, its emitter /// hands the source fn a `&[Foo]` built from the transient Rust-side `Vec`, and /// the parameter still spells `Box<&Vec>`. That is the same `E0308` class -/// [`a_transparently_wrapped_option_does_not_take_the_present_value_pair`] +/// [`a_transparently_wrapped_option_takes_the_present_value_pair_and_is_rebuilt`] /// covers, reached by peeling in the wrong order. /// /// So this pins the **ordering**, which the shape-by-shape tests cannot: every diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index eeb338bc..0289b803 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -703,6 +703,22 @@ const WRAPPER_OPS: &[WrapperOps] = &[ // implied by anything the model knows about the payload. Refused until // something needs it; that is one row, not a redesign. read: None, + // Building is a DIFFERENT question, and it is refused for a different + // reason — the two `None`s here are not one fact repeated. + // + // `Cow::Owned(v)` is well-typed: an input rebuild owns its value, and + // `Cow<'_, [T]>` takes a `Vec` while `Cow<'_, str>` takes a + // `String`. So this is not "cannot", it is **should not**. A source + // spells `Cow` to accept borrowed data without copying; a binding that + // can only ever hand it `Owned` pays that copy on every call and + // silently removes the borrow path — and the callee can see the + // difference (`matches!(c, Cow::Borrowed(_))`), so it is observable + // rather than merely wasteful. + // + // **Deliberate, not deferred.** If a binding decides the copy is + // acceptable for its own source, this is one line — + // `Some(|e| quote!(::std::borrow::Cow::Owned(#e)))` — and nothing else + // moves. The refusal is here so that decision is made on purpose. build: None, }, ]; @@ -787,6 +803,57 @@ pub(crate) fn read_through_erased_wrappers( Some(out) } +/// Put back the transparent wrappers a **rebuild** dropped, so a value the +/// emitter constructed from the classification has the type the source spelled +/// — `Box>` ← `Box::new(v)`, an unwrapped spelling ← `v` unchanged. +/// +/// The input-side dual of [`read_through_erased_wrappers`], and the reason both +/// live here rather than at the sites that need them: the specialized input +/// lowerings do not *decode* their parameter, they **rebuild** it — a literal +/// `S { .. }`, an `Option::Some(v)`, a `Vec` pushed element by element — and +/// a rebuild from the classification alone produces the *stripped* type. Handing +/// that to a parameter spelled `Box<..>` is an `E0308` in the generated crate, +/// which is why this is one rule in one place instead of three selection sites +/// each remembering it. +/// +/// Applied **innermost-out**, the reverse of reading: the value in hand is the +/// canonical shape, and each layer wraps what the previous one produced. +/// +/// `None` when any layer has no [`WrapperOps::build`] — `Cow`, by policy rather +/// than by impossibility; see its row. A caller that gets `None` has a crossing +/// it cannot serve and must decline or report it, never emit the bare value. +/// +/// **This answers for one layer's spelling.** It restores the wrappers standing +/// over `ty`'s own classification; a wrapper *inside* — the `Box` of +/// `Option>` — belongs to the inner reading, is applied when that layer +/// is built, and is invisible here. An erasure sits **outside** the layer it +/// wraps, so a rebuild collects wrappers as it descends and applies them as it +/// comes back out. +pub(crate) fn build_through_erased_wrappers( + ty: &crate::api::core::flat::TypeRef, + value: TokenStream, +) -> Option { + build_through_wrappers(&ty.erased_wrappers(), value) +} + +/// [`build_through_erased_wrappers`] over a wrapper list already taken off a +/// reading — for a plan that recorded *what to put back* rather than keeping the +/// whole `TypeRef` to ask again. +/// +/// The list is the only part of the reading a rebuild uses, and it is two +/// pointers instead of a `TypeRef`'s ~264 bytes. That matters because these +/// plans live in `InputKind`, whose size every variant pays. +pub(crate) fn build_through_wrappers( + names: &[&'static str], + value: TokenStream, +) -> Option { + let mut out = value; + for name in names.iter().rev() { + out = (wrapper_ops(name)?.build?)(out); + } + Some(out) +} + /// Whether the source wrote the canonical spelling itself — no wrapper to undo. /// /// Required by the converters that do **not** produce the spelled type by @@ -2046,6 +2113,79 @@ impl Declarations { /// **Input** wrapper shape (`pat` = the reconstructed canonical pattern, /// `t1` = its captured inner): the built-in `&`/`Option<&>`/`Vec`/`Option` /// handlers. + /// **Last resort**: a spelling whose only difference from something this + /// adapter can already convert is the transparent wrappers over it. + /// + /// The layer arms each handle one *classification* layer — `Optional`, + /// `Sequence`, `Ref` — and bridge a wrapper as part of doing so. What none of + /// them covers is a wrapper over a **terminal**: `Box` classifies as + /// `Named`, so no layer arm claims it, and `input_terminal` keys on the whole + /// spelling and finds no `Payload` config under `Box < Payload >`. Before + /// this it resolved to nothing at all — the crossing was refused for a + /// wrapper the model exists to make invisible. + /// + /// So this delegates to the **stripped** spelling's own converter and puts + /// the wrappers back on what it produced. The inner type is declared as a + /// `sub`, exactly as a layer arm declares its inner, so it is required and + /// resolved through the ordinary machinery rather than being resolved here. + /// + /// Deliberately tried **after** every layer arm, so nothing that resolves + /// today changes route: `Box>` keeps the `Optional` arm (which + /// bridges via `build_from_canonical`), and only the shapes that previously + /// reached `None` arrive here. + pub(crate) fn input_transparent_bridge( + &self, + reading: &crate::api::core::flat::TypeRef, + registry: &impl Conversions, + ) -> Option> { + if reading.erased_wrappers().is_empty() { + return None; + } + let produced = reading.syntax(); + // The spelling under every wrapper — by the model's own definition, the + // one whose lowering yields this `kind`. + let stripped = reading.stripped_syntax(); + // A wrapper over a **borrow** is not bridgeable here, and the reason is + // the converter's own shape rather than the wrapper's: this produces an + // owned value, and there is nothing for a `Box<&T>` to borrow *from* — + // the returned reference would have to outlive the call that made it + // (`E0106` on the generated signature). The borrow arms own that case, + // and they serve the canonical spelling only. + // + // Asked of the MODEL, not of `stripped`: an erasure is transparent, so + // `Box<&T>` already classifies as `Ref` and `kind` answers this without + // anything here matching a `syn` variant. + if matches!(reading.kind(), crate::api::core::flat::TypeKind::Ref { .. }) { + return None; + } + // It has to be a type this binding already crosses; if it is not, the + // ordinary "unresolved" diagnostic names it, which is the better error. + let inner = registry.reading_of(&stripped)?; + let entry = registry.input_entry(&inner)?; + let wire = entry.destination.clone(); + let inner_fn = &entry.function.sig.ident; + // Wrap what the inner converter produced. `None` here is `Cow`'s policy + // refusal — the crossing then stays unresolved and names the type, + // rather than resolving and emitting Rust the consumer cannot build. + let built = build_through_erased_wrappers(reading, quote!(__inner))?; + let body: syn::Expr = syn::parse_quote!({ + let __inner = #inner_fn(env, v)?; + #built + }); + Some(ConverterImpl { + subs: vec![stripped], + pre_stages: vec![], + function: self.build_input_fn(produced, &wire, &body, None), + destination: wire, + niches: entry.niches.clone(), + // The surface is the inner type's: a wrapper is invisible to the + // destination language, which is the whole reason the model erases + // it. Inheriting rather than recomputing also keeps a projection's + // Kotlin class from being lost behind the wrapper. + metadata: entry.metadata.clone(), + }) + } + pub(crate) fn input_wrapper_shape( &self, shape: WrapperShape, @@ -2696,4 +2836,61 @@ mod wrapper_ops_tests { "`WRAPPER_OPS` rows for non-erased {stray:?}" ); } + + /// A rebuild puts the wrappers back **innermost-out**, the reverse of the + /// order a read takes them off. + /// + /// Asserted on a `Box>` rather than a single layer, because a single + /// layer cannot tell the two orders apart — which is exactly how a + /// composition bug survives. And asserted against `read` on the same type, + /// so the two are pinned as duals rather than as two independent claims. + #[test] + fn a_rebuild_puts_the_wrappers_back_inside_out() { + let ty = crate::api::test_util::reading(syn::parse_quote!(Box>>)); + assert_eq!(ty.erased_wrappers(), ["Box", "Box"]); + + let built = build_through_erased_wrappers(&ty, quote!(v)).expect("Box builds"); + assert_eq!( + built.to_string().replace(' ', ""), + ":: std :: boxed :: Box :: new (:: std :: boxed :: Box :: new (v))".replace(' ', ""), + ); + // The dual, on the same type: reading takes them off outermost-first. + let read = read_through_erased_wrappers(&ty, quote!(v)).expect("Box reads"); + assert_eq!(read.to_string().replace(' ', ""), "**v"); + + // The control: nothing erased, so both are the identity and neither + // test above can be passing on an unconditional wrap. + let plain = crate::api::test_util::reading(syn::parse_quote!(Option)); + assert!(plain.erased_wrappers().is_empty()); + for e in [ + build_through_erased_wrappers(&plain, quote!(v)), + read_through_erased_wrappers(&plain, quote!(v)), + ] { + assert_eq!(e.expect("identity").to_string(), "v"); + } + } + + /// `Cow` declines a rebuild, and the two directions decline for **different + /// reasons** — which is why the row carries two `None`s rather than one + /// capability flag. + /// + /// Reading is impossible (`E0507`: a `Cow` payload cannot be moved through + /// `Deref`). Building is *possible* — `Cow::Owned(v)` is well-typed for an + /// owned payload — and refused on purpose, because a binding that can only + /// ever hand a `Cow` parameter `Owned` pays a copy per call and removes the + /// borrow path the source asked for. If that policy is ever revisited, this + /// test is the thing that has to change with it. + #[test] + fn a_cow_declines_a_rebuild_by_policy() { + let ty = crate::api::test_util::reading(syn::parse_quote!(Cow<'_, str>)); + assert_eq!(ty.erased_wrappers(), ["Cow"]); + assert!(build_through_erased_wrappers(&ty, quote!(v)).is_none()); + assert!(read_through_erased_wrappers(&ty, quote!(v)).is_none()); + + // A `Cow` under a `Box` declines too: one unbuildable layer refuses the + // whole chain, rather than the `Box` half quietly succeeding. + let nested = crate::api::test_util::reading(syn::parse_quote!(Box>)); + assert_eq!(nested.erased_wrappers(), ["Box", "Cow"]); + assert!(build_through_erased_wrappers(&nested, quote!(v)).is_none()); + } } diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index 3c52b5fd..3e9c23b9 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -114,10 +114,11 @@ mod spelling_census { /// `(file, call count)` — every `.rs` under `api/lang/jnigen`, checked /// against the directory tree so a new module cannot sit outside the census. const CENSUS: &[(&str, usize)] = &[ - // The L4 "layer questions" remainder — #229. Not migrated here because - // it is a separate consumer and bundling it would make one review of - // both impossible. - ("jni/emit/flat_input.rs", 18), + // 18 -> 9 (#289): `build_flat_struct_node` takes `flat::Struct` and + // peels its fields off the model. What remains is `struct_input_body` / + // `sum_input_body`, the `.jobject_input()` decoders — a separate walk + // over `syn::Fields`, and the rest of #289. + ("jni/emit/flat_input.rs", 9), ("jni/emit/struct_out.rs", 2), ("jni/emit/wrapper.rs", 2), // diff --git a/prebindgen/src/api/test_util.rs b/prebindgen/src/api/test_util.rs index ddb79062..054c1b60 100644 --- a/prebindgen/src/api/test_util.rs +++ b/prebindgen/src/api/test_util.rs @@ -32,6 +32,35 @@ pub(crate) fn scanned_with(sources: &[&str]) -> Registry { reg_with(sources).scanned().expect("scan") } +/// One type as the **model** reads it, for a test that needs a `TypeRef` and has +/// only a spelling. +/// +/// Minting is sealed to `api::core` (#280), and rightly — a hand-assembled +/// reading could pair a `kind` with a disagreeing `syntax`, which is the one +/// thing holding a `TypeRef` is supposed to rule out. So this does not reach +/// around the seal: it puts the spelling in a parameter position, runs the real +/// parse, and hands back the reading the model produced. A spelling the grammar +/// refuses panics here rather than yielding something weaker. +pub(crate) fn reading(ty: syn::Type) -> crate::core::flat::TypeRef { + let item: syn::Item = syn::parse_quote!( + pub fn __probe(v: #ty) { + unimplemented!() + } + ); + let flat = crate::core::Flat::builder() + .items(declare_referenced(vec![( + item, + crate::SourceLocation::default(), + )])) + .build() + .expect("the probe parses"); + flat.function("__probe") + .expect("the probe is indexed") + .params[0] + .ty + .clone() +} + /// Build a `Registry` from an item stream, the way `Registry::from_items` used /// to before reading captured output became `FlatBuilder`'s job alone. /// From 59d83b89a5b7d04e6e8440834ea3a1af67d69b63 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 22:53:30 +0200 Subject: [PATCH 39/52] cbindgen: one kind presents one C type, however it is spelled (#295) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #292 item 2, with the rule restated — as written it was wrong. "same `kind` ⇒ same wire" is false. The wire is the generator's to choose, and prebindgen varies it deliberately: jnigen crosses `&[Payload]` as a jlong Vec handle and `Vec>` as a `JObject`, both surfacing as `List`. The wrapper absorbs the difference and a caller cannot tell. What a caller CAN tell, and what the erasure promises will not happen, is the destination-language **type** changing because the source spelled a `Box`: Same `kind` ⇒ same destination-language type. The wire is free. It scopes to CONVERTED positions. A `repr_c_struct` is a layout mirror, reinterpreted from the source struct's bytes, so its field types are a layout fact — `Box` (a pointer) really is a different C type from `T` (inline) and the spelling is load-bearing by construction. That is the one place the usual split inverts, and it is why #230's headline example (`Payload.label`) is not a defect. Reusing a mirror's spelling test in a converted position is how the rule breaks. A tagged-union payload is converted, and took its opaque-pointer arm from the `Box` in the spelling: `Option>` crossed as `handle_t *` while `Option` — the same optional handle to every destination — was REFUSED, its structural output marker (`()`) unable to pass the converter-agreement check. An erased wrapper decided expressibility, the same defect shape #292 found on the jnigen side. The arm asks the declaration now, off the model. All three spellings — `Option>`, `Option`, `Handle` — present `*mut handle_t`, and their converter bodies differ: the boxed one hands over the box it has, the others are boxed by the converter. The C type follows `kind`; the conversion follows the syntax. Pure addition: `mirror_field_wire` is still consulted first, previously-refused shapes now resolve, and the regen is byte-identical against the merge base. Refs #292, #230. --- docs/language-integration.md | 40 +++++++++ prebindgen/src/api/lang/cbindgen/emit.rs | 52 +++++++++++ .../api/lang/cbindgen/tests/tagged_unions.rs | 88 +++++++++++++++++++ .../src/api/lang/cbindgen/trait_impl.rs | 45 ++++++++++ 4 files changed, 225 insertions(+) diff --git a/docs/language-integration.md b/docs/language-integration.md index c808bd89..2f585c94 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -77,6 +77,46 @@ classification stays small and genuinely neutral: > `Origin`'s syntax into `quote!` is spelling, and spelling the source is exactly > what generated Rust must do. +#### What the split does *not* say: who decides the destination type + +"Classify off `kind`, spell off `syntax`" tells an adapter where to get each +fact. It is silent on the question adapters actually face, which is what the +**destination language** ends up seeing. The companion rule: + +> **Same `kind` ⇒ same destination-language type.** The *wire* is the +> generator's to choose, and may differ per spelling. + +The weaker-sounding half is the important one. It is tempting to write "same +`kind` ⇒ same wire", and that is **false** — prebindgen violates it deliberately: + +| Rust | `kind` | Kotlin type | wire | +|---|---|---|---| +| `&[Payload]` | `Sequence` | `List` | `Long` — a jlong handle to a Rust-side `Vec` | +| `Vec>` | `Sequence` | `List` | `List` — a `JObject` | + +Two wires, one surface. Choosing a wire is exactly the generator's job, and the +destination-language wrapper absorbs the difference; a caller cannot tell. What +a caller *can* tell — and what the model's erasure promises will not happen — is +the **type** changing because the source spelled a `Box`. + +The rule scopes to **converted** positions, which is where a converter stands +between the Rust value and the destination and is free to bridge. It cannot apply +to a **layout mirror**: `Cbindgen`'s `repr_c_struct` crosses a struct zero-copy, +so the C struct is reinterpreted from the source struct's bytes and its field +types are a *layout* fact. There `Box` (a pointer) genuinely is a different C +type from `T` (inline), the spelling is load-bearing by construction, and no +erasure can apply. A mirror reads `syntax` for the destination type on purpose — +the one place the usual split inverts, and it inverts because the contract is +layout rather than surface. + +Reusing a mirror's spelling test in a converted position is how the rule gets +broken. A tagged-union payload is converted, and it used to take its +opaque-pointer arm from the `Box` in the spelling: `Option>` crossed +as `handle_t *` while `Option` — the same optional handle to every +destination — was **refused outright**. An erased wrapper decided whether the +shape was expressible. It asks the declaration now, and the two spellings share +one C type with different converter bodies. + This is mechanically measured, and needed no new mechanism: `core::flat::boundary` (ported from [#224](https://github.com/milyin/prebindgen/pull/224)) counts *variant mentions* diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index caa5281a..18aa0294 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -70,6 +70,30 @@ impl CbindgenBuilder { Some(item.variants.iter().cloned().collect()) } + /// The declared `opaque_ptr` under a union payload's spelling, when there is + /// one and the spelling does **not** already carry a `Box` — + /// `Option` / `Handle` → `Some(Handle)`, `Option>` → + /// `None` (that shape keeps its own arm, so its emitted Rust does not move). + /// + /// Keyed on `stripped_key`, because a **declaration** is about the type: a + /// wrapper the model erases cannot change which declaration a payload + /// matches. See [`Self::payload_field_wire`] for why a union payload asks + /// this at all where a `repr_c_struct` mirror must not. + pub(super) fn declared_opaque_payload_inner( + &self, + fty: &syn::Type, + registry: &impl Conversions<()>, + ) -> Option { + if opaque_ptr_payload_inner(fty).is_some() { + return None; + } + let reading = registry.reading_of(fty)?; + let core = reading.optional_inner().unwrap_or(&reading); + self.opaque + .contains_key(&core.stripped_key()) + .then(|| core.stripped_syntax()) + } + /// Wire type of one **tagged-union payload field**: the /// [`Self::mirror_field_wire`] policy (scalar / declared `enum_type` / /// opaque pointer `Option>` / `Box`) extended with `String` → @@ -130,6 +154,34 @@ impl CbindgenBuilder { if let Some(w) = self.mirror_field_wire(fty) { return Ok(w); } + // The opaque-pointer arm again, keyed on the **declaration** instead of + // on the spelling — which is what a *converted* position must do. + // + // `mirror_field_wire` above answers for a `repr_c_struct`, where the C + // type is a **layout** fact: the mirror is reinterpreted from the source + // struct's bytes, so `Box` (a pointer) and `T` (inline) genuinely are + // different C types and the spelling is load-bearing. A union payload is + // not mirrored — it is rebuilt arm by arm through real conversions — so + // that reasoning does not carry over, and reusing the same + // `Box`-in-the-spelling test made an erased wrapper decide what C sees. + // + // Concretely: `Option>` crossed as `*mut handle_t` while + // `Option` — the same optional handle to every destination + // language — was REFUSED, because it fell through to the + // converter-destination rule below where its output side is a structural + // marker (`()`) that cannot agree with the input's pointer. A wrapper the + // model erases decided whether the shape was expressible at all. + // + // So: peel the optional off the model and ask whether what is under it is + // a declared `opaque_ptr`. `stripped_key` rather than `key`, because a + // declaration is about the TYPE — see the same rule on the jnigen side + // (#292). The two spellings now share this C type; their converter bodies + // differ, which is exactly the split (`kind` decides what C sees, syntax + // decides how the value is built). + if let Some(inner) = self.declared_opaque_payload_inner(fty, registry) { + let c = self.c_type_ident(&inner); + return Ok(syn::parse_quote!(*mut #c)); + } // Otherwise the payload's wire is its **resolved converter // destination** — the same source a `data_struct` field effectively // uses. A union is rebuilt arm by arm through real per-field diff --git a/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs b/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs index 80e7bab5..b87e4fc7 100644 --- a/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs +++ b/prebindgen/src/api/lang/cbindgen/tests/tagged_unions.rs @@ -758,3 +758,91 @@ fn bool_payload_is_normalised_not_materialised() { // A bool owns nothing, so the union still gets no typed drop. assert!(!compact.contains("flagged_drop"), "{src}"); } + +/// **Same `kind` ⇒ same destination-language type.** Three spellings of one +/// optional handle present one C type. +/// +/// The rule the wire question actually obeys, and a correction to how #292 +/// stated it: "same `kind` ⇒ same wire" is false — a generator owns wire +/// selection and may legitimately vary it per spelling, absorbing the +/// difference in the destination-language wrapper (jnigen crosses `&[Payload]` +/// as a `Long` Vec handle and `Vec>` as a `JObject`, both surfacing +/// as `List`). What must not vary is **what the destination sees**. +/// +/// A union payload is the position where that bites in C, because it is +/// *converted* — rebuilt arm by arm — so nothing about the Rust layout is +/// forced on the C type. `Option>` took an opaque-pointer arm keyed +/// on the `Box` in the spelling, while `Option` fell through to the +/// converter-destination rule and was **refused outright** (its output side is a +/// structural marker whose `()` cannot agree with the input's pointer). So an +/// erased wrapper decided whether the shape was expressible at all — the same +/// defect shape as jnigen's declaration lookup in #292. +/// +/// The exemption this does *not* touch is [`CbindgenBuilder::repr_c_struct`]: a +/// zero-copy mirror is reinterpreted from the source struct's bytes, so its C +/// type is a **layout** fact and `Box` (a pointer) really is a different C +/// type from `T` (inline). There the spelling is load-bearing by construction. +#[test] +fn one_kind_presents_one_c_type_however_it_is_spelled() { + let loc = SourceLocation::default(); + let e: syn::ItemEnum = syn::parse_quote!( + pub enum Pick { + Boxed(Option>), + Bare(Option), + Owned(Handle), + } + ); + let h: syn::Item = syn::parse_quote!( + pub type Handle = __x::Handle; + ); + let make: syn::ItemFn = syn::parse_quote!( + pub fn pick_new() -> Pick { + unimplemented!() + } + ); + let take: syn::ItemFn = syn::parse_quote!( + pub fn pick_take(p: Pick) { + unimplemented!() + } + ); + let registry = crate::api::test_util::reg_from_items(declare_referenced([ + (syn::Item::Enum(e), loc.clone()), + (h, loc.clone()), + (syn::Item::Fn(make), loc.clone()), + (syn::Item::Fn(take), loc.clone()), + ])) + .expect("index items"); + let cbindgen = CbindgenBuilder::new() + .source_module(syn::parse_quote!(example_flat)) + .free_memory_function("example_free") + .mangle_type_name(|base| format!("{base}_t")) + .mangle_destructor(|base| format!("{base}_drop")) + .opaque_ptr(syn::parse_quote!(Handle)) + .tagged_union(syn::parse_quote!(Pick)) + .function(syn::parse_quote!(pick_new)) + .function(syn::parse_quote!(pick_take)) + .panic(); + let src = write(cbindgen, registry, "one_kind_one_c_type"); + let compact: String = src.split_whitespace().collect(); + + // The claim: one C type for all three. Asserted on the whole mirror rather + // than per arm, so a change that made them agree on the WRONG type by + // collapsing the arms would still have to say so here. + assert!( + compact + .contains("pubenumpick_t{Boxed(*muthandle_t),Bare(*muthandle_t),Owned(*muthandle_t),}"), + "three spellings of one optional handle, one C type:\n{src}" + ); + + // …and the converter BODIES differ, which is the other half of the split: + // the C type follows `kind`, the conversion follows the syntax. The `Box` + // spelling hands its box over; the bare ones are boxed here. + assert!( + compact.contains("::std::boxed::Box::into_raw(__b)as*muthandle_t"), + "the `Box` spelling hands over the box it already has:\n{src}" + ); + assert!( + compact.contains("::std::boxed::Box::into_raw(::std::boxed::Box::new(__v))as*muthandle_t"), + "a bare optional handle is boxed by the converter:\n{src}" + ); +} diff --git a/prebindgen/src/api/lang/cbindgen/trait_impl.rs b/prebindgen/src/api/lang/cbindgen/trait_impl.rs index 1b35c0ea..c3ba852a 100644 --- a/prebindgen/src/api/lang/cbindgen/trait_impl.rs +++ b/prebindgen/src/api/lang/cbindgen/trait_impl.rs @@ -1324,6 +1324,36 @@ impl CbindgenBuilder { let conv = Self::in_name(fty); return quote!(#conv(#b)?); } + // The same opaque-pointer arm the wire took, for a spelling with no + // `Box` in it: the C caller still hands over a `*mut handle_t` it gave + // up ownership of, so the pointer is reclaimed the same way — the value + // is just moved out of the box instead of kept in one. Conversion + // follows the SYNTAX; the C type followed `kind` + the declaration. + if let Some(inner) = self.declared_opaque_payload_inner(fty, registry) { + let src_inner = self.src_ty(&inner); + let owned = quote!(*::std::boxed::Box::from_raw(#b as *mut #src_inner)); + let null_msg = format!( + "null payload for `{}` (a non-optional handle payload cannot be NULL — the \ + union may already have been dropped)", + type_short(&inner) + ); + return if is_option(fty) { + quote!(if #b.is_null() { + ::core::option::Option::None + } else { + ::core::option::Option::Some(#owned) + }) + } else { + quote!({ + if #b.is_null() { + return ::core::result::Result::Err( + ::std::string::String::from(#null_msg), + ); + } + #owned + }) + }; + } if let Some(inner) = opaque_ptr_payload_inner(fty) { let src_inner = self.src_ty(&inner); let boxed = quote!(::std::boxed::Box::from_raw(#b as *mut #src_inner)); @@ -1398,6 +1428,21 @@ impl CbindgenBuilder { let conv = Self::out_name(fty); return quote!(::core::mem::MaybeUninit::new(#conv(#b))); } + // The peer of the input arm above: an owned value the C side must later + // release, so it is boxed HERE rather than having arrived boxed. + if let Some(inner) = self.declared_opaque_payload_inner(fty, registry) { + let c = self.c_type_ident(&inner); + return if is_option(fty) { + quote!(match #b { + ::core::option::Option::Some(__v) => { + ::std::boxed::Box::into_raw(::std::boxed::Box::new(__v)) as *mut #c + } + ::core::option::Option::None => ::core::ptr::null_mut(), + }) + } else { + quote!(::std::boxed::Box::into_raw(::std::boxed::Box::new(#b)) as *mut #c) + }; + } if let Some(inner) = opaque_ptr_payload_inner(fty) { let c = self.c_type_ident(&inner); return if is_option(fty) { From 53d4f1751a70f4d6938209eea2295d50be2b93b7 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Sun, 2 Aug 2026 22:55:22 +0200 Subject: [PATCH 40/52] jnigen: the wrapped-Vec-element refusal is reserved, not definitive (#297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #294 called it definitive on the grounds that the only alternative was spelling a Rust wrapper into a JNI symbol. That is a false dichotomy: keying the helper trio on the CANONICAL element gives one trio per Kotlin class, with the element's wrapper applied where the Vec is consumed. #296 has the sketch. The cost of leaving it is not correctness but a silent downgrade — a `Box` the model erases turns raw scalar leaves into a per-element JObject plus a field read per field. Refs #296. --- .../src/api/lang/jnigen/jni/emit/vec_build.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs index 0d2447f6..9c26a7be 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs @@ -84,11 +84,18 @@ pub(crate) fn vec_build_elem( // **Kotlin class** (`Payload` → `payloadVec`), which the two share, so both // would emit `payloadVecNew`/`Push`/`Free` and collide. // - // Definitive as long as the name comes from the Kotlin class: the - // alternative is spelling a Rust wrapper into a JNI symbol, which is the - // representation leak this whole layer exists to prevent. The general - // converter serves the shape correctly (see `input_transparent_bridge`), - // just without the per-element push loop. + // **Reserved, not definitive** (#296). The collision is real; the choice it + // seems to force is not. Keying the trio on the CANONICAL element gives one + // trio per Kotlin class — storage `Vec` — with the element's + // wrapper applied where the Vec is consumed + // (`.into_iter().map(Box::new).collect()`), so no Rust wrapper reaches a JNI + // symbol and nothing collides. + // + // The cost of not doing it is not correctness: the general converter serves + // the shape (see `input_transparent_bridge`). It is that a `Box` the model + // erases silently downgrades the crossing from raw scalar leaves to a + // per-element `JObject` plus a field read per field — which is exactly what + // this path exists to remove. if !elem.erased_wrappers().is_empty() { return None; } From 8cb1a5ea59d47ef30e4448f3f19a3acee6a10154 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 04:55:04 +0200 Subject: [PATCH 41/52] core: the edge walk takes the key it was re-deriving anyway (#291 stage A) (#298) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `immediate_edges` asked for a `&syn::Type` and opened by re-keying it — twice, once for the structural children and once for the declared fields. So `resolve.rs`, `order.rs` and `register_type_inner` each spelled a key into tokens purely so the callee could undo that: a normalize pass and a token render per call, to arrive back where it started. The most common use of `TypeKey::to_type()` was undoing itself. It takes a `&TypeKey` now. A table lookup takes an identity. The same round trip, one layer out, ran at every `key.to_type()` fed straight to `reading_of` — which is `reading(&TypeKey::from_type(..))`. Those become `reading(key)`, the route #284 already moved jnigen's `convert_crossing` to, and any spelling they still need comes off the reading rather than off the key. Two sites keep a spelling and say why: `order.rs`'s `plan_edges` needs real tokens for `extract_fn_trait_args`, and cbindgen's selector chain still takes `&syn::Type` — both now read them from the cell the registry already holds, so nothing is re-derived from a key. `to_type()` is not removed here and `TypeKey` is unchanged: this proves the `reading(key)` route before anything depends on it. 44 call sites to 31, all of it deletion. Verified: 631 lib tests, `cargo test --all --all-features`, clippy on 1.85.0 and stable, and regen-check byte-identical after a forced rebuild plus covertest-kotlin's 49-section JVM harness. --- prebindgen/src/api/core/registry/order.rs | 14 +++- prebindgen/src/api/core/registry/scan.rs | 24 +++--- prebindgen/src/api/core/registry/tests.rs | 14 ++-- prebindgen/src/api/core/resolve.rs | 3 +- prebindgen/src/api/lang/cbindgen/emit.rs | 20 ++--- .../src/api/lang/cbindgen/trait_impl.rs | 80 ++++++++----------- .../src/api/lang/jnigen/jni/trait_impl.rs | 19 ++--- 7 files changed, 85 insertions(+), 89 deletions(-) diff --git a/prebindgen/src/api/core/registry/order.rs b/prebindgen/src/api/core/registry/order.rs index 992a3a6d..b9d5e171 100644 --- a/prebindgen/src/api/core/registry/order.rs +++ b/prebindgen/src/api/core/registry/order.rs @@ -60,15 +60,23 @@ impl Registry { return; } let (dir, key) = node.clone(); - let ty = key.to_type(); + // Every node this walk reaches has a cell: the roots are the table's own + // keys, and each edge below is filtered by `contains_key`. So the + // spelling `plan_edges` needs is the reading the registry already + // stored — not one re-derived from the key (#291). + let plan_edges = self + .type_table(dir) + .get(&key) + .map(|cell| self.plan_edges(dir, cell.subject.syntax())) + .unwrap_or_default(); let mut edges: Vec = self - .immediate_edges(dir, &ty) + .immediate_edges(dir, &key) .into_iter() // The structural edges arrive as readings, so the key is the // model's own answer rather than one re-derived from a spelling. .map(|(d, t)| (d, t.key())) .chain( - self.plan_edges(dir, &ty) + plan_edges .into_iter() .map(|(d, t)| (d, TypeKey::from_type(&t))), ) diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index 4f3e8021..37630a6b 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -233,13 +233,13 @@ impl Registry { visited: &mut HashSet, ) { let key = reading.key(); - if !visited.insert(key) { + if !visited.insert(key.clone()) { return; // cycle guard } self.ensure_entry(dir, reading, is_top); - for (child_dir, sub) in self.immediate_edges(dir, reading.syntax()) { + for (child_dir, sub) in self.immediate_edges(dir, &key) { self.register_type_inner(child_dir, &sub, false, visited); } } @@ -363,8 +363,8 @@ impl Registry { Ok(()) } - /// Enumerate the immediate type-graph edges out of `(dir, ty)`: the model's - /// own children of this type, plus — if `ty` names a declared struct or sum — + /// Enumerate the immediate type-graph edges out of `(dir, key)`: the model's + /// own children of this type, plus — if `key` names a declared struct or sum — /// the field types of that item. /// /// A callback's argument types flow with `dir.flip()`, because an argument the @@ -372,6 +372,12 @@ impl Registry { /// inherits `dir`. Used by both `register_type_inner` (during scan) and the /// unresolved-descendants BFS in `resolve` (for diagnostics). /// + /// **Takes the key, because a key is all it ever used.** This asked for a + /// `&syn::Type` and opened by re-keying it, so every caller spelled a key into + /// tokens purely so this could undo that — a normalize pass and a token render + /// per call, to arrive back where it started. What the walk needs is a table + /// lookup, and a table lookup takes an identity (#291). + /// /// The children come from [`TypeKind`], not from taking the syntax apart, and /// the difference is load-bearing rather than cosmetic. `&mut MaybeUninit` /// is `Ref { mode: Out, inner: T }` — the model absorbed the `MaybeUninit`, so @@ -388,16 +394,12 @@ impl Registry { pub(crate) fn immediate_edges( &self, dir: Direction, - ty: &syn::Type, + key: &TypeKey, ) -> Vec<(Direction, crate::api::core::flat::TypeRef)> { use crate::api::core::flat::TypeKind; let mut out: Vec<(Direction, crate::api::core::flat::TypeRef)> = Vec::new(); - if let Some(reading) = self - .type_table(dir) - .get(&TypeKey::from_type(ty)) - .map(|c| &c.subject) - { + if let Some(reading) = self.type_table(dir).get(key).map(|c| &c.subject) { let (children, child_dir): (Vec<&crate::api::core::flat::TypeRef>, Direction) = match reading.kind() { TypeKind::Optional(t) @@ -436,7 +438,7 @@ impl Registry { // have answered `None` and dead-ended the walk. if let Some(name) = self .type_table(dir) - .get(&TypeKey::from_type(ty)) + .get(key) .and_then(|c| match c.subject.kind() { TypeKind::Named { id } => Some(id.name.clone()), _ => None, diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index 4ad9db6f..bc04da62 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -42,7 +42,9 @@ impl DeclareAndResolve<()> for RegistryBuilder<()> { .stub() .declare_into_any(self)? .validate_with(&ext)? - .convert_with(|crossing, _built| ext.converter(&crossing.1.to_type()))? + // The reading, not a spelling re-derived from the key: this is the + // route a real generator takes, so the stub takes it too (#291). + .convert_with(|crossing, built| ext.converter(built.reading(&crossing.1)?.syntax()))? .build()?; ext.validate_resolved(®istry) .map_err(|message| ScanError::AdapterInvariant { message })?; @@ -1721,19 +1723,19 @@ fn a_recursive_type_is_handed_out_once_and_terminates() { // fixture property this test needs. let node = TypeKey::parse("Node").expect("test type"); let mut seen_keys: Set = Set::new(); - let mut frontier = vec![node.to_type()]; + let mut frontier = vec![node]; let mut revisited = false; for _ in 0..8 { let mut next = Vec::new(); - for t in frontier { - if !seen_keys.insert(TypeKey::from_type(&t)) { + for k in frontier { + if !seen_keys.insert(k.clone()) { revisited = true; break; } next.extend( - reg.immediate_edges(Direction::Output, &t) + reg.immediate_edges(Direction::Output, &k) .into_iter() - .map(|(_, sub)| sub.syntax().clone()), + .map(|(_, sub)| sub.key()), ); } if revisited { diff --git a/prebindgen/src/api/core/resolve.rs b/prebindgen/src/api/core/resolve.rs index a5c1b894..f3af9448 100644 --- a/prebindgen/src/api/core/resolve.rs +++ b/prebindgen/src/api/core/resolve.rs @@ -130,8 +130,7 @@ fn collect_unresolved_descendants( key: &TypeKey, queue: &mut VecDeque<(Direction, TypeKey)>, seen: &mut std::collections::HashSet<(Direction, TypeKey)>| { - let ty = key.to_type(); - for (child_dir, sub) in registry.immediate_edges(dir, &ty) { + for (child_dir, sub) in registry.immediate_edges(dir, key) { let dep = (child_dir, sub.key()); if seen.insert(dep.clone()) { queue.push_back(dep); diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index 18aa0294..7f29ccc8 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -29,11 +29,11 @@ impl CbindgenBuilder { // A tagged union with a `String` payload hands out a `char*` per active // arm — allocated by its output converter, released by its typed drop. if self.tagged_unions.keys().any(|key| { - let ty = key.to_type(); - registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_some() + let Some(reading) = registry.reading(key) else { + return false; + }; + let ty = reading.syntax().clone(); + registry.output_entry(&reading).is_some() && self .enum_variants(registry, &ty) .map(|vs| { @@ -46,11 +46,11 @@ impl CbindgenBuilder { return true; } self.data.keys().any(|key| { - let ty = key.to_type(); - registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_some() + let Some(reading) = registry.reading(key) else { + return false; + }; + let ty = reading.syntax().clone(); + registry.output_entry(&reading).is_some() && self .struct_fields(registry, &ty) .map(|fields| fields.iter().any(|(_, fty)| is_string(fty))) diff --git a/prebindgen/src/api/lang/cbindgen/trait_impl.rs b/prebindgen/src/api/lang/cbindgen/trait_impl.rs index c3ba852a..82830ce7 100644 --- a/prebindgen/src/api/lang/cbindgen/trait_impl.rs +++ b/prebindgen/src/api/lang/cbindgen/trait_impl.rs @@ -532,18 +532,16 @@ impl CbindgenBuilder { fn prereq_opaque_handles(&self, registry: &Registry<()>) -> Vec { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.opaque) { - let ty = key.to_type(); - if registry - .reading_of(&ty) - .and_then(|tr| registry.input_entry(&tr)) - .is_none() - && registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_none() + // Keyed directly: this used to spell the key into tokens purely so + // `reading_of` could re-key them, twice (#291). + let Some(reading) = registry.reading(key) else { + continue; + }; + if registry.input_entry(&reading).is_none() && registry.output_entry(&reading).is_none() { continue; } + let ty = reading.syntax().clone(); let c_struct = self.c_type_ident(&ty); // Opaque/incomplete C type: the handle is `#c_struct *`, which IS the // `Box::into_raw` pointer to the source value. @@ -575,18 +573,14 @@ impl CbindgenBuilder { fn prereq_data_structs(&self, registry: &Registry<()>) -> Vec { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.data) { - let ty = key.to_type(); - if registry - .reading_of(&ty) - .and_then(|tr| registry.input_entry(&tr)) - .is_none() - && registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_none() + let Some(reading) = registry.reading(key) else { + continue; + }; + if registry.input_entry(&reading).is_none() && registry.output_entry(&reading).is_none() { continue; } + let ty = reading.syntax().clone(); let Some(fields) = self.struct_fields(registry, &ty) else { continue; }; @@ -625,18 +619,14 @@ impl CbindgenBuilder { let mut vo: Vec<(&TypeKey, &ValueOpaqueCfg)> = self.value_opaque.iter().collect(); vo.sort_by(|a, b| a.0.as_str().cmp(b.0.as_str())); for (key, cfg) in vo { - let ty = key.to_type(); - if registry - .reading_of(&ty) - .and_then(|tr| registry.input_entry(&tr)) - .is_none() - && registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_none() + let Some(reading) = registry.reading(key) else { + continue; + }; + if registry.input_entry(&reading).is_none() && registry.output_entry(&reading).is_none() { continue; } + let ty = reading.syntax().clone(); let src = self.src_ty(&ty); let opaque = &cfg.opaque; // `repr_c_struct`: the opaque counterpart is an auto-generated @@ -830,18 +820,14 @@ impl CbindgenBuilder { fn prereq_enums(&self, registry: &Registry<()>) -> Vec { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.enums) { - let ty = key.to_type(); - if registry - .reading_of(&ty) - .and_then(|tr| registry.input_entry(&tr)) - .is_none() - && registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_none() + let Some(reading) = registry.reading(key) else { + continue; + }; + if registry.input_entry(&reading).is_none() && registry.output_entry(&reading).is_none() { continue; } + let ty = reading.syntax().clone(); let Some(e) = enum_item(registry, &ty) else { continue; }; @@ -880,18 +866,14 @@ impl CbindgenBuilder { fn prereq_tagged_unions(&self, registry: &Registry<()>) -> Vec { let mut items: Vec = Vec::new(); for (key, _cfg) in sorted_by_key(&self.tagged_unions) { - let ty = key.to_type(); - if registry - .reading_of(&ty) - .and_then(|tr| registry.input_entry(&tr)) - .is_none() - && registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_none() + let Some(reading) = registry.reading(key) else { + continue; + }; + if registry.input_entry(&reading).is_none() && registry.output_entry(&reading).is_none() { continue; } + let ty = reading.syntax().clone(); let Some(e) = enum_item(registry, &ty) else { continue; }; @@ -1620,7 +1602,13 @@ impl CbindgenBuilder { built: &Building<'_, ()>, ) -> Option> { let (dir, key) = crossing; - let ty = key.to_type(); + // The reading the scan already took for this crossing, fetched by the + // key the crossing IS — the same migration the jnigen twin made in #284, + // in place of `key -> to_type() -> spelling` (#291). Every crossing + // `convert_with` hands out comes from a type table, so it has a cell. + // The selectors still take the spelling: moving cbindgen's selector + // chain onto readings is its own change. + let ty = built.reading(key)?.syntax().clone(); match dir { Direction::Input => self.select_input_type(&ty, built).or_else(|| { let args = crate::api::core::flat::extract_fn_trait_args(&ty)?; diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 0289b803..78e33d1e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -570,19 +570,16 @@ pub(crate) fn build_handle_destructor_items( continue; } // Skip handles the (feature-aware) scan never references — their - // type may not be in scope in the generated module. - let ty = key.to_type(); - if registry - .reading_of(&ty) - .and_then(|tr| registry.input_entry(&tr)) - .is_none() - && registry - .reading_of(&ty) - .and_then(|tr| registry.output_entry(&tr)) - .is_none() - { + // type may not be in scope in the generated module. Keyed directly: + // this used to spell the key into tokens purely so `reading_of` could + // re-key them, twice (#291). + let Some(reading) = registry.reading(key) else { + continue; + }; + if registry.input_entry(&reading).is_none() && registry.output_entry(&reading).is_none() { continue; } + let ty = reading.syntax().clone(); let class_fqn = cfg .name_spec .as_ref() From a2cb4f89fbcb50620e7fc6710a445302f9045302 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 04:55:45 +0200 Subject: [PATCH 42/52] A declaration keeps the type it was written with (#291 stage B) (#299) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * A declaration keeps the type it was written with (#291 stage B) `ptr_class!(Foo)` receives a real `syn::Type`, reduces it to a key, and throws it away. Everything that later needed those tokens back — to `intern` the type, to spell `Into<#target>`, to say whether the build script path-qualified it — asked the KEY to reproduce them. That is backwards: the declaration is where the type came from. So declarations carry `Origin` now, the model's own convention for a node's tokens, at `SourceLocation::default()` — the sanctioned placeless location for something a build script authored rather than a captured file. `RegistryBuilder::export_type` takes the type instead of the key, like its sibling `cross` already did, and `Declared::types` / `Decompositions::replaces` carry the spelling beside the identity. That is what unblocks the two sites a key genuinely could not serve: the qualified-declared-types diagnostic needs multi-segment path STRUCTURE, and the declared-type scan needs real tokens for `intern` — for a type that is in no table yet, so `reading()` has nothing to answer with. Both canonicalize explicitly at the point of use, which is what the key was silently providing; the comments say so. The same applies to every declare-phase consumer. `build_expansions`, `build_deconstructors`, `convert_input_body`, `build_sum_decons` and `validate_split_declarations` run while only a `RegistryBuilder` exists, where `reading()` would legitimately answer `None` — swapping them to it would have been a silent semantic change, not a refactor. They read their own decl. Two sites go the other way, to `reading()`, because they are past the declare phase and the sibling arm beside each already did: `SpecKey:: WholeFolder` in `derive_iface_spec` (which is contractually a pure function of its key, so a side channel was not open to it), and cbindgen's callback structs, which now read the argument types their declaration recorded rather than rebuilding them from a `Vec`. 28 `to_type()` call sites to 9, and every one that remains is a name lookup or the idempotence test — stages C1 and D. Verified: 631 lib tests, `cargo test --all --all-features`, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, and covertest-kotlin's 49-section JVM harness. * Review: exporting a type twice keeps the first spelling `declared.types` was a `HashSet`, so a repeated `export_type` was first-wins on the identity. Turning it into a map made `insert` overwrite the stored spelling — last-wins, and only for the spelling, which is the one thing about the pair that is not fixed by construction. `register_class` already documents keeping the first for a reopened declarator; `export_type` says and does the same now. Also from review: the fixture types in `write/tests.rs` were still named `key_a`/`key_b` after becoming `syn::Type`s, and `declared_origin`'s intra-doc link pointed at `crate::core::RegistryBuilder`, which is not re-exported there. Verified: 631 lib tests, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, covertest-kotlin 49/49. --- prebindgen/src/api/core/registry/declare.rs | 22 ++++++- prebindgen/src/api/core/registry/mod.rs | 18 ++++- prebindgen/src/api/core/registry/run.rs | 2 +- prebindgen/src/api/core/registry/scan.rs | 39 ++++++++--- prebindgen/src/api/core/registry/tests.rs | 34 +++++----- prebindgen/src/api/core/resolve/tests.rs | 20 +++--- prebindgen/src/api/core/write.rs | 4 +- prebindgen/src/api/core/write/tests.rs | 12 ++-- prebindgen/src/api/lang/cbindgen/builder.rs | 14 ++-- prebindgen/src/api/lang/cbindgen/convert.rs | 6 +- prebindgen/src/api/lang/cbindgen/mod.rs | 47 ++++++++++++- .../src/api/lang/cbindgen/trait_impl.rs | 45 ++++++++----- prebindgen/src/api/lang/jnigen/jni/builder.rs | 66 ++++++++++++++----- prebindgen/src/api/lang/jnigen/jni/decl.rs | 45 +++++++++++++ prebindgen/src/api/lang/jnigen/jni/iface.rs | 16 +++-- prebindgen/src/api/lang/jnigen/jni/mod.rs | 16 ++++- .../src/api/lang/jnigen/jni/overloads.rs | 2 +- prebindgen/src/api/lang/jnigen/jni/report.rs | 2 +- .../src/api/lang/jnigen/jni/tests/mod.rs | 8 +-- .../src/api/lang/jnigen/jni/trait_impl.rs | 35 +++++++--- prebindgen/src/api/test_util.rs | 8 +++ 21 files changed, 346 insertions(+), 115 deletions(-) diff --git a/prebindgen/src/api/core/registry/declare.rs b/prebindgen/src/api/core/registry/declare.rs index 0534f365..01f16e8b 100644 --- a/prebindgen/src/api/core/registry/declare.rs +++ b/prebindgen/src/api/core/registry/declare.rs @@ -139,8 +139,26 @@ impl RegistryBuilder { /// A type this binding **exports**: it crosses in both directions, and its /// body — a struct's fields, an enum's payloads — is scanned too. - pub fn export_type(mut self, key: TypeKey) -> Self { - self.registry.declared.types.insert(key); + /// + /// Takes the **type the declaration was written with**, like its sibling + /// [`Self::cross`], and derives the key here. It used to take the key alone, + /// which meant the scan had to recover tokens *from* the key to intern the + /// type and to diagnose its spelling — reasoning backwards from an identity + /// to a thing that already existed. A build script wrote `ptr_class!(Foo)`; + /// this is that `Foo` (#291). + /// + /// Declaring the same type twice keeps the **first** spelling. That is what + /// the `HashSet` this replaced did with the identity, and what + /// `register_class` does with a reopened declarator: the two spellings agree + /// on identity by construction, so the tie-break only decides which + /// equivalent rendering the scan reads, and it should not depend on + /// declaration order. + pub fn export_type(mut self, ty: Origin) -> Self { + self.registry + .declared + .types + .entry(TypeKey::from_type(&ty.syntax)) + .or_insert(ty); self } diff --git a/prebindgen/src/api/core/registry/mod.rs b/prebindgen/src/api/core/registry/mod.rs index 663e12e0..7d5143f1 100644 --- a/prebindgen/src/api/core/registry/mod.rs +++ b/prebindgen/src/api/core/registry/mod.rs @@ -161,6 +161,7 @@ use std::collections::{HashMap, HashSet}; use crate::{ api::core::{ + flat::Origin, niches::Niches, prebindgen::{Prebindgen, Stage}, types_util::bare_path_ident, @@ -306,7 +307,16 @@ pub(crate) struct Declared { pub(crate) helper_functions: HashSet, pub(crate) accessors: HashSet, pub(crate) method_receivers: HashMap, - pub(crate) types: HashSet, + /// Exported types, each **with the spelling its declaration was written + /// with**. + /// + /// Keyed by identity, because that is what a declaration is looked up by — + /// and carrying the `syn::Type` anyway, because the scan needs real tokens + /// for these: to `intern` a type that is not yet in any table, and to say + /// whether the build script path-qualified it. Recovering those *from the + /// key* was the wrong direction — a build script wrote a `syn::Type`, and + /// the declaration simply discarded it (#291). + pub(crate) types: HashMap>, /// Consts to scan and emit, or `None` when the adapter has no const /// declaration mechanism — then every captured const is re-emitted /// verbatim (see the const gate in [`crate::api::core::write`]). @@ -358,7 +368,11 @@ pub struct Decompositions { /// crosses only in pieces *because* something decomposes it, and once the /// plans are applied its own direct converter is genuinely not needed — for /// a type with no destination representation, not even resolvable. - pub replaces: HashSet, + /// + /// Carries each declaration's own spelling for the same reason + /// [`Declared::types`] does — these are build-script-authored types the scan + /// diagnoses before anything has classified them. + pub replaces: HashMap>, } #[cfg(test)] diff --git a/prebindgen/src/api/core/registry/run.rs b/prebindgen/src/api/core/registry/run.rs index 64a6da8e..8612f70c 100644 --- a/prebindgen/src/api/core/registry/run.rs +++ b/prebindgen/src/api/core/registry/run.rs @@ -58,7 +58,7 @@ impl Registry { // unresolvable, since such a type has no destination representation. // Drop it both ways; the cell stays, so a converter is still produced // if one happens to resolve. - for key in &declared.decompositions.replaces { + for key in declared.decompositions.replaces.keys() { // The key is what a root flag is stored under, so it goes straight // in — no `to_type()` round trip to be re-keyed on the far side. self.clear_root(Direction::Input, key); diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index 37630a6b..ac920cb5 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -22,16 +22,23 @@ impl Registry { // shadows a captured item's name — the likely-mistake heuristic). // // The two syntax matches below **stay** as this file's boundary-ledger - // entries, and the reason is what they look at: `declared.types` are keys a + // entries, and the reason is what they look at: `declared.types` are types a // *build script author* wrote, and this is a diagnostic about the spelling // they wrote — is it path-qualified, and does its tail shadow a captured // item? No source type is being classified, so there is no element to read // instead; asking the model would answer about a type rather than about the // declaration. This is the "legitimately the adapter's business" case the // integration map (L2, #229) predicts, not a migration still owed. + // + // It reads the **declaration's own** spelling, canonicalized here. Both + // halves of that matter. Normalizing is what the paragraph above relies + // on — `crate::Foo` must not read as a qualified path — and it used to + // arrive for free because the tokens came out of the key, which is + // normalized by construction. Doing it explicitly costs one call and + // stops the key from being the source of tokens at all (#291). let mut qualified: Vec<(String, String)> = Vec::new(); let mut probed: HashSet<&TypeKey> = HashSet::new(); - for key in declared + for (key, declared_ty) in declared .types .iter() .chain(declared.decompositions.replaces.iter()) @@ -39,7 +46,7 @@ impl Registry { if !probed.insert(key) { continue; } - let ty = key.to_type(); + let ty = crate::api::core::flat::canonical_type(&declared_ty.syntax); // Peel one reference level; the qualified head only appears on // path types. let inner = match &ty { @@ -119,9 +126,15 @@ impl Registry { self.intern(*dir, ty, true)?; } - // Scan declared types. - for key in &declared.types { - let ty = key.to_type(); + // Scan declared types. The spelling is the declaration's own — `intern` + // needs real tokens for a type that is in no table yet, which is exactly + // the case a key cannot answer once it is only an identity (#291). + for declared_ty in declared.types.values() { + // Canonicalized for the same reason the diagnostic above is: this + // is the form the type used to arrive in, and interning the + // as-written spelling instead would put a differently-spelled + // reading in the cell for the same key. + let ty = crate::api::core::flat::canonical_type(&declared_ty.syntax); let mut matched = false; if let Some(ident) = bare_path_ident(&ty) { if let Some(s) = self @@ -469,18 +482,24 @@ impl Registry { /// key is held to the same grammar. A test that wants the whole scan builds its /// registry from items instead; this is for the ones that need a specific table /// shape and nothing else. + /// Takes the **spelling**, like every other door into the table: interning + /// needs real tokens, and a fixture has them — it wrote them (#291). #[cfg(test)] pub(crate) fn insert_crossing( &mut self, dir: Direction, - key: &TypeKey, + ty: &syn::Type, root: bool, entry: Option>, ) { - self.intern(dir, &key.to_type(), root) - .unwrap_or_else(|e| panic!("fixture key `{key}` is not expressible: {e}")); + self.intern(dir, ty, root).unwrap_or_else(|e| { + panic!( + "fixture type `{}` is not expressible: {e}", + ty.to_token_stream() + ) + }); self.type_table_mut(dir) - .get_mut(key) + .get_mut(&TypeKey::from_type(ty)) .expect("just registered") .entry = entry; } diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index bc04da62..b46d34aa 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -60,7 +60,9 @@ struct StubExt { functions: HashSet, helper_functions: HashSet, consts: Option>, - types: HashSet, + /// Declared with the spelling a build script would write, which is what + /// `export_type` takes (#291). + types: Vec, local_fns: Vec<(syn::ItemFn, String)>, } @@ -86,8 +88,8 @@ impl StubExt { reg = reg.export_const(i); } } - for k in &self.types { - reg = reg.export_type(k.clone()); + for t in &self.types { + reg = reg.export_type(crate::api::test_util::declared_origin(t.clone())); } Ok(reg) } @@ -507,8 +509,7 @@ fn qualified_signature_matches_bare_declaration() { let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("get").unwrap()); - ext.types - .insert(TypeKey::parse("Thing").expect("test type")); + ext.types.push(syn::parse_str("Thing").expect("test type")); let reg = ext .declare_into_any(reg) .expect("declare") @@ -541,10 +542,8 @@ fn multi_source_rename_cross_reference_normalizes() { let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("use_a").unwrap()); - ext.types - .insert(TypeKey::parse("TypeA").expect("test type")); - ext.types - .insert(TypeKey::parse("TypeB").expect("test type")); + ext.types.push(syn::parse_str("TypeA").expect("test type")); + ext.types.push(syn::parse_str("TypeB").expect("test type")); let reg = ext .declare_into_any(reg) .expect("declare") @@ -564,7 +563,7 @@ fn qualified_declared_type_is_hard_error() { let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.types - .insert(TypeKey::parse("myflat::Thing").expect("test type")); + .push(syn::parse_str("myflat::Thing").expect("test type")); match ext.declare_into_any(reg).expect("declare").scanned() { Err(ScanError::QualifiedDeclaredTypes { entries }) => { assert_eq!(entries.len(), 1); @@ -586,8 +585,9 @@ fn foreign_qualified_declared_type_stays_supported() { let items = vec![fn_item("fn touch(x: u64) -> u64 { x }")]; let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); - let foreign = TypeKey::parse("zenoh::KeyExpr<'static>").expect("test type"); - ext.types.insert(foreign.clone()); + let foreign_ty: syn::Type = syn::parse_str("zenoh::KeyExpr<'static>").expect("test type"); + let foreign = TypeKey::from_type(&foreign_ty); + ext.types.push(foreign_ty); let reg = ext .declare_into_any(reg) .expect("declare") @@ -865,7 +865,7 @@ fn a_composed_reading_reaches_the_cell_unchanged() { let reg: RegistryBuilder<()> = crate::api::test_util::reg_from_items(items).unwrap(); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("f").unwrap()); - ext.types.insert(TypeKey::parse("Thing").unwrap()); + ext.types.push(syn::parse_str("Thing").unwrap()); let mut reg = ext .declare_into_any(reg) .expect("declare") @@ -959,7 +959,7 @@ fn an_adapter_authored_type_cell_is_classified_but_placeless() { let mut ext = StubExt::default(); ext.types - .insert(TypeKey::parse("Foreign").expect("test type")); + .push(syn::parse_str("Foreign").expect("test type")); let reg = ext .declare_into_any(reg) .expect("declare") @@ -1408,7 +1408,7 @@ fn a_qualified_alias_warns_rather_than_failing() { let mut ext = StubExt::default(); // Head is NOT a source module, so this is the warn branch. ext.types - .insert(TypeKey::parse("foreign::Handle").expect("test type")); + .push(syn::parse_str("foreign::Handle").expect("test type")); ext.declare_into_any(reg) .expect("declare") .scanned() @@ -1596,7 +1596,7 @@ fn a_declared_crossing_the_grammar_refuses_is_not_called_a_prebindgen_item() { let mut ext = StubExt::default(); ext.types - .insert(TypeKey::parse("*const u8").expect("a key can hold it; the language cannot")); + .push(syn::parse_str("*const u8").expect("a key can hold it; the language cannot")); let err = ext .declare_into_any(reg) @@ -1686,7 +1686,7 @@ fn a_recursive_type_is_handed_out_once_and_terminates() { ]); let mut ext = StubExt::default(); ext.functions.insert(syn::parse_str("walk").unwrap()); - ext.types.insert(TypeKey::parse("Node").expect("test type")); + ext.types.push(syn::parse_str("Node").expect("test type")); let reg = ext .declare_into_any(reg) .expect("declare") diff --git a/prebindgen/src/api/core/resolve/tests.rs b/prebindgen/src/api/core/resolve/tests.rs index 7b4a12ce..3d8d0e6b 100644 --- a/prebindgen/src/api/core/resolve/tests.rs +++ b/prebindgen/src/api/core/resolve/tests.rs @@ -51,7 +51,7 @@ fn final_invariant_reports_unresolved_field_of_unresolved_struct() { /// that the resolved converter doesn't actually depend on. #[test] fn final_invariant_stops_at_resolved_nodes() { - use crate::api::core::registry::{Direction, Registry, TypeEntry, TypeKey}; + use crate::api::core::registry::{Direction, Registry, TypeEntry}; // Through the real scan, so the state under test is one the pipeline can // actually produce: `Unrelated` is a field type nothing declares. @@ -62,15 +62,15 @@ fn final_invariant_stops_at_resolved_nodes() { // `Outer` required & unresolved; `Inner` RESOLVED (with a dummy // entry); `Unrelated` unresolved but only reachable through Inner. - let outer_key = TypeKey::parse("Outer").expect("test type"); - let inner_key = TypeKey::parse("Inner").expect("test type"); - let unrelated_key = TypeKey::parse("Unrelated").expect("test type"); + let outer_ty: syn::Type = syn::parse_quote!(Outer); + let inner_ty: syn::Type = syn::parse_quote!(Inner); + let unrelated_ty: syn::Type = syn::parse_quote!(Unrelated); - reg.insert_crossing(Direction::Input, &outer_key, true, None); + reg.insert_crossing(Direction::Input, &outer_ty, true, None); reg.insert_crossing( Direction::Input, - &inner_key, + &inner_ty, false, Some(TypeEntry { destination: syn::parse_quote!(i64), @@ -84,7 +84,7 @@ fn final_invariant_stops_at_resolved_nodes() { }), ); - reg.insert_crossing(Direction::Input, &unrelated_key, false, None); + reg.insert_crossing(Direction::Input, &unrelated_ty, false, None); let err = check_complete(®).expect_err("must surface Outer"); let ResolveError::Unresolved { entries } = err; @@ -113,8 +113,8 @@ fn a_type_reachable_only_through_subs_must_still_resolve() { use crate::api::core::registry::{Registry, TypeKey}; let mut reg: Registry<()> = Registry::empty(); - let outer = TypeKey::parse("Outer").expect("test type"); - let mid = TypeKey::parse("Mid").expect("test type"); + let outer: syn::Type = syn::parse_quote!(Outer); + let mid: syn::Type = syn::parse_quote!(Mid); // `Outer` is a root AND resolved — so it is not itself reportable — but its // converter delegates to `Mid`. @@ -128,7 +128,7 @@ fn a_type_reachable_only_through_subs_must_still_resolve() { fn __outer() {} ), pre_stages: vec![], - subs: vec![mid.clone()], + subs: vec![TypeKey::from_type(&mid)], niches: crate::api::core::niches::Niches::empty(), metadata: (), }), diff --git a/prebindgen/src/api/core/write.rs b/prebindgen/src/api/core/write.rs index f252cd0d..5b9a9bad 100644 --- a/prebindgen/src/api/core/write.rs +++ b/prebindgen/src/api/core/write.rs @@ -97,7 +97,7 @@ pub fn write_rust, E: Prebindgen>( _ => None, })) .into_iter() - .filter(|(ident, _)| declared_types.contains(&TypeKey::from_ident(ident))) + .filter(|(ident, _)| declared_types.contains_key(&TypeKey::from_ident(ident))) .map(|(_, item)| ext.on_struct(item, registry)), )?); // Both enum shapes emit through `on_enum` and sort together: they were one @@ -113,7 +113,7 @@ pub fn write_rust, E: Prebindgen>( _ => None, })) .into_iter() - .filter(|(ident, _)| declared_types.contains(&TypeKey::from_ident(ident))) + .filter(|(ident, _)| declared_types.contains_key(&TypeKey::from_ident(ident))) .map(|(_, t)| match t { crate::api::core::flat::Type::Variant(v) => ext.on_variant(v, registry), crate::api::core::flat::Type::Enum(e) => ext.on_enum(e, registry), diff --git a/prebindgen/src/api/core/write/tests.rs b/prebindgen/src/api/core/write/tests.rs index 371b9568..2d0ca24c 100644 --- a/prebindgen/src/api/core/write/tests.rs +++ b/prebindgen/src/api/core/write/tests.rs @@ -17,7 +17,9 @@ impl IdentityExt { reg = reg.export(&f); } for t in ["AEnum", "AStruct", "BEnum", "BStruct"] { - reg = reg.export_type(TypeKey::parse(t).expect("test type")); + reg = reg.export_type(crate::api::test_util::declared_origin( + syn::parse_str(t).expect("test type"), + )); } reg } @@ -62,14 +64,14 @@ impl Prebindgen for IdentityExt { #[test] fn dedup_and_sort() { let mut reg: Registry<()> = Registry::empty(); - let key_a = TypeKey::parse("u64").expect("test type"); - let key_b = TypeKey::parse("Sample").expect("test type"); + let ty_a: syn::Type = syn::parse_quote!(u64); + let ty_b: syn::Type = syn::parse_quote!(Sample); let wire: syn::Type = syn::parse_quote!(i64); let wire2: syn::Type = syn::parse_quote!(*const u8); reg.insert_crossing( Direction::Input, - &key_a, + &ty_a, true, Some(TypeEntry { destination: wire.clone(), @@ -86,7 +88,7 @@ fn dedup_and_sort() { ); reg.insert_crossing( Direction::Input, - &key_b, + &ty_b, true, Some(TypeEntry { destination: wire2.clone(), diff --git a/prebindgen/src/api/lang/cbindgen/builder.rs b/prebindgen/src/api/lang/cbindgen/builder.rs index b774de26..601cbe85 100644 --- a/prebindgen/src/api/lang/cbindgen/builder.rs +++ b/prebindgen/src/api/lang/cbindgen/builder.rs @@ -203,7 +203,7 @@ impl CbindgenBuilder { "Cbindgen::opaque_ptr cannot declare `{}` because it is already ignored", key ); - self.opaque.insert(key.clone(), TypeCfg::default()); + self.opaque.insert(key.clone(), TypeCfg::new(ty)); self.current = Some(CurrentDecl::Ptr(key)); self } @@ -216,7 +216,7 @@ impl CbindgenBuilder { "Cbindgen::data_struct cannot declare `{}` because it is already ignored", key ); - self.data.insert(key.clone(), TypeCfg::default()); + self.data.insert(key.clone(), TypeCfg::new(ty)); self.current = Some(CurrentDecl::Data(key)); self } @@ -271,7 +271,7 @@ impl CbindgenBuilder { kind, generate_mirror: false, assume_c_field_validity: false, - cfg: TypeCfg::default(), + cfg: TypeCfg::new(rust_ty), }, ); self.current = Some(CurrentDecl::ValueOpaque(key)); @@ -318,7 +318,7 @@ impl CbindgenBuilder { kind: OpaqueKind::Data, generate_mirror: true, assume_c_field_validity: false, - cfg: TypeCfg::default(), + cfg: TypeCfg::new(ty), }, ); self.current = Some(CurrentDecl::ValueOpaque(key)); @@ -485,7 +485,7 @@ impl CbindgenBuilder { "Cbindgen::enum_type cannot declare `{}` because it is already ignored", key ); - self.enums.insert(key.clone(), TypeCfg::default()); + self.enums.insert(key.clone(), TypeCfg::new(ty)); self.current = Some(CurrentDecl::Enum(key)); self } @@ -522,7 +522,7 @@ impl CbindgenBuilder { "Cbindgen::tagged_union cannot declare `{}` because it is already ignored", key ); - self.tagged_unions.insert(key.clone(), TypeCfg::default()); + self.tagged_unions.insert(key.clone(), TypeCfg::new(ty)); self.current = Some(CurrentDecl::TaggedUnion(key)); self } @@ -541,7 +541,7 @@ impl CbindgenBuilder { ) }); let key: CallbackKey = args.iter().map(TypeKey::from_type).collect(); - self.callbacks.insert(key.clone(), CbCfg::default()); + self.callbacks.insert(key.clone(), CbCfg::new(args)); self.current = Some(CurrentDecl::Callback(key)); self } diff --git a/prebindgen/src/api/lang/cbindgen/convert.rs b/prebindgen/src/api/lang/cbindgen/convert.rs index 4b741a8b..96ab6eaa 100644 --- a/prebindgen/src/api/lang/cbindgen/convert.rs +++ b/prebindgen/src/api/lang/cbindgen/convert.rs @@ -18,7 +18,7 @@ impl CbindgenBuilder { .get(&decl.key) .cloned() .unwrap_or_else(|| { - let short = type_short(&decl.key.to_type()); + let short = type_short(&decl.rust_type.syntax.clone()); self.mangle_rust_type .as_ref() .map(|m| m(&short)) @@ -190,7 +190,7 @@ impl CbindgenBuilder { spec: &ConvertSpec, registry: &impl Conversions<()>, ) -> (syn::Type, syn::Expr, bool) { - let target = self.src_ty(&decl.key.to_type()); + let target = self.src_ty(&decl.rust_type.syntax.clone()); match spec { ConvertSpec::PrebindgenFn(f) => { let item = ®istry @@ -234,7 +234,7 @@ impl CbindgenBuilder { spec: &ConvertSpec, registry: &impl Conversions<()>, ) -> (syn::Type, syn::Expr, bool) { - let target = self.src_ty(&decl.key.to_type()); + let target = self.src_ty(&decl.rust_type.syntax.clone()); match spec { ConvertSpec::PrebindgenFn(f) => { let item = ®istry diff --git a/prebindgen/src/api/lang/cbindgen/mod.rs b/prebindgen/src/api/lang/cbindgen/mod.rs index a70a149d..da2aafca 100644 --- a/prebindgen/src/api/lang/cbindgen/mod.rs +++ b/prebindgen/src/api/lang/cbindgen/mod.rs @@ -108,6 +108,7 @@ pub(crate) use crate::api::core::types_util::{ }; use crate::api::{ core::{ + flat::Origin, niches::{NicheSlot, Niches}, prebindgen::{ConverterImpl, Prebindgen}, registry::{extract_fn_trait_args, Conversions, Direction, Registry, TypeKey}, @@ -115,14 +116,29 @@ use crate::api::{ lang::jnigen::{ConvertDecl, ConvertSpec}, }; +/// The origin of a type a **build script** wrote: real tokens, and deliberately +/// no source position — `SourceLocation::default()` is the sanctioned placeless +/// location for a type that was never in a captured file. +fn declared_origin(ty: syn::Type) -> Origin { + Origin::new(ty, std::rc::Rc::new(crate::SourceLocation::default())) +} + /// Identity of a declared callback signature: its argument-type list (the /// dedup key, since two `impl Fn` params with the same args share one closure /// struct). The return is always unit for the supported callbacks. type CallbackKey = Vec; /// Per-opaque-handle / per-data-struct / per-enum configuration. -#[derive(Clone, Default)] +#[derive(Clone)] struct TypeCfg { + /// The type this declaration was **written with** — the `ty` handed to + /// `opaque_ptr` / `data_struct` / `enum_type` / `tagged_union`. + /// + /// A declarator receives a real `syn::Type` and used to keep only the key + /// derived from it, so later sites had to ask the key for the tokens back. + /// The declaration is where the type came from, and this is where it stays + /// (#291). + rust_type: Origin, /// Per-declaration **base** token override, fed to the name manglers /// (`mangle_type_name` / `mangle_destructor` / `mangle_take`) in place of the /// `mangle_rust_type`-derived base. Set by [`CbindgenBuilder::base_name`]. `None` ⇒ @@ -130,6 +146,16 @@ struct TypeCfg { base: Option, } +impl TypeCfg { + /// A freshly declared type, no naming override yet. + fn new(rust_type: syn::Type) -> Self { + Self { + rust_type: declared_origin(rust_type), + base: None, + } + } +} + /// What an inline-opaque by-value type holds, which decides whether its consume /// path needs a gravestone write-back (and thus a [`crate::core::Gravestone`] /// impl). See [`CbindgenBuilder::opaque_data_struct`] / [`CbindgenBuilder::opaque_owned_struct`]. @@ -176,8 +202,14 @@ struct ValueOpaqueCfg { } /// Per-declared-callback configuration. -#[derive(Clone, Default)] +#[derive(Clone)] struct CbCfg { + /// The argument types this callback was declared with, in order. + /// + /// `CallbackKey` is a `Vec` — a list of identities, which is what + /// the map is keyed by. Emission needs the argument *types*, and these are + /// the ones `extract_fn_trait_args` produced at declaration time (#291). + args: Vec, /// Per-declaration **base** token override fed to `mangle_callback` (as the /// sole base, replacing the args' derived bases). Set by /// [`CbindgenBuilder::base_name`]. `None` ⇒ bases come from the arguments. @@ -191,6 +223,17 @@ struct CbCfg { takeable: std::collections::BTreeSet, } +impl CbCfg { + /// A freshly declared callback signature, no naming or takeable overrides yet. + fn new(args: Vec) -> Self { + Self { + args, + base: None, + takeable: std::collections::BTreeSet::new(), + } + } +} + /// Per-declared-function configuration. #[derive(Clone, Default)] struct FnCfg { diff --git a/prebindgen/src/api/lang/cbindgen/trait_impl.rs b/prebindgen/src/api/lang/cbindgen/trait_impl.rs index 82830ce7..8ec3d1e2 100644 --- a/prebindgen/src/api/lang/cbindgen/trait_impl.rs +++ b/prebindgen/src/api/lang/cbindgen/trait_impl.rs @@ -1469,13 +1469,14 @@ impl CbindgenBuilder { /// context. Deterministic order by emitted name. fn prereq_callback_structs(&self, registry: &Registry<()>) -> Vec { let mut items: Vec = Vec::new(); - let mut cb_keys: Vec<&CallbackKey> = self.callbacks.keys().collect(); - cb_keys.sort_by_key(|k| { - let args: Vec = k.iter().map(|t| t.to_type()).collect(); - self.callback_c_name(&args) - }); - for key in cb_keys { - let args: Vec = key.iter().map(|t| t.to_type()).collect(); + // The declaration's own argument types. `CallbackKey` is a list of + // identities — what the map is keyed by — and the arguments it was + // declared with are beside it, so neither is rebuilt from the other + // (#291). + let mut cb_keys: Vec<(&CallbackKey, &CbCfg)> = self.callbacks.iter().collect(); + cb_keys.sort_by_key(|(_, cfg)| self.callback_c_name(&cfg.args)); + for (key, cfg) in cb_keys { + let args: Vec = cfg.args.clone(); // Emit only if the callback is required (its input resolved); skip a // declared-but-unused signature. if registry @@ -1631,8 +1632,8 @@ impl CbindgenBuilder { for ident in self.helper_functions() { registry = registry.reference(&ident); } - for key in self.declared_types() { - registry = registry.export_type(key); + for ty in self.declared_types().into_values() { + registry = registry.export_type(ty); } Ok(registry) } @@ -1768,7 +1769,10 @@ impl Prebindgen for CbindgenBuilder { binding.flat(), &crate::core::Claimed { functions, - types: self.declared_types(), + // The report asks what was *claimed*, which is a set of + // identities — the declarations' spellings are the scan's + // business, not this one's. + types: self.declared_types().into_keys().collect(), consts: None, ignored_functions: self.ignored_functions(), ignored_types: self.ignored_types(), @@ -2539,14 +2543,21 @@ impl CbindgenBuilder { .filter(|ident| !self.functions.contains_key(ident)) .collect() } - pub(crate) fn declared_types(&self) -> HashSet { + /// Each with the spelling its declarator was written with — the scan needs + /// real tokens to intern a type that is in no table yet (#291). + pub(crate) fn declared_types(&self) -> HashMap> { self.opaque - .keys() - .chain(self.data.keys()) - .chain(self.value_opaque.keys()) - .chain(self.enums.keys()) - .chain(self.tagged_unions.keys()) - .cloned() + .iter() + .chain(self.data.iter()) + .map(|(k, c)| (k, &c.rust_type)) + .chain(self.value_opaque.iter().map(|(k, c)| (k, &c.cfg.rust_type))) + .chain( + self.enums + .iter() + .chain(self.tagged_unions.iter()) + .map(|(k, c)| (k, &c.rust_type)), + ) + .map(|(k, t)| (k.clone(), t.clone())) .collect() } pub(crate) fn ignored_types(&self) -> HashSet { diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index f5f3f62f..2621fbb4 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -412,9 +412,14 @@ impl JniGenBuilder { /// /// Returns the stored config so the caller can fold in its cross-kind /// options (`jobject_input`, interfaces). + /// + /// `rust_type` is the declaration's own spelling; `key` is the identity + /// derived from it. They cannot disagree — every `*ClassDecl::new` builds + /// both from the one `syn::Type` it was handed. fn register_class( &mut self, key: &TypeKey, + rust_type: Origin, kind: DeclaredKind, spec: NameSpec, ) -> &mut TypeConfig { @@ -437,12 +442,17 @@ impl JniGenBuilder { let short = rust_short_name(key); match self.decls.types.entry(key.clone()) { std::collections::hash_map::Entry::Occupied(e) => { + // A reopened declarator keeps the first spelling: the two agree + // on identity by construction, and the model indexes types + // first-mention-wins for the same reason. let cfg = e.into_mut(); cfg.kind.merge(kind, &short); cfg.name_spec = Some(spec); cfg } - std::collections::hash_map::Entry::Vacant(e) => e.insert(TypeConfig::new(kind, spec)), + std::collections::hash_map::Entry::Vacant(e) => { + e.insert(TypeConfig::new(kind, spec, rust_type)) + } } } @@ -472,6 +482,7 @@ impl JniGenBuilder { let key = decl.key; self.register_class( &key, + decl.rust_type, DeclaredKind::Ptr(OpaqueConfig { gc_managed: decl.gc_managed, }), @@ -491,6 +502,7 @@ impl JniGenBuilder { let key = decl.key; self.register_class( &key, + decl.rust_type, DeclaredKind::Enum(EnumConfig::default()), NameSpec { subpackage: subpackage.to_string(), @@ -509,6 +521,7 @@ impl JniGenBuilder { fn accept_sealed_class(&mut self, subpackage: &str, decl: SealedClassDecl) { let short = rust_short_name(&decl.key); let key = decl.key; + let rust_type = decl.rust_type; // Reopened decls merge — `DeclaredKind::merge` owns that rule for // every kind, so this acceptor only builds its own payload. let mut sum = SumConfig::default(); @@ -519,6 +532,7 @@ impl JniGenBuilder { } self.register_class( &key, + rust_type, DeclaredKind::Sealed(sum), NameSpec { subpackage: subpackage.to_string(), @@ -547,7 +561,7 @@ impl JniGenBuilder { let short = rust_short_name(&decl.key); let key = decl.key; let spec = Self::data_value_name_spec(subpackage, short, decl.name_override); - self.register_class(&key, DeclaredKind::Data, spec) + self.register_class(&key, decl.rust_type, DeclaredKind::Data, spec) .jobject_input |= decl.jobject_input; self.store_iface_opts(&key, decl.iface); self.accept_members(&key, decl.members); @@ -772,7 +786,7 @@ impl Declarations { } exp.constructors .push(crate::api::core::expand::ConstructorDecl { - target: decl.key.to_type(), + target: decl.rust_type.syntax.clone(), variants: decl.variants.iter().map(lower).collect(), default: true, }); @@ -795,7 +809,7 @@ impl Declarations { exp.expands.push(ExpandDecl { func: func.clone(), param: syn::Ident::new(param, Span::call_site()), - declared_target: Some(decl.key.to_type()), + declared_target: Some(decl.rust_type.syntax.clone()), sel: ExpandSel::Subset(decl.variants.iter().map(lower).collect()), }); } @@ -1158,7 +1172,7 @@ impl Declarations { k = decl.key.as_str() ); dec.deconstructors.push(DeconstructorDecl { - target: decl.key.to_type(), + target: decl.rust_type.syntax.clone(), records: self.lower_fields(registry, &decl.key, &decl.fields), default: Some((DeconTarget::Output, Delivery::Callback)), }); @@ -1183,7 +1197,7 @@ impl Declarations { sel: DeconSel::Inline(self.lower_fields(registry, &decl.key, &decl.fields)), target: DeconTarget::Output, delivery: Delivery::Callback, - declared_source: Some(decl.key.to_type()), + declared_source: Some(decl.rust_type.syntax.clone()), }); } dec @@ -1315,15 +1329,33 @@ impl Declarations { /// **rust-side-only** types. Unioned into [`Prebindgen::ignored_types`] /// so the registry treats them as acknowledged (no "skipping undeclared" /// warning, no direct converter requirement, no Kotlin emission). - pub(crate) fn rust_side_only_types(&self) -> impl Iterator + '_ { + /// + /// Yields each decl's own `syn::Type` beside its key: these are types a + /// build script wrote, and the scan diagnoses their spelling before + /// anything has classified them (#291). + pub(crate) fn rust_side_only_types( + &self, + ) -> impl Iterator)> + '_ { self.param_expand_decls .iter() - .map(|d| &d.key) - .chain(self.return_expand_decls.iter().map(|d| &d.key)) - .chain(self.fn_param_expands.iter().map(|(_, _, d)| &d.key)) - .chain(self.fn_return_expands.iter().map(|(_, d)| &d.key)) - .filter(|k| !self.is_class_declared(k)) - .cloned() + .map(|d| (&d.key, &d.rust_type)) + .chain( + self.return_expand_decls + .iter() + .map(|d| (&d.key, &d.rust_type)), + ) + .chain( + self.fn_param_expands + .iter() + .map(|(_, _, d)| (&d.key, &d.rust_type)), + ) + .chain( + self.fn_return_expands + .iter() + .map(|(_, d)| (&d.key, &d.rust_type)), + ) + .filter(|(k, _)| !self.is_class_declared(k)) + .map(|(k, t)| (k.clone(), t.clone())) } /// Function idents referenced only inside boundary decls (type-level and @@ -1454,7 +1486,9 @@ impl Declarations { registry: &impl Conversions, ) -> Option<(syn::Type, Option, syn::Expr)> { let decl = self.convert_decls.iter().find(|d| &d.key == key)?; - let target = key.to_type(); + // The `convert!` declaration's own spelling — the key is how the decl + // was found, not a second source for what it says (#291). + let target = decl.rust_type.syntax.clone(); let result = match decl.input.as_ref()? { ConvertSpec::PrebindgenFn(f) => { let item_fn = registry @@ -1530,7 +1564,9 @@ impl Declarations { registry: &impl Conversions, ) -> Option<(syn::Type, Option, syn::Expr)> { let decl = self.convert_decls.iter().find(|d| &d.key == key)?; - let target = key.to_type(); + // The `convert!` declaration's own spelling — the key is how the decl + // was found, not a second source for what it says (#291). + let target = decl.rust_type.syntax.clone(); let result = match decl.output.as_ref()? { ConvertSpec::PrebindgenFn(g) => { let item_fn = registry diff --git a/prebindgen/src/api/lang/jnigen/jni/decl.rs b/prebindgen/src/api/lang/jnigen/jni/decl.rs index 78439658..2e2a245c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/decl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/decl.rs @@ -12,6 +12,16 @@ use super::*; +/// The origin of a type a **build script** wrote. +/// +/// Real tokens, and deliberately no source position: `SourceLocation::default()` +/// is the sanctioned placeless location for exactly this — a signature or type a +/// build script authored was never in a captured file, and `has_position` already +/// gates what a diagnostic prints for one. +pub(crate) fn declared_origin(ty: syn::Type) -> Origin { + Origin::new(ty, std::rc::Rc::new(crate::SourceLocation::default())) +} + // ────────────────────────────────────────────────────────────────────── // Shared local accumulators (replayed into `Expansions`/`Deconstructors` // by the accept logic in `builder.rs` once a decl is handed to `Declarations`) @@ -349,6 +359,10 @@ macro_rules! fields { /// [`implements`](Self::implements). pub struct PtrClassDecl { pub(crate) key: TypeKey, + /// The type this declaration was **written with** — the `X` the macro + /// received. Kept because the declaration is where it came from: recovering + /// it later *from* the key was reasoning backwards from an identity (#291). + pub(crate) rust_type: Origin, pub(crate) name_override: Option, pub(crate) members: Vec<(FunctionDecl, MemberKind)>, pub(crate) iface: IfaceOpts, @@ -443,6 +457,7 @@ impl PtrClassDecl { pub fn new(rust_type: syn::Type) -> Self { Self { key: TypeKey::from_type(&rust_type), + rust_type: declared_origin(rust_type), name_override: None, members: Vec::new(), iface: IfaceOpts::default(), @@ -562,6 +577,10 @@ impl From for PtrClassDecl { #[derive(Clone)] pub struct ExpandParamDecl { pub(crate) key: TypeKey, + /// The type this declaration was **written with** — the `X` the macro + /// received. Kept because the declaration is where it came from: recovering + /// it later *from* the key was reasoning backwards from an identity (#291). + pub(crate) rust_type: Origin, pub(crate) variants: Vec, /// `.no_split()` — suppress the proactive splittability check for this /// variant set (it will only ever be used as the selector form). See @@ -573,6 +592,7 @@ impl ExpandParamDecl { pub fn new(rust_type: syn::Type) -> Self { Self { key: TypeKey::from_type(&rust_type), + rust_type: declared_origin(rust_type), variants: Vec::new(), no_split: false, } @@ -664,6 +684,10 @@ impl ExpandParamDecl { #[derive(Clone)] pub struct ExpandReturnDecl { pub(crate) key: TypeKey, + /// The type this declaration was **written with** — the `X` the macro + /// received. Kept because the declaration is where it came from: recovering + /// it later *from* the key was reasoning backwards from an identity (#291). + pub(crate) rust_type: Origin, pub(crate) fields: Vec, } @@ -671,6 +695,7 @@ impl ExpandReturnDecl { pub fn new(rust_type: syn::Type) -> Self { Self { key: TypeKey::from_type(&rust_type), + rust_type: declared_origin(rust_type), fields: Vec::new(), } } @@ -993,6 +1018,10 @@ impl From for ExpandDecl { /// identity — a "method" on it is just a free function taking the enum. pub struct EnumClassDecl { pub(crate) key: TypeKey, + /// The type this declaration was **written with** — the `X` the macro + /// received. Kept because the declaration is where it came from: recovering + /// it later *from* the key was reasoning backwards from an identity (#291). + pub(crate) rust_type: Origin, pub(crate) name_override: Option, pub(crate) iface: IfaceOpts, } @@ -1001,6 +1030,7 @@ impl EnumClassDecl { pub fn new(rust_type: syn::Type) -> Self { Self { key: TypeKey::from_type(&rust_type), + rust_type: declared_origin(rust_type), name_override: None, iface: IfaceOpts::default(), } @@ -1053,6 +1083,10 @@ impl From for EnumClassDecl { /// taking it. pub struct SealedClassDecl { pub(crate) key: TypeKey, + /// The type this declaration was **written with** — the `X` the macro + /// received. Kept because the declaration is where it came from: recovering + /// it later *from* the key was reasoning backwards from an identity (#291). + pub(crate) rust_type: Origin, pub(crate) name_override: Option, pub(crate) variants: Vec, pub(crate) iface: IfaceOpts, @@ -1062,6 +1096,7 @@ impl SealedClassDecl { pub fn new(rust_type: syn::Type) -> Self { Self { key: TypeKey::from_type(&rust_type), + rust_type: declared_origin(rust_type), name_override: None, variants: Vec::new(), iface: IfaceOpts::default(), @@ -1124,6 +1159,10 @@ impl VariantDecl { /// destructuring a data-class parameter gets), just rebased to `this`. pub struct DataClassDecl { pub(crate) key: TypeKey, + /// The type this declaration was **written with** — the `X` the macro + /// received. Kept because the declaration is where it came from: recovering + /// it later *from* the key was reasoning backwards from an identity (#291). + pub(crate) rust_type: Origin, pub(crate) name_override: Option, pub(crate) jobject_input: bool, pub(crate) iface: IfaceOpts, @@ -1134,6 +1173,7 @@ impl DataClassDecl { pub fn new(rust_type: syn::Type) -> Self { Self { key: TypeKey::from_type(&rust_type), + rust_type: declared_origin(rust_type), name_override: None, jobject_input: false, iface: IfaceOpts::default(), @@ -1880,6 +1920,10 @@ impl From for ConvertSourceDecl { #[derive(Clone)] pub struct ConvertDecl { pub(crate) key: TypeKey, + /// The type this declaration was **written with** — the `X` the macro + /// received. Kept because the declaration is where it came from: recovering + /// it later *from* the key was reasoning backwards from an identity (#291). + pub(crate) rust_type: Origin, pub(crate) input: Option, pub(crate) output: Option, pub(crate) domain: Option, @@ -1910,6 +1954,7 @@ impl ConvertDecl { reject_builtin_convert_type(&TypeKey::from_type(&rust_type)); Self { key: TypeKey::from_type(&rust_type), + rust_type: declared_origin(rust_type), input: None, output: None, domain: None, diff --git a/prebindgen/src/api/lang/jnigen/jni/iface.rs b/prebindgen/src/api/lang/jnigen/jni/iface.rs index 74357346..596dadaa 100644 --- a/prebindgen/src/api/lang/jnigen/jni/iface.rs +++ b/prebindgen/src/api/lang/jnigen/jni/iface.rs @@ -981,10 +981,11 @@ pub(crate) fn fixed_leaf_element_keys( } /// Derive the spec for one identity — the SINGLE construction point behind -/// [`Declarations::iface_spec`]. Any `syn` context comes from the key's stored -/// normalized type ([`TypeKey::to_type`] — a clone, not a reparse). A -/// `Folder` derivation folds the fixed-builder typed-group view in per -/// `DeconId` (see [`fixed_decon_ids`]). +/// [`Declarations::iface_spec`]. Any `syn` context is **looked up**, never +/// rebuilt from the key: `Registry::reading` answers from the type table, and a +/// key it cannot answer for defers rather than producing tokens nothing +/// classified (#291). A `Folder` derivation folds the fixed-builder +/// typed-group view in per `DeconId` (see [`fixed_decon_ids`]). fn derive_iface_spec( ext: &Declarations, registry: &impl Conversions, @@ -1019,7 +1020,12 @@ fn derive_iface_spec( } Some(spec) } - SpecKey::WholeFolder(el_key) => whole_folder_iface_spec(ext, registry, &el_key.to_type()), + // Same round trip, same reason, same answer as the `Callback` arm + // above: the memo key holds an identity, and the reading behind it is a + // lookup. `None` defers, exactly as it does there (#291). + SpecKey::WholeFolder(el_key) => { + whole_folder_iface_spec(ext, registry, registry.reading(el_key)?.syntax()) + } SpecKey::Handler(d) => error_handler_iface_spec(ext, registry, d), SpecKey::JniErrorHandler => Some(jni_error_handler_iface_spec(ext)), } diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index b66092f3..5284299d 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -50,6 +50,7 @@ pub(crate) use crate::api::gen::kotlin as kt; pub(crate) use crate::api::{ core::{ domain::ScalarValue, + flat::Origin, niches::{NicheSlot, Niches}, prebindgen::{ConverterImpl, Prebindgen, Stage}, registry::{Direction, Registry, TypeKey}, @@ -186,6 +187,14 @@ pub(crate) struct TypeConfig { /// unlike a wrapper registration, which is required per **usage** /// direction. pub kind: DeclaredKind, + /// The type this declaration was **written with**, e.g. the `Foo` in + /// `ptr_class!(Foo)`. + /// + /// A class declarator receives a real `syn::Type` and used to keep only the + /// key derived from it, so every later site that needed the tokens back had + /// to ask the key for them. That is the wrong direction: the declaration is + /// where the type came from, and this is where it stays (#291). + pub rust_type: Origin, /// Raw naming spec of the type as declared — verbatim Kotlin type or /// settings-derived class name. Required for any type emitted in /// Kotlin; the concrete FQN (`Sample` → `"io.zenoh.jni.Sample"`, @@ -215,9 +224,14 @@ impl TypeConfig { /// A freshly declared type: the declarator's kind and naming spec, every /// cross-kind option unset. Reopening the same declarator goes through /// [`DeclaredKind::merge`] instead. - pub(crate) fn new(kind: DeclaredKind, name_spec: NameSpec) -> Self { + pub(crate) fn new( + kind: DeclaredKind, + name_spec: NameSpec, + rust_type: Origin, + ) -> Self { Self { kind, + rust_type, name_spec: Some(name_spec), jobject_input: false, interface_enabled: false, diff --git a/prebindgen/src/api/lang/jnigen/jni/overloads.rs b/prebindgen/src/api/lang/jnigen/jni/overloads.rs index 3a5bbccc..2ccfe991 100644 --- a/prebindgen/src/api/lang/jnigen/jni/overloads.rs +++ b/prebindgen/src/api/lang/jnigen/jni/overloads.rs @@ -62,7 +62,7 @@ impl Declarations { if decl.no_split || decl.variants.len() < 2 { continue; } - let target = decl.key.to_type(); + let target = decl.rust_type.syntax.clone(); let sigs: Vec<(String, Vec)> = decl .variants .iter() diff --git a/prebindgen/src/api/lang/jnigen/jni/report.rs b/prebindgen/src/api/lang/jnigen/jni/report.rs index bb4171a6..f2cc3b75 100644 --- a/prebindgen/src/api/lang/jnigen/jni/report.rs +++ b/prebindgen/src/api/lang/jnigen/jni/report.rs @@ -167,7 +167,7 @@ impl super::JniGen { // listed above with the other declared classes, not here. let mut boundary: Vec = ext .rust_side_only_types() - .map(|k| format!("- `{}` (never materializes in Kotlin)\n", k.as_str())) + .map(|(k, _)| format!("- `{}` (never materializes in Kotlin)\n", k.as_str())) .collect(); boundary.sort(); if !boundary.is_empty() { diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs index 4d820448..46649824 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/mod.rs @@ -67,8 +67,8 @@ fn install_input( _rank: usize, e: TypeEntry, ) { - let key = TypeKey::parse(ty_str).expect("test type"); - reg.insert_crossing(Direction::Input, &key, true, Some(e)); + let ty: syn::Type = syn::parse_str(ty_str).expect("test type"); + reg.insert_crossing(Direction::Input, &ty, true, Some(e)); } fn install_output( @@ -77,6 +77,6 @@ fn install_output( _rank: usize, e: TypeEntry, ) { - let key = TypeKey::parse(ty_str).expect("test type"); - reg.insert_crossing(Direction::Output, &key, true, Some(e)); + let ty: syn::Type = syn::parse_str(ty_str).expect("test type"); + reg.insert_crossing(Direction::Output, &ty, true, Some(e)); } diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 78e33d1e..2ff08d61 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -367,7 +367,7 @@ impl Declarations { // Rust-side-only boundary types are absent from the type table but // still appear in emitted signatures (e.g. the `E` of a peeled // `Result`), so they need the same qualification. - for key in self.rust_side_only_types().collect::>() { + for (key, _) in self.rust_side_only_types().collect::>() { add(&key); } // `convert!`-declared types likewise have no type-table entry but @@ -1341,8 +1341,8 @@ impl Declarations { for ident in self.declared_consts().into_iter().flatten() { registry = registry.export_const(&ident); } - for key in self.declared_types() { - registry = registry.export_type(key); + for ty in self.declared_types().into_values() { + registry = registry.export_type(ty); } for ident in self.accessor_functions() { registry = registry.accessor(&ident); @@ -1449,7 +1449,11 @@ impl Declarations { let Some(sum_cfg) = self.types[key].sum() else { continue; }; - let source = key.to_type(); + // The `sealed_class!` declaration's own spelling. This runs during + // the declare phase, where a `reading()` would legitimately answer + // `None` for a type nothing has interned yet — the declaration is + // the only thing that can say (#291). + let source = self.types[key].rust_type.syntax.clone(); let Some(ident) = bare_path_ident(&source) else { continue; }; @@ -2737,8 +2741,14 @@ impl Declarations { /// registrations live in their own tables and are deliberately excluded: a /// wrapper type is required per **usage** direction, so an output-only /// wrapper needs no input twin. - pub(crate) fn declared_types(&self) -> std::collections::HashSet { - self.types.keys().cloned().collect() + /// + /// Each with the spelling its declarator was written with — the scan needs + /// real tokens to intern a type that is in no table yet (#291). + pub(crate) fn declared_types(&self) -> std::collections::HashMap> { + self.types + .iter() + .map(|(k, c)| (k.clone(), c.rust_type.clone())) + .collect() } /// Types acknowledged-but-undeclared via [`JniGenBuilder::ignore`]. pub(crate) fn ignored_types(&self) -> std::collections::HashSet { @@ -2751,8 +2761,11 @@ impl Declarations { pub(crate) fn claimed(&self) -> crate::core::Claimed { let mut functions = self.declared_functions(); functions.extend(self.helper_functions()); - let mut types = self.declared_types(); - types.extend(self.boundary_only_types()); + // The report asks what was *claimed*, which is a set of identities — + // the declarations' spellings are the scan's business, not this one's. + let mut types: std::collections::HashSet = + self.declared_types().into_keys().collect(); + types.extend(self.boundary_only_types().into_keys()); crate::core::Claimed { functions, types, @@ -2769,7 +2782,9 @@ impl Declarations { /// (unfold / error channel) cross the boundary — so the registry /// acknowledges them and drops their direct converter requirements once /// the plans are in place. - pub(crate) fn boundary_only_types(&self) -> std::collections::HashSet { + pub(crate) fn boundary_only_types( + &self, + ) -> std::collections::HashMap> { // A `sealed_class!`-declared sum has no single wire: it crosses as a // tag plus one leaf group per variant, so a direct converter for the // value itself is genuinely not needed. Declaring it boundary-only @@ -2781,7 +2796,7 @@ impl Declarations { self.types .iter() .filter(|(_, c)| c.sum().is_some()) - .map(|(k, _)| k.clone()), + .map(|(k, c)| (k.clone(), c.rust_type.clone())), ) .collect() } diff --git a/prebindgen/src/api/test_util.rs b/prebindgen/src/api/test_util.rs index 054c1b60..06474415 100644 --- a/prebindgen/src/api/test_util.rs +++ b/prebindgen/src/api/test_util.rs @@ -32,6 +32,14 @@ pub(crate) fn scanned_with(sources: &[&str]) -> Registry { reg_with(sources).scanned().expect("scan") } +/// A type as a **build script** would declare it: real tokens, no source +/// position. What +/// [`RegistryBuilder::export_type`](crate::api::core::registry::RegistryBuilder::export_type) +/// takes. +pub(crate) fn declared_origin(ty: syn::Type) -> crate::core::flat::Origin { + crate::core::flat::Origin::new(ty, std::rc::Rc::new(crate::SourceLocation::default())) +} + /// One type as the **model** reads it, for a test that needs a `TypeRef` and has /// only a spelling. /// From a89783faa34685b83a1f53940ed36e3dc2cbd773 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 04:56:13 +0200 Subject: [PATCH 43/52] A key answers what it is called (#291 stage C1) (#300) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * A key answers what it is called (#291 stage C1) Eight sites asked `to_type()` for a whole `syn::Type` and then threw all of it away but one ident. They were not asking for syntax; they were asking the key what it is called, and a key can answer that itself. TypeKey::ident() -> Option // bare_path_ident's rule TypeKey::short_name() -> Option // last segment, generics and all Two, because the incumbent walks genuinely differ: `bare_path_ident` refuses a type carrying generic arguments, while the Kotlin class-name derivation reads `Publisher<'static>` as `Publisher` — a declaration writes the latter and means the class. Keeping one accessor would have had to pick a winner and silently change one set of call sites. Both read the canonical string, and that is deliberate rather than a shortcut: `canon` is a token-stream rendering, so tokens are space-separated (`Vec < u8 >`, `& Foo`, `a :: Foo`), which puts a path's head before the first `<` and its last segment after the last `::`, with `syn::parse_str::` as the total validator on the far end. Reparsing the type instead would make a NAME depend on a serialize-then-reparse round trip, which is the dependency #95 removed. `key_name_accessors_match_the_syn_walks` is the warrant: sixteen shapes — bare and qualified paths, generics, references, slices, arrays, tuples, unit, raw pointers, trait objects, fn pointers — each asserted equal to the walk it replaces. It also pins the one documented limit, a qualified-self path answering `None`; `scan_declared_items` refuses one, and refusing beats guessing for a shape this cannot read. The boundary ledger moved 120 -> 117 and `kotlin_emit.rs` left it entirely: these were real source-syntax classification sites, not just call-site noise. Verified: 634 lib tests, `cargo test --all --all-features`, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, and covertest-kotlin's 49-section JVM harness. * Review: a path segment is not the last thing before a `<` The string walk split at the FIRST `<` and took the last `::` of what was left. That gets `Vec` right — the `::` belongs to the argument — and `a::Foo::Bar` wrong: `short_name` answered `Foo`, and `ident` answered `None` because the canon contained a `<` at all, where both syn walks answer `Bar`. Generic arguments on a NON-FINAL segment were the case the split could not see, and the migrated Kotlin lookups would have resolved the wrong name or silently skipped the declaration. The walk is nesting-aware now: `path_segments` tracks angle depth, splits on `::` only at depth 0, and takes each segment's arguments from that segment. `ident` then applies `bare_path_ident`'s actual rule — arguments on the LAST segment only — instead of on the whole string. Two things fell out of doing it properly: * A qualified-self path no longer needs its documented exception. syn keeps only `Item` of `::Item` in `path.segments`, and skipping the leading group reads it the same way, so the accessors now MATCH the walk there rather than declining. * The `>` of a bare fn's `->` is an arrow, not a bracket. Miscounting it makes the `::` inside `Vec a::B>` look top-level, which would answer `B` for a type whose name is `Vec`. Both reviewers also noted `ident` did redundant work — it built a String via `short_name`, then reparsed it. It shares the one scan now and parses each segment once. SHAPES grows from 16 to 24, with the three cases above plus the ones that pull against them, and three focused tests pin the last-segment rule, the qualified-self tail, and separators inside arguments. Verified: 636 lib tests, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, covertest-kotlin 49/49. --- prebindgen/src/api/core/diagnostics.rs | 3 +- prebindgen/src/api/core/flat/boundary.ledger | 5 +- prebindgen/src/api/core/registry/key.rs | 260 ++++++++++++++++++ .../src/api/lang/jnigen/jni/emit/names.rs | 8 +- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 23 +- prebindgen/src/api/lang/jnigen/jni/render.rs | 3 +- prebindgen/src/api/lang/jnigen/jni/symbols.rs | 9 +- 7 files changed, 274 insertions(+), 37 deletions(-) diff --git a/prebindgen/src/api/core/diagnostics.rs b/prebindgen/src/api/core/diagnostics.rs index 8b185087..8962d70c 100644 --- a/prebindgen/src/api/core/diagnostics.rs +++ b/prebindgen/src/api/core/diagnostics.rs @@ -15,7 +15,6 @@ use crate::api::core::{ flat::{type_from_ident, Flat}, prebindgen::NamePredicate, registry::TypeKey, - types_util::bare_path_ident, }; /// What a binding claimed, so everything else can be reported. @@ -79,7 +78,7 @@ pub(crate) fn unclaimed_report(flat: &Flat, claimed: &Claimed) -> Vec { for key in sorted(claimed.ignored_types.iter().map(|k| k.as_str().to_owned())) { let named = TypeKey::parse(&key) .ok() - .and_then(|k| bare_path_ident(&k.to_type())) + .and_then(|k| k.ident()) .is_some_and(|ident| flat.declared_type(&ident).is_some()); if !named { out.push(format!( diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index e263c69b..82b1db05 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -55,11 +55,10 @@ 4 api/lang/jnigen/jni/emit/convert.rs 2 api/lang/jnigen/jni/emit/delivery.rs 5 api/lang/jnigen/jni/emit/flat_input.rs -17 api/lang/jnigen/jni/emit/names.rs +16 api/lang/jnigen/jni/emit/names.rs 11 api/lang/jnigen/jni/emit/wrapper.rs 3 api/lang/jnigen/jni/fold.rs 4 api/lang/jnigen/jni/iface.rs -2 api/lang/jnigen/jni/kotlin_emit.rs 2 api/lang/jnigen/jni/overloads.rs 1 api/lang/jnigen/jni/prim.rs 3 api/lang/jnigen/jni/prim_array.rs @@ -69,4 +68,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 120 +# total: 117 diff --git a/prebindgen/src/api/core/registry/key.rs b/prebindgen/src/api/core/registry/key.rs index 0ce807b1..7d8b35b4 100644 --- a/prebindgen/src/api/core/registry/key.rs +++ b/prebindgen/src/api/core/registry/key.rs @@ -102,6 +102,143 @@ impl TypeKey { pub fn to_type(&self) -> syn::Type { (*self.ty).clone() } + + /// The bare item ident this key names — `Foo`, `a::Foo` → `Foo`, + /// `a::Foo::Bar` → `Bar`; `None` when the **last** segment carries + /// generic arguments (`Vec` names no bare item) or the key is not a + /// path. + /// + /// Matches [`bare_path_ident`](crate::api::core::types_util::bare_path_ident) + /// on the same type, which is what + /// `key_name_accessors_match_the_syn_walks` pins. + /// + /// **A name is not syntax**, which is why this is the key's business and + /// producing a `syn::Type` is not. A caller that wants to look a declared + /// item up by name was never asking for tokens; it was asking the key what + /// it is called (#291). + pub fn ident(&self) -> Option { + let (ident, generic) = self.path_segments()?.pop()?; + // `bare_path_ident` reads `PathArguments` on the LAST segment only, so + // arguments earlier in the path do not disqualify the name. + if generic { + return None; + } + Some(ident) + } + + /// The last path segment's ident, **ignoring** its generic arguments — + /// `Publisher<'static>` → `"Publisher"`, `a::Foo::Bar` → `"Bar"`. + /// `None` for anything that is not a path. + /// + /// The looser sibling of [`Self::ident`], for the callers that derive a + /// destination-language class name from a Rust type: a declaration writes + /// `ptr_class!(Publisher<'static>)` and means the class `Publisher`. + pub fn short_name(&self) -> Option { + Some(self.path_segments()?.pop()?.0.to_string()) + } + + /// The **top-level** path segments of the canonical string: each segment's + /// ident, and whether that segment carried generic arguments. `None` if the + /// canon is not a path at all. + /// + /// # Read off the canonical string + /// + /// Deliberately, and not as a shortcut. `canon` is a token-stream + /// rendering, so its tokens are space-separated — `Vec < u8 >`, `& Foo`, + /// `a :: Foo` — and the structure is recoverable by tracking angle depth. + /// Reparsing the whole type instead would make a NAME depend on a + /// serialize-then-reparse round trip, which is the dependency #95 removed; + /// storing the derived names on the key would put derived state back on a + /// value whose whole point is that it carries none. + /// + /// **Nesting-aware, because a path segment is not the last thing before a + /// `<`.** `a::Foo::Bar` names `Bar`, and `Vec` names `Vec` — the + /// `::` in the second belongs to the argument. Splitting at the first `<` + /// got the second right and the first wrong. + /// + /// `syn::parse_str::` on every segment is the totality check: + /// each non-path shape puts something in a segment that is not an ident — + /// `& Foo`, `[u8 ; 4]`, `( )`, `* const u8`, `dyn Error`, `fn () -> u8`. + fn path_segments(&self) -> Option> { + let mut rest: &str = &self.canon; + // A qualified-self path renders its qualification first + // (`< T as Tr > :: Item`) and syn keeps only the tail in + // `path.segments` — so drop the group and read the rest as a plain path. + if rest.starts_with('<') { + rest = rest[close_angle(rest)? + 1..] + .trim_start() + .strip_prefix("::")?; + } + + let bytes = rest.as_bytes(); + let mut out = Vec::new(); + let mut depth = 0usize; + let mut start = 0usize; + let mut ident_end: Option = None; + let mut i = 0usize; + while i < bytes.len() { + match bytes[i] { + b'<' => { + if depth == 0 && ident_end.is_none() { + ident_end = Some(i); + } + depth += 1; + } + // The `>` of a bare fn's `->` is an arrow, not a bracket, and + // miscounting it would let a `::` inside `Vec a::B>` + // read as a top-level separator. + b'>' if i > 0 && bytes[i - 1] == b'-' => {} + b'>' => depth = depth.saturating_sub(1), + b':' if depth == 0 && bytes.get(i + 1) == Some(&b':') => { + out.push(segment(rest, start, ident_end, i)?); + i += 2; + start = i; + ident_end = None; + continue; + } + _ => {} + } + i += 1; + } + out.push(segment(rest, start, ident_end, bytes.len())?); + Some(out) + } +} + +/// One path segment's ident (up to its own generic arguments, if any) and +/// whether it had them. `None` when the text is not an ident, which is how a +/// non-path canon is refused. +fn segment( + s: &str, + start: usize, + ident_end: Option, + end: usize, +) -> Option<(syn::Ident, bool)> { + let text = s[start..ident_end.unwrap_or(end)].trim(); + Some(( + syn::parse_str::(text).ok()?, + ident_end.is_some(), + )) +} + +/// Byte index of the `>` closing the angle group `s` opens with. +fn close_angle(s: &str) -> Option { + let bytes = s.as_bytes(); + let mut depth = 0usize; + for (i, b) in bytes.iter().enumerate() { + match b { + b'<' => depth += 1, + b'>' if i > 0 && bytes[i - 1] == b'-' => {} + b'>' => { + depth = depth.saturating_sub(1); + if depth == 0 { + return Some(i); + } + } + _ => {} + } + } + None } impl fmt::Display for TypeKey { @@ -109,3 +246,126 @@ impl fmt::Display for TypeKey { f.write_str(&self.canon) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::core::types_util::bare_path_ident; + + /// Every shape a key can hold, as a build script or a source could spell it. + /// + /// Generics on a **non-final** segment (`a::Foo::Bar`) and a `::` inside + /// an argument (`Vec`) pull in opposite directions, and a `->` inside + /// an argument (`Vec a::B>`) breaks naive angle counting — each is a + /// way the string walk can be wrong while the easy cases still pass. + const SHAPES: &[&str] = &[ + "Foo", + "a::Foo", + "a::b::Foo", + "std::string::String", + "Vec", + "Vec", + "Vec>", + "a::Foo::Bar", + "Foo::Assoc", + "::Item", + "Vec a::B>", + "Publisher<'static>", + "Option>", + "&Foo", + "&mut Foo", + "&[u8]", + "[u8; 4]", + "()", + "(u8, u8)", + "(a::B, c::D)", + "*const u8", + "dyn Error", + "fn() -> u8", + "fn(u8) -> a::B", + ]; + + /// The accessors and the `syn` walks they replace answer identically. + /// + /// This is the whole warrant for reading names off the canonical string + /// instead of off a parsed type. Both walks are the incumbent definition — + /// `bare_path_ident` for [`TypeKey::ident`], and `rust_short_name_opt`'s + /// last-segment rule (spelled out here rather than imported, since it lives + /// under a language adapter) for [`TypeKey::short_name`]. + #[test] + fn key_name_accessors_match_the_syn_walks() { + for spec in SHAPES { + let ty: syn::Type = syn::parse_str(spec).expect("test shape parses"); + let key = TypeKey::from_type(&ty); + + assert_eq!( + key.ident(), + bare_path_ident(&crate::api::core::flat::canonical_type(&ty)), + "ident() disagrees with bare_path_ident on `{spec}` (canon `{key}`)" + ); + + // `rust_short_name_opt`: the last path segment's ident, generic + // arguments and all. + let expected_short = match &crate::api::core::flat::canonical_type(&ty) { + syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.to_string()), + _ => None, + }; + assert_eq!( + key.short_name(), + expected_short, + "short_name() disagrees with the last-segment rule on `{spec}` (canon `{key}`)" + ); + } + } + + /// `short_name` is looser than `ident` in exactly one way: generic arguments + /// **on the last segment**. + #[test] + fn short_name_reads_through_last_segment_generics_and_ident_does_not() { + let key = TypeKey::from_type(&syn::parse_quote!(Publisher<'static>)); + assert_eq!(key.short_name().as_deref(), Some("Publisher")); + assert_eq!(key.ident(), None); + + // Arguments EARLIER in the path disqualify nothing: the segment being + // named is `Bar`, and it has none. + let nested = TypeKey::from_type(&syn::parse_quote!(a::Foo::Bar)); + assert_eq!(nested.short_name().as_deref(), Some("Bar")); + assert_eq!( + nested.ident().map(|i| i.to_string()).as_deref(), + Some("Bar") + ); + } + + /// A qualified-self path names its tail, like `bare_path_ident` does — + /// syn keeps only `Item` in `path.segments`, and so does the string walk. + #[test] + fn qualified_self_paths_name_their_tail() { + let key = TypeKey::from_type(&syn::parse_quote!(::Item)); + assert_eq!(key.short_name().as_deref(), Some("Item")); + assert_eq!(key.ident().map(|i| i.to_string()).as_deref(), Some("Item")); + } + + /// A `::` inside a generic argument is not a path separator, and neither + /// angle counting nor the `->` in a bare-fn argument may make it look like + /// one. + #[test] + fn separators_inside_generic_arguments_are_not_path_separators() { + for (spec, expected) in [ + ("Vec", Some("Vec")), + ("Vec a::B>", Some("Vec")), + ("Vec>", Some("Vec")), + ] { + let key = TypeKey::from_type(&syn::parse_str(spec).expect("test shape")); + assert_eq!(key.short_name().as_deref(), expected, "on `{spec}`"); + } + } + + /// A name comes back out as the ident it names — `from_ident` is the inverse. + #[test] + fn ident_round_trips_through_from_ident() { + let ident = syn::Ident::new("ZKeyExpr", proc_macro2::Span::call_site()); + let key = TypeKey::from_ident(&ident); + assert_eq!(key.ident().as_ref(), Some(&ident)); + assert_eq!(key.short_name().as_deref(), Some("ZKeyExpr")); + } +} diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs index eba5dab9..94c8b5b7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/names.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/names.rs @@ -27,13 +27,7 @@ pub(crate) fn rust_short_name(key: &TypeKey) -> String { /// wrapper patterns including non-path shapes like `()` where there /// is no Kotlin short name to derive. pub(crate) fn rust_short_name_opt(key: &TypeKey) -> Option { - let ty = key.to_type(); - if let syn::Type::Path(tp) = &ty { - if let Some(last) = tp.path.segments.last() { - return Some(last.ident.to_string()); - } - } - None + key.short_name() } /// `VisitMut` that prefixes every bare single-segment `Type::Path` whose diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 1e46d539..e3525c18 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -487,16 +487,11 @@ impl Declarations { let Some(kotlin_fqn) = cfg.name_spec.as_ref().map(|s| self.fqn_of(s)) else { continue; }; - // Look up the syn::ItemEnum by the type-key's bare ident. - let ty = key.to_type(); - let Some(ident) = (if let syn::Type::Path(tp) = &ty { - tp.path.segments.last().map(|s| s.ident.clone()) - } else { - None - }) else { + // Look up the syn::ItemEnum by the type-key's own short name. + let Some(name) = key.short_name() else { continue; }; - let Some(item_enum) = registry.flat().enum_item(&ident) else { + let Some(item_enum) = registry.flat().enum_item(&name) else { continue; }; let (package, class_name) = match kotlin_fqn.rsplit_once('.') { @@ -548,8 +543,7 @@ impl Declarations { let Some(kotlin_fqn) = cfg.name_spec.as_ref().map(|s| self.fqn_of(s)) else { continue; }; - let ty = key.to_type(); - let Some(ident) = bare_path_ident(&ty) else { + let Some(ident) = key.ident() else { continue; }; // The sum as the MODEL holds it: its alternatives' payloads are @@ -891,15 +885,10 @@ impl Declarations { continue; }; - let ty = key.to_type(); - let Some(ident) = (if let syn::Type::Path(tp) = &ty { - tp.path.segments.last().map(|s| s.ident.clone()) - } else { - None - }) else { + let Some(name) = key.short_name() else { continue; }; - let Some(item_struct) = registry.flat().struct_type(&ident) else { + let Some(item_struct) = registry.flat().struct_type(&name) else { continue; }; diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index c9f3cc5b..1910f3c1 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -2332,8 +2332,7 @@ fn shape_notes( /// The `///` doc of the `#[prebindgen]` struct/enum behind a declared type /// key, when the item is indexed (a re-exported foreign type has none). pub(crate) fn source_item_doc(registry: &Registry, key: &TypeKey) -> Option { - let ident = bare_path_ident(&key.to_type())?; - let name = ident.to_string(); + let name = key.ident()?.to_string(); let attrs = registry .flat() .struct_type(&name) diff --git a/prebindgen/src/api/lang/jnigen/jni/symbols.rs b/prebindgen/src/api/lang/jnigen/jni/symbols.rs index 44354f79..1b50398b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/symbols.rs +++ b/prebindgen/src/api/lang/jnigen/jni/symbols.rs @@ -127,9 +127,7 @@ pub(crate) fn validate_symbols(ext: &Declarations, registry: &Registry .collect(); class_keys.sort_by_key(|k| k.as_str().to_string()); for key in class_keys { - let ident = match bare_path_ident(&key.to_type()) { - Some(i) => i, - None => continue, + let Some(ident) = key.ident() else { + continue; }; if let Some(s) = registry .flat() From c4fa23c42b72a26c521baca4756a079312194773 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 04:56:45 +0200 Subject: [PATCH 44/52] A key is only an identity (#291 stage D, closes #291) (#301) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * A key is only an identity (#291 stage D, closes #291) The last three readers, then the channel itself. `option_depth` peeled `Option<…>` tokens to count layers the model had already counted: `TypeKind::Optional` is produced for exactly `Option`, and `optional_inner()` names the layer. It takes the reading now, and its two callers hand over the readings they were already sitting next to. `KotlinMeta::value_rust_key` held a `TypeKey` and had exactly ONE reader, which immediately spent it on `to_type()`. It was never an identity here — only a detour through one — and both producers have the `syn::Type` in hand. It is `value_rust_type: Option` now, carrying the same canonical form it always yielded. The idempotence assertion in `typekey_normalizes_equivalent_spellings` re-keyed `to_type()`, which was really a claim about the parsed form a key kept beside its string. A key keeps no such thing; the next line's string round trip is the whole claim and it stays. Then: pub struct TypeKey { canon: std::rc::Rc, } `from_type` stops allocating the second `Rc`. `parse` still parses, to VALIDATE, and discards it. `Eq`/`Hash`/`Ord`/`Debug`/`Display` never read anything else, so nothing about identity or error text moves. What this closes is not a miscompilation — none was known. It is that the type system permitted a category of mistake: spell a type nobody classified. #280 sealed `TypeRef` so only the model may mint a reading, and a key that hands out tokens walked straight around that seal. The route from a key to syntax is `Conversions::reading` + `TypeRef::syntax`, and now it is the only one. 44 call sites to 0, across four PRs, with the generated output byte-identical at every step. Verified: 634 lib tests, `cargo test --all --all-features`, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, and covertest-kotlin's 49-section JVM harness. * Review: say what a missing reading means instead of asserting it cannot happen `c_domain_niches` carried a comment claiming every crossing key has a cell, next to a `filter_map` that would have silently dropped one if it did not. Review is right that a claim the code does not check is worse than no claim. It is not an `expect`, though. A crossing with no reading contributes no demand, and that is an ANSWER: the niche allocator reserves values no sibling conversion can produce, and a crossing the registry never entered has no conversion to produce one. So the arm is an explicit `0` — the same answer jnigen's twin at `conversion_domain_niches` already gives — and the reasoning lives in the code rather than in a claim a `filter_map` was quietly leaning on. Also from review: the `classify_return` comment still said the peeled type "comes straight off the stored key". It comes off a stored `syn::Type` now, and the fallback beside it is not a miss — the field is `None` exactly for plain values and arity-0 converters, which have no inner identity to peel to. Verified: 636 lib tests, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, covertest-kotlin 49/49. --- prebindgen/src/api/core/registry/key.rs | 48 ++++++++++++------- prebindgen/src/api/core/registry/tests.rs | 6 ++- prebindgen/src/api/lang/cbindgen/convert.rs | 37 +++++++++----- prebindgen/src/api/lang/jnigen/jni/builder.rs | 23 +++++---- prebindgen/src/api/lang/jnigen/jni/fn_plan.rs | 20 ++++---- .../src/api/lang/jnigen/jni/metadata.rs | 16 ++++--- .../src/api/lang/jnigen/jni/trait_impl.rs | 12 ++--- 7 files changed, 102 insertions(+), 60 deletions(-) diff --git a/prebindgen/src/api/core/registry/key.rs b/prebindgen/src/api/core/registry/key.rs index 7d8b35b4..58d59109 100644 --- a/prebindgen/src/api/core/registry/key.rs +++ b/prebindgen/src/api/core/registry/key.rs @@ -7,16 +7,32 @@ use quote::ToTokens; /// Canonical type-shape key: identity is the token string of the /// **normalized** type ([`crate::api::core::flat::spelling::normalize_type`] — /// group/paren unwrap, `crate::`/`self::` and std-prelude path reduction; -/// the complete equivalence rule set is documented there). The normalized -/// parsed form is kept alongside the string, so [`Self::to_type`] is an -/// infallible clone — no core invariant depends on serialize-then-reparse -/// round trips (issue #95). +/// the complete equivalence rule set is documented there). +/// +/// # A key is an identity, and nothing else +/// +/// It is what a table is indexed by. It is **not** a route to `syn::Type`: the +/// only way to reach a type's syntax is +/// [`Conversions::reading`](super::Conversions::reading) followed by +/// [`TypeRef::syntax`](crate::api::core::flat::TypeRef::syntax), because a +/// reading is what pairs a spelling with the classification that vouches for +/// it. +/// +/// This used to keep the parsed form beside the string and hand it out through +/// `to_type()`, which let any holder of a key produce tokens for a type the +/// model never classified — the same capability #280 sealed `TypeRef` against, +/// granted by the key itself. A caller that wants tokens now has to have gotten +/// them from somewhere that knows what they mean: the registry's reading, or +/// the declaration that wrote them (#291). +/// +/// What a key can still answer about itself is what it is **called** — +/// [`Self::as_str`], [`Self::ident`], [`Self::short_name`] — because a name is +/// not syntax. #[derive(Clone)] pub struct TypeKey { - /// Canonical token string — the identity `Eq`/`Hash` compare. + /// Canonical token string — the identity `Eq`/`Hash` compare, and the whole + /// of what a key is. canon: std::rc::Rc, - /// The normalized parsed form the string was rendered from. - ty: std::rc::Rc, } impl PartialEq for TypeKey { @@ -66,6 +82,10 @@ impl std::error::Error for TypeKeyParseError {} impl TypeKey { /// Build a key by parsing the input as a type and normalizing. + /// + /// The parse is kept for **validation** and then discarded: a key that + /// cannot be a type is a mistake worth reporting at the declaration, and a + /// key that can is still only its canonical string. pub fn parse(s: &str) -> Result { let ty: syn::Type = syn::parse_str(s).map_err(|error| TypeKeyParseError { input: s.to_string(), @@ -79,10 +99,11 @@ impl TypeKey { pub fn from_type(ty: &syn::Type) -> Self { // Off the shared reduction, so this key and the model's type index // cannot drift apart about what a type is called. - let t = crate::api::core::flat::canonical_type(ty); Self { - canon: t.to_token_stream().to_string().into(), - ty: std::rc::Rc::new(t), + canon: crate::api::core::flat::canonical_type(ty) + .to_token_stream() + .to_string() + .into(), } } @@ -97,17 +118,10 @@ impl TypeKey { &self.canon } - /// The normalized parsed form. Infallible — a clone of the stored type, - /// never a reparse. - pub fn to_type(&self) -> syn::Type { - (*self.ty).clone() - } - /// The bare item ident this key names — `Foo`, `a::Foo` → `Foo`, /// `a::Foo::Bar` → `Bar`; `None` when the **last** segment carries /// generic arguments (`Vec` names no bare item) or the key is not a /// path. - /// /// Matches [`bare_path_ident`](crate::api::core::types_util::bare_path_ident) /// on the same type, which is what /// `key_name_accessors_match_the_syn_walks` pins. diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index b46d34aa..c5f9e52d 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -479,9 +479,11 @@ fn typekey_equivalence_rules() { assert_ne!(k("std::ffi::CString"), k("CString")); assert_ne!(k("&Foo"), k("&'a Foo")); assert_ne!(k("Foo<'static>"), k("Foo")); - // Idempotence: re-keying a key's own type or string is the identity. + // Idempotence: re-keying a key's own string is the identity. The key IS the + // canonical string now, so that is the whole claim — there used to be a + // second assertion re-keying `to_type()`, which was really about the parsed + // form a key kept beside the string, and a key keeps no such thing (#291). let once = k("std::vec::Vec"); - assert_eq!(once, TypeKey::from_type(&once.to_type())); assert_eq!(once, k(once.as_str())); assert_eq!(once.as_str(), "Vec < Foo >"); } diff --git a/prebindgen/src/api/lang/cbindgen/convert.rs b/prebindgen/src/api/lang/cbindgen/convert.rs index 96ab6eaa..3d5f8253 100644 --- a/prebindgen/src/api/lang/cbindgen/convert.rs +++ b/prebindgen/src/api/lang/cbindgen/convert.rs @@ -8,8 +8,8 @@ impl CbindgenBuilder { let Some(domain) = &decl.domain else { continue }; let demand = [Direction::Input, Direction::Output] .into_iter() - .flat_map(|direction| registry.type_table(direction).keys()) - .map(|candidate| option_depth(candidate, &decl.key)) + .flat_map(|direction| registry.type_table(direction).values()) + .map(|cell| option_depth(&cell.subject, &decl.key)) .max() .unwrap_or(0); let ty = domain.ty(); @@ -281,10 +281,21 @@ impl CbindgenBuilder { let Some(domain) = &decl.domain else { return Niches::empty(); }; + // A crossing with no reading contributes no demand, and that is an + // answer rather than a gap being swallowed: the niche allocator is + // reserving values no SIBLING CONVERSION can produce, and a crossing + // the registry never entered has no conversion to produce one. Spelled + // as an explicit `0` — the same answer jnigen's twin gives — so the + // reasoning is in the code instead of in a claim that a `filter_map` + // silently relied on. let demand = registry .crossing_keys(direction) .iter() - .map(|candidate| option_depth(candidate, &decl.key)) + .map(|candidate| { + registry + .reading(candidate) + .map_or(0, |reading| option_depth(&reading, &decl.key)) + }) .max() .unwrap_or(0); Niches::from_slots( @@ -351,17 +362,21 @@ fn fn_ret(item: &syn::ItemFn) -> syn::Type { } } -fn option_depth(candidate: &TypeKey, target: &TypeKey) -> usize { - let mut ty = candidate.to_type(); +/// How many `Option<…>` layers `candidate` puts over `target`, or 0 if it is +/// not that type under any number of them. +/// +/// Counted off the **reading**. The optional layers are already what the model +/// says this type is — `TypeKind::Optional` is produced for exactly `Option` +/// — so peeling tokens to rediscover them was re-deriving the classification +/// the registry stored (#291). +fn option_depth(candidate: &crate::api::core::flat::TypeRef, target: &TypeKey) -> usize { + let mut reading = candidate; let mut depth = 0; - while is_option(&ty) { - let Some(inner) = first_type_arg(&ty) else { - return 0; - }; - ty = inner; + while let Some(inner) = reading.optional_inner() { + reading = inner; depth += 1; } - if TypeKey::from_type(&ty) == *target { + if reading.key() == *target { depth } else { 0 diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 2621fbb4..063ebcb0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -1790,7 +1790,7 @@ impl Declarations { pub(crate) fn framework_meta(&self, kotlin_name: Option) -> KotlinMeta { KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, projection: None, } } @@ -1931,7 +1931,7 @@ impl Declarations { niches, metadata: KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, // Terminal: body produces the wire directly, no inner // converter composed, so no handle to carry. projection: None, @@ -1954,7 +1954,7 @@ impl Declarations { let mut pre_stages = vec![stage]; pre_stages.extend(inner.pre_stages.iter().cloned()); let kotlin_name = inner.metadata.kotlin_name.clone(); - let value_rust_key = None; + let value_rust_type = None; let (niches, sentinels) = self.conversion_domain_niches( &key, registry, @@ -1963,7 +1963,7 @@ impl Declarations { ); let mut metadata = KotlinMeta { kotlin_name, - value_rust_key, + value_rust_type, projection: inner.metadata.projection.clone(), }; Self::attach_domain_sentinels(&mut metadata, sentinels); @@ -2054,11 +2054,16 @@ impl Declarations { match inner { None if is_self || is_wire_type(&ty) => { // Terminal: `ty` is the wire; the body produces it from `outer`. - let (kotlin_name, value_rust_key) = if let Some(a0) = arg0 { + let (kotlin_name, value_rust_type) = if let Some(a0) = arg0 { registry .reading_of(a0) .and_then(|tr| registry.output_entry(&tr)) - .map(|e| (e.metadata.kotlin_name.clone(), Some(TypeKey::from_type(a0)))) + .map(|e| { + ( + e.metadata.kotlin_name.clone(), + Some(crate::api::core::flat::canonical_type(a0)), + ) + }) .unwrap_or((None, None)) } else { let kn = self @@ -2081,7 +2086,7 @@ impl Declarations { niches, metadata: KotlinMeta { kotlin_name, - value_rust_key, + value_rust_type, // Terminal: body produces the wire directly, no inner // converter composed, so no handle to carry. projection: None, @@ -2099,7 +2104,7 @@ impl Declarations { let mut pre_stages = vec![stage]; pre_stages.extend(inner.pre_stages.iter().cloned()); let kotlin_name = inner.metadata.kotlin_name.clone(); - let value_rust_key = arg0.map(TypeKey::from_type); + let value_rust_type = arg0.map(crate::api::core::flat::canonical_type); let (niches, sentinels) = match arg0 { None => self.conversion_domain_niches( &key, @@ -2111,7 +2116,7 @@ impl Declarations { }; let mut metadata = KotlinMeta { kotlin_name, - value_rust_key, + value_rust_type, projection: inner.metadata.projection.clone(), }; Self::attach_domain_sentinels(&mut metadata, sentinels); diff --git a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs index 65ae13a0..6ab87adf 100644 --- a/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/fn_plan.rs @@ -185,10 +185,10 @@ pub(crate) struct ValueOutputPlan { pub wire_ty: syn::Type, /// Kotlin surface classification over the **declared** return /// (`convert_out_ty` for a convert, else `f.sig.output` — not - /// `target_ty`: the Kotlin error peel rides `value_rust_key`). + /// `target_ty`: the Kotlin error peel rides `value_rust_type`). pub surface: ReturnSurface, /// `enum_class` / `Option` probes over the canonical - /// (`value_rust_key`-peeled) declared return. The extern decl uses them + /// (`value_rust_type`-peeled) declared return. The extern decl uses them /// raw; the wrapper surface masks them with `!is_convert` (the historical /// `unfold.is_none()` gate). pub is_enum: bool, @@ -781,7 +781,7 @@ fn build_output( // The Kotlin surface classifies the DECLARED return — `convert_out_ty` // for a convert, else the signature's own output. (Not `target_ty`: the - // Kotlin error peel rides the entry's `value_rust_key`, so the full + // Kotlin error peel rides the entry's `value_rust_type`, so the full // `Result` type is looked up as written.) let ret_decl: syn::ReturnType = if is_convert { syn::parse_quote!(-> #target_ty) @@ -807,7 +807,7 @@ fn build_output( impl ReturnSurface { /// Classify a declared return type. Returns the surface plus the - /// canonical (`value_rust_key`-peeled) type the enum probes run over — + /// canonical (`value_rust_type`-peeled) type the enum probes run over — /// the single peel that subsumed both `classify_return`'s inline peel /// and the former `canonical_return_ty`. pub fn classify( @@ -824,13 +824,15 @@ impl ReturnSurface { .and_then(|tr| registry.output_entry(&tr)) .map(|e| e.metadata.clone()); // Unit returns (incl. `ZResult<()>`, whose inner identity rides - // `value_rust_key`) declare no Kotlin return type. The peeled type - // comes straight off the stored key — no reparse, no silent - // fallback. + // `value_rust_type`) declare no Kotlin return type. The peeled type is + // the one the converter's metadata stored — a canonical `syn::Type`, + // so nothing is rebuilt here. Falling back to the declared return is + // not a miss: `value_rust_type` is `None` exactly for plain values and + // arity-0 converters, which have no inner identity to peel to. let canonical: syn::Type = outer_meta .as_ref() - .and_then(|m| m.value_rust_key.as_ref()) - .map(TypeKey::to_type) + .and_then(|m| m.value_rust_type.as_ref()) + .cloned() .unwrap_or_else(|| ty.clone()); if crate::api::lang::jnigen::util::is_unit(&canonical) { return (Self::Unit, canonical); diff --git a/prebindgen/src/api/lang/jnigen/jni/metadata.rs b/prebindgen/src/api/lang/jnigen/jni/metadata.rs index 16f12d2c..b49f7db6 100644 --- a/prebindgen/src/api/lang/jnigen/jni/metadata.rs +++ b/prebindgen/src/api/lang/jnigen/jni/metadata.rs @@ -99,15 +99,19 @@ pub struct KotlinMeta { pub kotlin_name: Option, /// For wrapper converters whose Kotlin projection is the *inner* /// type's projection (e.g. `ZResult` → `Publisher`), - /// this carries the inner Rust type's canonical key so downstream + /// this carries the inner Rust type — canonicalized — so downstream /// emitters (typed-handle constructor lookup in `classify_return`) can find /// the wrapped value's identity without baking in any specific shape. - /// Populated with `args[0]`'s canonical key for arity-1 wrappers, and + /// Populated with `args[0]`'s canonical type for arity-1 wrappers, and /// inherited by the built-in `Option<_>` / `Vec<_>` / `&_` wrappers from /// their inner type's metadata. `None` for plain values and arity-0 - /// converters. A typed key — readers get the type via `to_type()`, no - /// reparse. - pub value_rust_key: Option, + /// converters. + /// + /// **A type, because that is the only thing anyone reads it as.** It held a + /// `TypeKey` and had exactly one reader, which immediately spent it on + /// `to_type()` — so the key was never an identity here, only a detour + /// through one. The producers have the `syn::Type` in hand (#291). + pub value_rust_type: Option, /// Present iff this (possibly wrapped) value is an opaque native handle. Set /// at the opaque-handle leaf and folded outward by the `&_` / `Option<_>` /// wrappers and the `lookup_*` composed branches. The single source of truth @@ -119,7 +123,7 @@ impl KotlinMeta { pub fn from_name(name: impl Into) -> Self { Self { kotlin_name: Some(kt::KtType::cls(name)), - value_rust_key: None, + value_rust_type: None, projection: None, } } diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index 2ff08d61..cfdaed5e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -929,7 +929,7 @@ impl Declarations { niches: inner.niches.clone(), metadata: KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, projection, }, }) @@ -1001,7 +1001,7 @@ impl Declarations { niches: Niches::empty(), metadata: KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, projection, }, }) @@ -1070,7 +1070,7 @@ impl Declarations { niches: Niches::empty(), metadata: KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, projection: None, }, }) @@ -1142,7 +1142,7 @@ impl Declarations { niches: Niches::empty(), metadata: KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, projection, }, }); @@ -2543,7 +2543,7 @@ impl Declarations { niches, metadata: KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, projection, }, }); @@ -2610,7 +2610,7 @@ impl Declarations { niches, metadata: KotlinMeta { kotlin_name, - value_rust_key: None, + value_rust_type: None, projection, }, }) From 7ec41219e63bf897b88e63d17ca64d30417928fd Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 08:52:56 +0200 Subject: [PATCH 45/52] jnigen: the jobject decoder reads the element, and finds a wrong descriptor (#289) (#302) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * jnigen: the jobject decoder reads the element, and finds a wrong descriptor `struct_input_body` took the `syn::ItemStruct` and walked `syn::Fields::Named`, while its caller was already holding the `flat::Struct` that `struct_type()` handed back — and `flat::Field::ty` is a `TypeRef` the model classified when it parsed the item. So the whole-object `.jobject_input()` decoder re-derived by token what the element had already answered, four times per field. It takes `&flat::Struct` now and asks the optional layer ONCE, the way `build_flat_struct_node` has since #294. The name comes off `TypeKind::Named`, "is it a run" off `sequence_elem()`, the enum probe off `is_kotlin_enum_reading` — each with the precedent #288 set in `struct_plan.rs`. **This one is not output-preserving, and that is the finding.** `WrappedFields { boxed: Box>, plain: Option }` is the fixture #294 added because those two fields MEAN the same thing. Kotlin declares both `Long?`. The old emitter asked JNI for: "boxed" -> Ljava/lang/Object; "plain" -> Ljava/lang/Long; `option_inner_type` reads the last path segment, so `Box>` answered "not optional", the descriptor chain fell through to its `Object` fallback, and the twins diverged — the #273 signature exactly. `GetFieldID` requires the field's EXACT declared descriptor, so that lookup throws `NoSuchFieldError`. The golden now says `Ljava/lang/Long;` for both. It was never observed because `JObject_to_WrappedFields_*` is emitted and never called — this fixture crosses via the flatten path, and its siblings appear seven times each. Any `.jobject_input()` data class with a wrapped optional field would have hit it. Two residuals #294 left inside the converted fn go too: an `is_kotlin_enum` on a spelling three lines from the reading, and a `reading_of` re-looking-up a `&TypeRef` already in hand. `FlatFieldNode::Value::direct_handle` becomes `Option>` carrying the handle target the plan peeled, instead of a `bool` beside a spelling the renderer re-peeled with the same last-segment test — a `Box>` handle field would have been handed the wrong `Box::from_raw` target. The node stays token-carrying; it is an emission IR, and the answer travels from where the reading was. Ledger 117 -> 116, jnigen census 9 -> 4. `types_util` does NOT move and was never going to: `option_inner_type` keeps callers in struct_out, trait_impl, fold, fn_plan and wrapper. The remaining four are the sum side, which needs `Type::Variant` rather than the `syn::ItemEnum` `enum_item` hands back — the rest of #289. Verified: 636 lib tests, clippy on 1.85.0 and stable, fmt, covertest-kotlin 49/49, and regen-check clean apart from the one diagnosed descriptor. * Review: a struct keeps its own constructor delimiters The `syn::Fields::Named` guard this walk replaced refused a unit struct by returning `None`. The per-field name check that replaced it cannot: an empty struct has no field to refuse, so the loop fell straight through to a hard-coded braced initializer and emitted `myflat::Unit {}` for `pub struct Unit;`. That is not Rust. `flat::Struct` does not record whether its fields were named — that is spelling — and `Struct::spell` is the one place those delimiters are chosen. It is the exact dual of the `Alternative::spell` the sum decoder uses so `enum E { B() }` is written `E::B()`; I used the model's helper there and hand-rolled the braces here, which is the whole defect. `empty_structs_keep_their_own_constructor_delimiters` covers it, and fails without the fix. A tuple struct is deliberately absent from it: the model reads one as an `Extern`, so `Flat::struct_type` answers `None` and it never reaches this decoder — declaring one as a `jobject_input` data class fails to resolve instead. Goldens byte-identical: no in-tree example declares a payload-less jobject_input data class, which is why the shape had no signal. Verified: 637 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild, covertest-kotlin 49/49. --- .../src/generated_bindings.rs | 2 +- prebindgen/src/api/core/flat/boundary.ledger | 4 +- .../api/lang/jnigen/jni/emit/flat_input.rs | 162 ++++++++++-------- .../src/api/lang/jnigen/jni/tests/values.rs | 73 ++++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 2 +- prebindgen/src/api/lang/jnigen/mod.rs | 12 +- 6 files changed, 178 insertions(+), 77 deletions(-) diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index d4a9abcf..8eb7c7f9 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -3570,7 +3570,7 @@ pub(crate) unsafe fn JObject_to_WrappedFields_f14f08c1<'env, 'v>( >>::from(format!("WrappedFields.id: {}", e)))? as _; let id = jlong_to_i64_fbf9a9bc(env, &__id_raw)?; let __boxed_raw: jni::objects::JObject = env - .get_field(v, "boxed", "Ljava/lang/Object;") + .get_field(v, "boxed", "Ljava/lang/Long;") .and_then(|val| val.l()) .map_err(|e| <__JniErr as ::core::convert::From< String, diff --git a/prebindgen/src/api/core/flat/boundary.ledger b/prebindgen/src/api/core/flat/boundary.ledger index 82b1db05..9228ffc4 100644 --- a/prebindgen/src/api/core/flat/boundary.ledger +++ b/prebindgen/src/api/core/flat/boundary.ledger @@ -54,7 +54,7 @@ 4 api/lang/jnigen/jni/builder.rs 4 api/lang/jnigen/jni/emit/convert.rs 2 api/lang/jnigen/jni/emit/delivery.rs -5 api/lang/jnigen/jni/emit/flat_input.rs +4 api/lang/jnigen/jni/emit/flat_input.rs 16 api/lang/jnigen/jni/emit/names.rs 11 api/lang/jnigen/jni/emit/wrapper.rs 3 api/lang/jnigen/jni/fold.rs @@ -68,4 +68,4 @@ 2 api/lang/jnigen/jni/wire_access.rs 2 api/lang/jnigen/util.rs -# total: 117 +# total: 116 diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index c5d4ad6d..cd892b0b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -12,34 +12,46 @@ use crate::api::{ lang::jnigen::jni::trait_impl::{build_through_erased_wrappers, build_through_wrappers}, }; +/// Takes the **element**, not the `syn::ItemStruct` it was parsed from (#289): +/// `flat::Field::ty` is already a `TypeRef`, so every peel below is the model's +/// answer rather than a last-path-segment test on tokens that had a reading one +/// level up. Same move `build_flat_struct_node` made for the flatten path; this +/// is the whole-object `.jobject_input()` decoder. pub(crate) fn struct_input_body( ext: &Declarations, - s: &syn::ItemStruct, + s: &flat::Struct, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { - let struct_name = s.ident.to_string(); - let struct_module = struct_module_path(ext, registry, &s.ident); - let struct_ident = &s.ident; - - let syn::Fields::Named(named) = &s.fields else { - return None; - }; + let struct_name = s.name.to_string(); + let struct_module = struct_module_path(ext, registry, &s.name); + let struct_ident = &s.name; let mut field_preludes: Vec = Vec::new(); let mut field_init: Vec = Vec::new(); - for field in &named.named { - let fname_ident = field.ident.as_ref().unwrap().clone(); + for field in &s.fields { + // A positional field has no name to read a JVM slot by, which is what + // the `syn::Fields::Named` guard used to say one level up. Said per + // field now, because the element models a field list rather than a + // `syn::Fields` shape. + let fname_ident = field.name.clone()?; let fname = fname_ident.to_string(); let camel = mangle_kotlin_ident(&snake_to_camel(&fname)); let err_prefix = format!("{struct_name}.{camel}: {{}}"); let raw_ident = format_ident!("__{}_raw", fname_ident); // Defer if any field's input converter isn't resolved yet — the - // fixed-point loop will retry on the next iteration. - let field_entry = registry - .reading_of(&field.ty) - .and_then(|tr| registry.input_entry(&tr))?; + // fixed-point loop will retry on the next iteration. The field's own + // reading straight to its entry — the `reading_of` hop only ever + // recovered what the field already carried. + let field_entry = registry.input_entry(&field.ty)?; + // The optional layer off the MODEL, asked once and reused: every site + // below that wants "is this field optional" reads this, so they cannot + // disagree with each other the way four independent path-segment tests + // could (#273). `option_inner_type` compared the last path segment, so + // a field spelled `Box>` answered "not optional" here. + let field_optional = field.ty.optional_inner().is_some(); + let inner = field.ty.optional_inner().unwrap_or(&field.ty); let field_wire = field_entry.destination.clone(); // The field's COMPLETE decode, stages included — a `convert!` type // reaches its Rust value through them (`jlong -> u64 -> Duration`). @@ -62,13 +74,8 @@ pub(crate) fn struct_input_body( // converter would yield `OwnedObject`, which can't // populate an owned field. `Option<_>` handle fields keep // the niche-aware converter (jlong 0 ⇒ `None`). - let field_ty = &field.ty; - let field_is_option = matches!( - field_ty, - syn::Type::Path(p) if p.path.segments.last() - .map(|s| s.ident == "Option").unwrap_or(false) - ); - let decode = if field_is_option { + let field_ty = field.ty.syntax(); + let decode = if field_optional { quote! { let #fname_ident = #field_conv; } } else { quote! { @@ -102,15 +109,13 @@ pub(crate) fn struct_input_body( }); } ProjectionKind::Unsigned64 => { - if let Some(inner_ty) = option_inner_type(&field.ty) { + if field_optional { let niche = matches!( proj.strategy, FoldStrategy::Optional(NullableKind::Niche, _) ); let inner_conv = composed_entry_decode( - registry - .reading_of(&inner_ty) - .and_then(|tr| registry.input_entry(&tr))?, + registry.input_entry(inner)?, &raw_ident, &fname_ident, ); @@ -163,22 +168,22 @@ pub(crate) fn struct_input_body( // its `value` getter (`getValue()I`); a null object is the `None` arm. // (The generic converters can't be used here: the bare-enum one is // jint-keyed, the `Option` one unboxes `java.lang.Integer`.) - let f_inner = option_inner_type(&field.ty).unwrap_or_else(|| field.ty.clone()); - if ext.is_kotlin_enum(&f_inner) { - if let Some(fqn) = bare_path_ident(&f_inner) - .and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n))) - .map(|v| v.to_string()) + if ext.is_kotlin_enum_reading(inner) { + // The NAME off the classification, not off the last path segment: + // `Box` IS `T` here, and taking the spelling apart would answer + // about the wrapper. + if let Some(fqn) = match inner.kind() { + flat::TypeKind::Named { id } => id.ident(), + _ => None, + } + .and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n))) + .map(|v| v.to_string()) { let sig = format!("L{};", fqn.replace('.', "/")); - let inner_conv = composed_entry_decode( - registry - .reading_of(&f_inner) - .and_then(|tr| registry.input_entry(&tr))?, - &raw_ident, - &fname_ident, - ); + let inner_conv = + composed_entry_decode(registry.input_entry(inner)?, &raw_ident, &fname_ident); let tmp_ident = format_ident!("__{}_jobj", fname_ident); - let decode = if option_inner_type(&field.ty).is_some() { + let decode = if field_optional { quote! { let #fname_ident = if #tmp_ident.is_null() { ::core::option::Option::None @@ -234,10 +239,8 @@ pub(crate) fn struct_input_body( // Kotlin class for a nested data-class field (Option-stripped // — a nullable field keeps the same descriptor), `List` for a // `Vec` field. - let slot_ty = option_inner_type(&field.ty).unwrap_or_else(|| field.ty.clone()); let sig = registry - .reading_of(&slot_ty) - .and_then(|tr| registry.input_entry(&tr)) + .input_entry(inner) .and_then(|e| jni_field_access(&e.destination)) .and_then(|(sig, _, is_obj)| { if is_obj { @@ -247,13 +250,22 @@ pub(crate) fn struct_input_body( } }) .or_else(|| { - bare_path_ident(&slot_ty).and_then(|name| { + // The NAME off the classification, not off the last + // path segment. + match inner.kind() { + flat::TypeKind::Named { id } => id.ident(), + _ => None, + } + .and_then(|name| { ext.kotlin_fqn(&TypeKey::from_ident(&name)) .map(|v| format!("L{};", v.replace('.', "/"))) }) }) .or_else(|| { - if pat_match_top(&slot_ty, "Vec") { + // A run of values is what `kind` says it is. + // `pat_match_top(.., "Vec")` compared the last path + // segment, so a `Box>` answered false. + if inner.sequence_elem().is_some() { Some("Ljava/util/List;".to_string()) } else { None @@ -271,9 +283,18 @@ pub(crate) fn struct_input_body( field_init.push(quote!(#fname_ident)); } + // The struct's OWN delimiters, from the one place that chooses them. + // `flat::Struct` does not record whether its fields were named — that is + // spelling — so hard-coding braces here emitted `Unit {}` for + // `struct Unit;` and `Empty {}` for `struct Empty()`, neither of which is + // Rust. The `syn::Fields::Named` guard this walk replaced happened to + // refuse both; the per-field name check cannot, because an empty struct + // has no field to refuse. `Struct::spell` is the dual of the + // `Alternative::spell` the sum decoder uses for exactly this. + let ctor = s.spell(quote!(#struct_module::#struct_ident), &field_init); let body: syn::Expr = syn::parse_quote!({ #(#field_preludes)* - #struct_module::#struct_ident { #(#field_init),* } + #ctor }); Some((syn::parse_quote!(jni::objects::JObject), body)) } @@ -712,7 +733,17 @@ pub(crate) enum FlatFieldNode { field: syn::Ident, value_leaf: usize, present_leaf: Option, - direct_handle: bool, + /// `Some(target)` iff this field crosses as a raw handle jlong, where + /// `target` is the type the `Box` points at — the field's own type with + /// its optional layer peeled, **taken off the model at plan time**. + /// + /// Paired rather than a `bool` beside a spelling the renderer re-peels: + /// `option_inner_type` compared the last path segment, so a field + /// spelled `Box>` would have handed `Box::from_raw` the wrong + /// target. There is no reading here to ask — `FlatFieldNode` is an + /// emission IR and tokens are what it is for — so the answer travels + /// from where the reading was (#289). + direct_handle: Option>, optional_handle: bool, rust_ty: Box, /// The transparent wrappers this field's spelling adds over its @@ -1486,7 +1517,6 @@ fn build_flat_struct_node( // Nullable primitive/enum with no niche: keep the allocation-free // `(present, value)` representation at every recursion depth. if let Some(inner_reading) = field.ty.optional_inner() { - let inner_ty = inner_reading.syntax().clone(); if inner_reading.borrow_target().is_none() { if let Some(inner) = registry.input_entry(inner_reading) { if let Some(prim) = JniPrim::from_wire(&inner.destination) { @@ -1500,7 +1530,7 @@ fn build_flat_struct_node( format!("{field_ref} != null"), Some(fident.clone()), ); - let value_access = if ext.is_kotlin_enum(&inner_ty) { + let value_access = if ext.is_kotlin_enum_reading(inner_reading) { format!("{field_ref}?.value ?: {}", prim.kotlin_zero()) } else { format!("{field_ref} ?: {}", prim.kotlin_zero()) @@ -1517,7 +1547,7 @@ fn build_flat_struct_node( field: fident, value_leaf: value_index, present_leaf: Some(present_index), - direct_handle: false, + direct_handle: None, optional_handle: false, rust_ty: Box::new(field.ty.syntax().clone()), wrappers: field.ty.erased_wrappers(), @@ -1537,21 +1567,18 @@ fn build_flat_struct_node( // provides a niche already have a primitive destination and stay // a single leaf below. if proj.kind == ProjectionKind::Unsigned64 { - if let Some(inner_ty) = field.ty.optional_inner().map(|t| t.syntax().clone()) { + if let Some(inner_reading) = field.ty.optional_inner() { if JniPrim::from_wire(&fentry.destination).is_none() { - let inner = registry - .reading_of(&inner_ty) - .and_then(|tr| registry.input_entry(&tr)) - .ok_or_else(|| { - flat_error( - root, - &path, - format!( - "unsigned field representation `{}` has no input converter", - TypeKey::from_type(&inner_ty) - ), - ) - })?; + let inner = registry.input_entry(inner_reading).ok_or_else(|| { + flat_error( + root, + &path, + format!( + "unsigned field representation `{}` has no input converter", + inner_reading.key() + ), + ) + })?; let present_index = push_present_leaf( leaves, &format!("{child_native}_present"), @@ -1570,7 +1597,7 @@ fn build_flat_struct_node( field: fident, value_leaf: value_index, present_leaf: Some(present_index), - direct_handle: false, + direct_handle: None, optional_handle: false, rust_ty: Box::new(field.ty.syntax().clone()), wrappers: field.ty.erased_wrappers(), @@ -1600,7 +1627,7 @@ fn build_flat_struct_node( field: fident, value_leaf: value_index, present_leaf: None, - direct_handle: true, + direct_handle: Some(Box::new(nested.syntax().clone())), optional_handle, rust_ty: Box::new(field.ty.syntax().clone()), wrappers: field.ty.erased_wrappers(), @@ -1631,7 +1658,7 @@ fn build_flat_struct_node( field: fident, value_leaf: value_index, present_leaf: None, - direct_handle: false, + direct_handle: None, optional_handle: false, rust_ty: Box::new(field.ty.syntax().clone()), wrappers: field.ty.erased_wrappers(), @@ -1678,7 +1705,7 @@ fn build_flat_struct_node( field: fident, value_leaf: value_index, present_leaf: None, - direct_handle: false, + direct_handle: None, optional_handle: false, rust_ty: Box::new(field.ty.syntax().clone()), wrappers: field.ty.erased_wrappers(), @@ -1887,8 +1914,7 @@ fn render_flat_struct_node( build_through_wrappers(wrappers, e) .expect("a field spelling the plan accepted is buildable") }; - if *direct_handle { - let target = option_inner_type(rust_ty).unwrap_or_else(|| (**rust_ty).clone()); + if let Some(target) = direct_handle { if *optional_handle { let gated = wrap(quote! { if #wire == 0 { diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs index 6cac249c..cdc3698c 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs @@ -715,6 +715,79 @@ fn jobject_input_is_an_explicit_hybrid_leaf_escape_hatch() { assert!(generation.report().contains("input `JObject` opt-in")); } +/// A payload-less struct keeps its own delimiters through the `.jobject_input()` +/// decoder. +/// +/// The decoder walks `flat::Struct::fields`, and the element does not record +/// whether the fields were named — that is spelling. So the braced initializer +/// this used to hard-code emitted `myflat::Unit {}` for `struct Unit;`, which is +/// not Rust. The `syn::Fields::Named` guard the walk replaced happened to refuse +/// it by returning `None`; the per-field name check cannot, because an empty +/// struct has no field to refuse. +/// +/// A tuple struct is absent because it cannot reach here at all: the model +/// reads one as an `Extern`, so `Flat::struct_type` answers `None` for it. +/// +/// `Struct::spell` is the one place those delimiters are chosen — the dual of +/// the `Alternative::spell` the sum decoder uses for `E::B()`. +#[test] +fn empty_structs_keep_their_own_constructor_delimiters() { + let loc = myflat_loc(); + let items = vec![ + ( + syn::Item::Struct(syn::parse_quote!( + pub struct Unit; + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct EmptyNamed {} + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn take_empties(a: Unit, c: EmptyNamed) -> i64 { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::data_class!(Unit).jobject_input()) + .class(crate::data_class!(EmptyNamed).jobject_input()) + .fun(crate::fun!(take_empties)), + ); + let dir = unique_test_dir("jnigen_empty_struct_delimiters"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let generation = jni.build_with(registry).expect("resolve"); + let rust = std::fs::read_to_string(generation.write_rust(dir.join("gen.rs")).unwrap()).unwrap(); + let rc: String = rust.split_whitespace().collect(); + + // Each shape gets the delimiters Rust demands for it, and none gets braces + // it cannot have. + assert!( + rc.contains("myflat::Unit)") || rc.contains("myflat::Unit}"), + "unit struct must be constructed bare, got:\n{rust}" + ); + assert!( + !rc.contains("myflat::Unit{}"), + "unit struct must not take braces:\n{rust}" + ); + assert!( + rc.contains("myflat::EmptyNamed{}"), + "empty named struct keeps its braces:\n{rust}" + ); +} + #[test] fn recursive_flattened_owned_handles_join_lock_and_consume_scaffold() { let loc = myflat_loc(); diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index cfdaed5e..f9511e78 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -2084,7 +2084,7 @@ impl Declarations { } } if let Some(s) = registry.flat().struct_type(&name) { - let (wire, body) = struct_input_body(self, &s.origin.syntax, registry)?; + let (wire, body) = struct_input_body(self, s, registry)?; let niches = default_niches_for_wire(&wire); // Auto-generated struct: the value-context Kotlin name is // whatever the user pinned via `data_class`. If diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index 3e9c23b9..a3532d0c 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -114,11 +114,13 @@ mod spelling_census { /// `(file, call count)` — every `.rs` under `api/lang/jnigen`, checked /// against the directory tree so a new module cannot sit outside the census. const CENSUS: &[(&str, usize)] = &[ - // 18 -> 9 (#289): `build_flat_struct_node` takes `flat::Struct` and - // peels its fields off the model. What remains is `struct_input_body` / - // `sum_input_body`, the `.jobject_input()` decoders — a separate walk - // over `syn::Fields`, and the rest of #289. - ("jni/emit/flat_input.rs", 9), + // 18 -> 9 -> 4 (#289): `build_flat_struct_node` took `flat::Struct` + // first, then `struct_input_body` — the whole-object `.jobject_input()` + // decoder — did the same. What remains is the SUM side: + // `sum_input_body` and the `read_kotlin_property` helper it alone + // calls, which need `Type::Variant` rather than the `syn::ItemEnum` + // `Flat::enum_item` hands back. + ("jni/emit/flat_input.rs", 4), ("jni/emit/struct_out.rs", 2), ("jni/emit/wrapper.rs", 2), // From 15daa1ddc98d21fadff807ac205a54b295404560 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 08:53:28 +0200 Subject: [PATCH 46/52] jnigen: the sum decoder reads the element too (#289) (#303) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sum_input_body` took the `syn::ItemEnum` and ran two zips to get back to per-field types: a `SumSpec` derived from the item, paired against the item it was derived from. `Alternative` already IS that pairing — name, index, and a `Vec` whose `ty` is a `TypeRef` — so both zips and the `SumSpec` go, and the payload reads ask the model. It needs a different accessor to get there. `Flat::enum_item` hands back only the `syn::ItemEnum`, deliberately: its own doc says a consumer that acts on the Variant/Enum distinction should ask `declared_type`. Both `sum_input_body` and `build_flat_sum_field` do that now and match `Type::Variant`. The constructor's delimiters come from `Alternative::spell`, which is the one place they are chosen. That is not a tidy-up: `Alternative::is_empty` is the GROUP question — `B`, `B()` and `B {}` are all empty by it — and Rust demands the delimiters wherever the last two are named, so a three-arm `syn::Fields` match was the only thing standing in for a helper the model already owns. `Field::bind` shapes each init the same way. `sum_field_prop_name` takes a `&syn::Member` instead of a `&SumField`. The member is the whole of what the name depends on, so a caller holding a `flat::Field` asks `Field::member()` and a caller holding a `SumField` reads its own — one derivation for both, rather than a second convention that could drift from the sealed-interface emitter's. **flat_input.rs is now at zero on every count.** No `option_inner_type`, no `reading_of`, no `bare_path_ident`, no `pat_match_top`, no `SumSpec`, no `syn::Fields` — the file comes off the spelling census (4 -> 0, and 18 -> 0 across #294 + #302 + this). The boundary ledger does NOT move, and should not: its four remaining entries in this file classify `entry.destination`, a wire type the adapter itself produced, which is legitimately its business rather than a reading it should have asked for. `SumSpec` stays for now — it still has callers in `struct_plan`, `kotlin_emit` and `sum_out`, and retiring it crate-wide would put the Kotlin emitters and the sum OUTPUT path in a PR about input decoding. It owns one thing the model does not, the leaf-naming convention, so that has to be rehomed rather than deleted. Verified: 636 lib tests, clippy on 1.85.0 and stable, fmt, regen-check BYTE-IDENTICAL after a forced rebuild, covertest-kotlin 49/49. --- .../api/lang/jnigen/jni/emit/flat_input.rs | 149 ++++++++++-------- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 2 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 11 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 6 +- prebindgen/src/api/lang/jnigen/mod.rs | 12 +- 5 files changed, 101 insertions(+), 79 deletions(-) diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index cd892b0b..6f9c95fc 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -315,26 +315,28 @@ pub(crate) fn struct_input_body( /// design's rejected-alternative note about per-crossing JVM objects. Reading /// one field out of a `JObject` the caller already handed us costs nothing /// extra, so the asymmetry is real rather than an oversight. +/// Takes the **element**, not the `syn::ItemEnum` it was parsed from (#289): +/// `Alternative::fields` carries a `TypeRef` per payload, so the property read +/// below asks the model instead of peeling tokens. It also retires the two zips +/// this used to run — a `SumSpec` derived from the item, paired back against the +/// item it came from — because `Alternative` already is that pairing. pub(crate) fn sum_input_body( ext: &Declarations, - e: &syn::ItemEnum, + v: &flat::Variant, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Expr)> { - use crate::api::core::types_util::SumSpec; - - let key = TypeKey::from_ident(&e.ident); + let key = TypeKey::from_ident(&v.name); let cfg = ext.types.get(&key)?; let sum_cfg = cfg.sum()?; let iface_fqn = cfg.name_spec.as_ref().map(|s| ext.fqn_of(s))?; let iface_path = iface_fqn.replace('.', "/"); - let source_module = ext.fn_module(registry, &e.ident); - let enum_ident = &e.ident; - let enum_name = e.ident.to_string(); + let source_module = ext.fn_module(registry, &v.name); + let enum_ident = &v.name; + let enum_name = v.name.to_string(); - let spec = SumSpec::from_item_enum(e); let mut arms: Vec = Vec::new(); - for (v, item_variant) in spec.variants.iter().zip(&e.variants) { - let vident = &v.ident; + for alt in &v.alternatives { + let vident = &alt.name; let kotlin_name = ext.sum_variant_class_name(sum_cfg, vident); // A variant class is NESTED in the interface, so its JVM binary name // is `Outer$Variant`. @@ -342,8 +344,9 @@ pub(crate) fn sum_input_body( let mut preludes: Vec = Vec::new(); let mut inits: Vec = Vec::new(); - for (f, item_field) in v.fields.iter().zip(item_variant.fields.iter()) { - let prop = crate::api::lang::jnigen::jni::struct_plan::sum_field_prop_name(f); + for field in &alt.fields { + let prop = + crate::api::lang::jnigen::jni::struct_plan::sum_field_prop_name(&field.member()); let bind = format_ident!("__p_{}", prop); let err_prefix = format!("{enum_name}.{kotlin_name}.{prop}: {{}}"); let (pre, value) = read_kotlin_property( @@ -351,25 +354,19 @@ pub(crate) fn sum_input_body( registry, "e!(__obj), &prop, - &item_field.ty, + &field.ty, &bind, &err_prefix, )?; preludes.push(pre); - match &f.member { - syn::Member::Named(n) => inits.push(quote!(#n: #value)), - syn::Member::Unnamed(_) => inits.push(quote!(#value)), - } + inits.push(field.bind(&value)); } - let ctor = match item_variant.fields { - syn::Fields::Unit => quote!(#source_module::#enum_ident::#vident), - syn::Fields::Named(_) => { - quote!(#source_module::#enum_ident::#vident { #(#inits),* }) - } - syn::Fields::Unnamed(_) => { - quote!(#source_module::#enum_ident::#vident(#(#inits),*)) - } - }; + // The alternative's OWN delimiters, from the one place that chooses + // them. `B()` carries no payload and still must be written `E::B()` — + // a three-arm `syn::Fields` match here would have had to re-derive + // that, and `Alternative::is_empty()` cannot: `B`, `B()` and `B {}` + // are all empty by it. + let ctor = alt.spell(quote!(#source_module::#enum_ident::#vident), &inits); arms.push(quote! { if env.is_instance_of(__obj, #jvm_class) .map_err(|e| <__JniErr as ::core::convert::From>::from( @@ -417,13 +414,18 @@ fn read_kotlin_property( registry: &impl Conversions, receiver: &TokenStream, prop: &str, - ty: &syn::Type, + reading: &TypeRef, bind: &syn::Ident, err_prefix: &str, ) -> Option<(TokenStream, TokenStream)> { - let entry = registry - .reading_of(ty) - .and_then(|tr| registry.input_entry(&tr))?; + // The payload's own reading straight to its entry, and the layer questions + // below asked of it once — `option_inner_type` compared the last path + // segment, so a payload spelled `Box>` answered "not optional" + // four separate times here (#289). + let entry = registry.input_entry(reading)?; + let ty = reading.syntax(); + let optional = reading.optional_inner().is_some(); + let inner = reading.optional_inner().unwrap_or(reading); let wire = entry.destination.clone(); let raw = format_ident!("{}_raw", bind); // The COMPLETE wire → Rust chain, not just the wire-facing converter: a @@ -449,7 +451,7 @@ fn read_kotlin_property( // (and same reasoning) as an owned handle field of a data class; // `Option<_>` keeps the niche-aware converter (jlong 0 ⇒ `None`). let closed_msg = "Operation on a closed native handle."; - let decode = if option_inner_type(ty).is_some() { + let decode = if optional { quote! { let #bind = #conv; } } else { quote! { @@ -489,23 +491,20 @@ fn read_kotlin_property( // `Option` one unboxes a `java.lang.Integer`, and neither matches // what the JVM slot actually holds. `struct_input_body` makes the same // distinction for data-class fields; this is that logic for a property. - let enum_inner = option_inner_type(ty).unwrap_or_else(|| ty.clone()); - if ext.is_kotlin_enum(&enum_inner) { - let fqn = bare_path_ident(&enum_inner) - .and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n))) - .map(|v| v.to_string())?; + if ext.is_kotlin_enum_reading(inner) { + // The NAME off the classification, not off the last path segment. + let fqn = match inner.kind() { + flat::TypeKind::Named { id } => id.ident(), + _ => None, + } + .and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n))) + .map(|v| v.to_string())?; let sig = format!("L{};", fqn.replace('.', "/")); let obj = format_ident!("{}_obj", bind); // Under `Option`, JVM null is `None` and the INNER converter decodes // the discriminant; the outer converter would expect a boxed Integer. - let decode = if option_inner_type(ty).is_some() { - let inner_conv = composed_entry_decode( - registry - .reading_of(&enum_inner) - .and_then(|tr| registry.input_entry(&tr))?, - &raw, - bind, - ); + let decode = if optional { + let inner_conv = composed_entry_decode(registry.input_entry(inner)?, &raw, bind); quote! { let #bind = if #obj.is_null() { ::core::option::Option::None @@ -562,12 +561,24 @@ fn read_kotlin_property( // class, another sum, a `List`): the slot's descriptor is the // registered Kotlin class and the value decodes through its own // converter — the same delegation the data-class path uses. - let slot_ty = option_inner_type(ty).unwrap_or_else(|| ty.clone()); - let sig = bare_path_ident(&slot_ty) - .and_then(|name| ext.kotlin_fqn(&TypeKey::from_ident(&name))) - .map(|v| format!("L{};", v.replace('.', "/"))) - .or_else(|| pat_match_top(&slot_ty, "Vec").then(|| "Ljava/util/List;".to_string())) - .unwrap_or_else(|| "Ljava/lang/Object;".to_string()); + let sig = match inner.kind() { + // The NAME off the classification, not off the last path + // segment: `Box` IS `T` here. + flat::TypeKind::Named { id } => id.ident(), + _ => None, + } + .and_then(|name| ext.kotlin_fqn(&TypeKey::from_ident(&name))) + .map(|v| format!("L{};", v.replace('.', "/"))) + .or_else(|| { + // A run of values is what `kind` says it is. + // `pat_match_top(.., "Vec")` compared the last path segment, so + // a `Box>` answered false. + inner + .sequence_elem() + .is_some() + .then(|| "Ljava/util/List;".to_string()) + }) + .unwrap_or_else(|| "Ljava/lang/Object;".to_string()); Some(( quote! { let #raw: jni::objects::JObject = env.get_field(#receiver, #prop, #sig) @@ -1037,7 +1048,7 @@ fn wire_kotlin_type(entry: &crate::api::core::registry::TypeEntry) - fn build_flat_sum_field( ext: &Declarations, registry: &Registry, - sum_ty: &syn::Type, + sum_reading: &TypeRef, field: syn::Ident, optional: bool, native_prefix: &str, @@ -1047,14 +1058,20 @@ fn build_flat_sum_field( leaves: &mut Vec, ) -> Option { let rust_ty = field_reading.syntax(); - use crate::api::core::types_util::SumSpec; - let ident = bare_path_ident(sum_ty)?; - let item_enum = registry.flat().enum_item(&ident)?; + // The NAME off the classification, and then the ELEMENT — `enum_item` + // hands back only the `syn::ItemEnum`, deliberately, so a consumer that + // acts on the Variant/Enum distinction asks `declared_type` (#289). + let ident = match sum_reading.kind() { + flat::TypeKind::Named { id } => id.ident(), + _ => None, + }?; + let flat::Type::Variant(sum) = registry.flat().declared_type(&ident)? else { + return None; + }; let cfg = ext.types.get(&TypeKey::from_ident(&ident))?; let sum_cfg = cfg.sum()?; let iface_fqn = cfg.name_spec.as_ref().map(|s| ext.fqn_of(s))?; - let spec = SumSpec::from_item_enum(item_enum); // Plan every group first: a single unflattenable payload means the whole // sum stays object-shaped, so nothing may be pushed until all of them are @@ -1071,13 +1088,12 @@ fn build_flat_sum_field( nullable_wire: bool, } let mut planned: Vec = Vec::new(); - for (v, item_variant) in spec.variants.iter().zip(&item_enum.variants) { - let kotlin = ext.sum_variant_class_name(sum_cfg, &v.ident); + for alt in &sum.alternatives { + let kotlin = ext.sum_variant_class_name(sum_cfg, &alt.name); let mut fields = Vec::new(); - for (f, item_field) in v.fields.iter().zip(item_variant.fields.iter()) { - let entry = registry - .reading_of(&item_field.ty) - .and_then(|tr| registry.input_entry(&tr))?; + for field in &alt.fields { + // The payload's own reading straight to its entry. + let entry = registry.input_entry(&field.ty)?; // A projection payload (handle) carries ownership // and locking rules the tag-gated group does not model yet. if entry.metadata.projection.is_some() { @@ -1091,7 +1107,8 @@ fn build_flat_sum_field( if prim.is_none() && !is_string_like { return None; } - let prop = crate::api::lang::jnigen::jni::struct_plan::sum_field_prop_name(f); + let member = field.member(); + let prop = crate::api::lang::jnigen::jni::struct_plan::sum_field_prop_name(&member); let slot = crate::api::lang::jnigen::jni::struct_plan::sum_slot_fragment(&kotlin, &prop); // `(.field as? io.x.E.V)?.prop` — inert groups yield null, @@ -1101,7 +1118,7 @@ fn build_flat_sum_field( // An `enum_class` payload is a Kotlin enum object whose wire is // the `jint` discriminant, so the access reads `.value` — without // it the slot would be `Priority?` where the wire wants `Int`. - let read = if ext.is_kotlin_enum(&item_field.ty) { + let read = if ext.is_kotlin_enum_reading(&field.ty) { format!("{prop}?.value") } else { prop.clone() @@ -1112,7 +1129,7 @@ fn build_flat_sum_field( None => (cast, true), }; fields.push(( - f.member.clone(), + member, PlannedLeaf { native: format!("{native_prefix}_{slot}"), entry: entry.clone(), @@ -1122,7 +1139,7 @@ fn build_flat_sum_field( )); } planned.push(Planned { - rust_ident: v.ident.clone(), + rust_ident: alt.name.clone(), kotlin, fields, }); @@ -1463,7 +1480,7 @@ fn build_flat_struct_node( if let Some(node) = build_flat_sum_field( ext, registry, - &nested_ty, + nested, fident.clone(), field_optional, &child_native, diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index 706a6464..b1814ffb 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -77,7 +77,7 @@ pub(crate) fn synth_sum_leaves( for (variant, alt) in spec.variants.iter().zip(&sum.alternatives) { let kotlin_name = ext.sum_variant_class_name(sum_cfg, &variant.ident); for (field, alt_field) in variant.fields.iter().zip(&alt.fields) { - let prop = sum_field_prop_name(field); + let prop = sum_field_prop_name(&field.member); leaves.push(UnfoldLeaf { name: sum_slot_fragment(&kotlin_name, &prop), path: Vec::new(), diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 753cf598..da6ebb0d 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -531,7 +531,7 @@ fn sum_plan_kind( let kotlin_name = ext.sum_variant_class_name(sum_cfg, &v.ident); let mut fields: Vec = Vec::new(); for (f, alt_field) in v.fields.iter().zip(alt.fields.iter()) { - let prop = sum_field_prop_name(f); + let prop = sum_field_prop_name(&f.member); let slot = sum_slot_fragment(&kotlin_name, &prop); let owner = format!("{ident}::{}.{prop}", v.ident); // `?` — a payload whose converter has not resolved yet defers the @@ -564,8 +564,13 @@ fn sum_plan_kind( /// Kotlin property name of one sum payload field — a named field keeps its /// camelCased name, a tuple field becomes `v0`, `v1`. Must agree with the /// sealed-interface emitter, which is why both call this. -pub(crate) fn sum_field_prop_name(field: &crate::api::core::types_util::SumField) -> String { - match &field.member { +/// +/// Takes the **member**, which is the whole of what the name depends on, so a +/// caller holding a `flat::Field` asks `Field::member()` and a caller holding a +/// `SumField` reads its own — one derivation for both, rather than a second +/// convention that could drift from this one (#289). +pub(crate) fn sum_field_prop_name(member: &syn::Member) -> String { + match member { syn::Member::Named(id) => mangle_kotlin_ident(&kt_snake_to_camel(&id.to_string())), syn::Member::Unnamed(i) => format!("v{}", i.index), } diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index f9511e78..d9b2484f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -2062,8 +2062,10 @@ impl Declarations { // OUTPUT direction has no counterpart: a sum crosses Rust → // Kotlin flattened, always.) if self.types.get(&key).is_some_and(|c| c.sum().is_some()) { - if let Some(e) = registry.flat().enum_item(&name) { - let (wire, body) = sum_input_body(self, e, registry)?; + if let Some(crate::api::core::flat::Type::Variant(v)) = + registry.flat().declared_type(&name) + { + let (wire, body) = sum_input_body(self, v, registry)?; // The wire's own null niche, exactly as a data class gets // — that is what lets `Option` fold with JVM null as // `None` instead of needing a boxed wrapper. diff --git a/prebindgen/src/api/lang/jnigen/mod.rs b/prebindgen/src/api/lang/jnigen/mod.rs index a3532d0c..cdac2a01 100644 --- a/prebindgen/src/api/lang/jnigen/mod.rs +++ b/prebindgen/src/api/lang/jnigen/mod.rs @@ -114,13 +114,11 @@ mod spelling_census { /// `(file, call count)` — every `.rs` under `api/lang/jnigen`, checked /// against the directory tree so a new module cannot sit outside the census. const CENSUS: &[(&str, usize)] = &[ - // 18 -> 9 -> 4 (#289): `build_flat_struct_node` took `flat::Struct` - // first, then `struct_input_body` — the whole-object `.jobject_input()` - // decoder — did the same. What remains is the SUM side: - // `sum_input_body` and the `read_kotlin_property` helper it alone - // calls, which need `Type::Variant` rather than the `syn::ItemEnum` - // `Flat::enum_item` hands back. - ("jni/emit/flat_input.rs", 4), + // flat_input.rs is off the census: 18 -> 9 -> 4 -> 0 (#289). The + // flatten path went first (#294), then the whole-object + // `.jobject_input()` decoders — struct, then sum. Every layer question + // in that file now asks a `TypeRef`, and its walks take `flat::Struct` + // and `flat::Variant` rather than the items they were parsed from. ("jni/emit/struct_out.rs", 2), ("jni/emit/wrapper.rs", 2), // From d0e3eefea9ed7f0cc3b05fc631c0e1e618ac3dff Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 09:53:36 +0200 Subject: [PATCH 47/52] jnigen: a sum's match pattern keeps its own delimiters (#304) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `encode_sum_group` built each arm's pattern by branching on `variant.fields.first()`, so an alternative with no fields took the `None` arm and was spelled bare. For `enum E { B() }` and `enum E { B {} }` that emits `myflat::E::B`, which is E0533 in pattern position: a zero-field tuple or struct variant still needs its delimiters. Branching on the first field cannot answer this, because an EMPTY alternative has no first field to branch on — the same shape as the empty struct that had no field to refuse, which is how #302 came to emit `myflat::Unit {}`. That is now three instances of one defect class in this area: a constructor caught in review (#302), a constructor avoided by using the model's helper (#303), and this pattern, which nothing had found. `Alternative::spell` is the fix, and its doc names the case: "the one place those delimiters are chosen — for match patterns and constructors alike, in either direction". This is the pattern half of that sentence; `Field::bind` shapes each binding the same way. Getting there needs the element rather than the item, so the arm list comes off `Flat::declared_type` -> `Type::Variant` and walks `alternatives`. `enum_item` hands back only the `syn::ItemEnum`, deliberately — its own doc says a consumer acting on the Variant/Enum distinction should ask `declared_type`. The tag is `alt.index`, which is what `SumVariant::tag` was a copy of. `empty_sum_alternatives_keep_their_own_pattern_delimiters` covers all three shapes and fails against the old branch. Goldens byte-identical: no in-tree fixture declares an empty non-unit alternative, which is exactly why this had no signal. Verified: 638 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild, covertest-kotlin 49/49. --- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 52 +++++++------ .../src/api/lang/jnigen/jni/tests/sealed.rs | 74 +++++++++++++++++++ 2 files changed, 102 insertions(+), 24 deletions(-) diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index b1814ffb..0781af20 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -239,20 +239,23 @@ pub(crate) fn encode_sum_group( .expect("a sum plan carries its selector leaf"); let tag_id = &obj_idents[tag_idx]; // A unit variant contributes no leaf, so the arm list is driven by the - // enum's own variants, not by the grouped leaves. - let item_enum = registry.flat().enum_item(&ident).unwrap_or_else(|| { - panic!("jnigen sum unfold: no indexed enum `{ident}` for the decomposed sum") - }); - let spec = crate::api::core::types_util::SumSpec::from_item_enum(item_enum); + // enum's own alternatives, not by the grouped leaves. `enum_item` hands + // back only the `syn::ItemEnum`, deliberately — a consumer that acts on the + // Variant/Enum distinction asks `declared_type` (#289). + let Some(crate::api::core::flat::Type::Variant(sum)) = registry.flat().declared_type(&ident) + else { + panic!("jnigen sum unfold: no indexed sum `{ident}` for the decomposed sum") + }; - let arms: Vec = spec - .variants + let arms: Vec = sum + .alternatives .iter() - .map(|variant| { + .map(|alt| { + let tag = alt.index as i32; let group: Vec = leaves .iter() .enumerate() - .filter(|(_, l)| l.group == Some(variant.tag)) + .filter(|(_, l)| l.group == Some(tag)) .map(|(i, _)| i) .collect(); let binds: Vec = group @@ -260,20 +263,21 @@ pub(crate) fn encode_sum_group( .enumerate() .map(|(k, _)| format_ident!("__sv{}", k)) .collect(); - let vident = &variant.ident; - let pattern = match variant.fields.first().map(|f| &f.member) { - None => quote!(#source::#vident), - Some(syn::Member::Named(_)) => { - let pairs = variant.fields.iter().zip(&binds).map(|(f, b)| { - let syn::Member::Named(n) = &f.member else { - unreachable!("variant field shapes are uniform") - }; - quote!(#n: #b) - }); - quote!(#source::#vident { #(#pairs),* }) - } - Some(syn::Member::Unnamed(_)) => quote!(#source::#vident(#(#binds),*)), - }; + let vident = &alt.name; + // The alternative's OWN delimiters, from the one place that chooses + // them — for match patterns and constructors alike. Branching on + // `fields.first()` could not answer this: an empty alternative has + // no first field, so `enum E { B() }` and `enum E { B {} }` both + // matched the `None` arm and emitted the bare `E::B`, which is + // E0533 in pattern position. Same shape as the empty struct that + // emitted `Unit {}` in #302. + let parts: Vec = alt + .fields + .iter() + .zip(&binds) + .map(|(f, b)| f.bind(b)) + .collect(); + let pattern = alt.spell(quote!(#source::#vident), &parts); // The live group: convert each payload through its own output // converter, exactly as a struct field of the same type would be. let live: TokenStream = group @@ -299,7 +303,7 @@ pub(crate) fn encode_sum_group( quote! { #id = #d; } }) .collect(); - let tag_lit = proc_macro2::Literal::i32_unsuffixed(variant.tag); + let tag_lit = proc_macro2::Literal::i32_unsuffixed(tag); // A nullable selector rides an OBJECT slot (its absent case is JVM // null, which a raw `jint` has no room for), so the live tag boxes // like any other nullable primitive leaf. diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs index aa1517bb..b09d23f0 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs @@ -1702,3 +1702,77 @@ fn a_raw_named_sum_generates() { "the sum encoder matches the raw-named enum by its real path:\n{rust}" ); } + +/// A payload-less alternative keeps its own delimiters in the output match. +/// +/// The arm builder branched on `variant.fields.first()`, so an alternative with +/// no fields took the `None` arm and was spelled bare — `myflat::Shape::Parens` +/// for `Parens()`, which is **E0533**: a zero-field tuple or struct variant +/// still needs its delimiters in pattern position. An empty alternative has no +/// first field to branch on, exactly as the empty struct in #302 had no field to +/// refuse. +/// +/// `Alternative::spell` is the one place those delimiters are chosen — its own +/// doc says "for match patterns and constructors alike, in either direction" — +/// and this is the pattern half of that sentence. +#[test] +fn empty_sum_alternatives_keep_their_own_pattern_delimiters() { + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum Shape { + Bare, + Parens(), + Braces {}, + Full(i64), + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn make_shape() -> Shape { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Shape)) + .fun(crate::fun!(make_shape)), + ); + + let dir = unique_test_dir("jnigen_empty_sum_alternatives"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + let gen = jni.build_with(registry).expect("resolve"); + let rust = std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust")) + .expect("read rust"); + let rc: String = rust.split_whitespace().collect(); + + // Each alternative is matched with the delimiters Rust demands for it. + assert!( + rc.contains("myflat::Shape::Bare=>"), + "a unit alternative is matched bare:\n{rust}" + ); + assert!( + rc.contains("myflat::Shape::Parens()=>"), + "an empty TUPLE alternative keeps its parens:\n{rust}" + ); + assert!( + rc.contains("myflat::Shape::Braces{}=>"), + "an empty STRUCT alternative keeps its braces:\n{rust}" + ); + // And the bare spelling must not appear for the two that cannot take it. + assert!( + !rc.contains("myflat::Shape::Parens=>") && !rc.contains("myflat::Shape::Braces=>"), + "neither empty alternative may be matched bare:\n{rust}" + ); +} From b13b5197fdafa8ccae6edff20c6c85e44d4e1c05 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 09:58:22 +0200 Subject: [PATCH 48/52] =?UTF-8?q?core:=20delete=20`SumSpec`=20=E2=80=94=20?= =?UTF-8?q?the=20model=20already=20describes=20a=20sum=20(#305)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * core: delete `SumSpec` — the model already describes a sum `SumSpec`/`SumVariant`/`SumField` described a data-carrying enum as a tag plus one leaf group per variant. `flat::Variant` describes the same thing, and better: `Alternative` carries the name, the declaration-order index and a `Vec` whose `ty` is a classified `TypeRef`, where `SumField` kept a bare `syn::Type`. It was #211's own thesis sitting in `api/core`. Every live field had an exact model equivalent: v.ident -> alt.name v.tag -> alt.index v.is_unit() -> alt.is_empty() f.member -> field.member() and four fields had no reader at all — `SumSpec::{key, source}`, `SumField::{name, ty}`. **`SumField::name` is the one that matters, because two comments and I said it was the blocker.** `emit/sum_out.rs` and `kotlin_emit.rs` both claimed "`SumSpec` owns the leaf-NAMING convention, which is jnigen's own", so retiring it would need that rehomed first. Nothing read it. jnigen names its slots with `sum_field_prop_name` + `sum_slot_fragment`, a different convention living in `struct_plan.rs`. `SumField::ty` was dead for the same reason one layer down: every site already took `alt_field.ty` off the element sitting beside it. The doc's other premise was also unmet — "both adapters read one definition instead of growing a private one each" — cbindgen never used it. The `#[allow(dead_code)]` on all three structs was the tell. Three of the four remaining sites already held the `&flat::Variant` and zipped `SumSpec` back against `sum.alternatives`: derived from the item, then re-paired with the element it was derived from. That is the shape #303 removed from `flat_input.rs`. The fourth reached `Flat::enum_item` and moves to `declared_type` -> `Type::Variant`, as #304 did. Also gone: `kotlin_emit`'s `sum_field_property_name`, a second copy of `sum_field_prop_name` keyed on the deleted type, and a redundant `enum_item` lookup in `struct_plan` that sat beside the `declared_type` doing the real work. Ledger and census do not move, and were not going to: `SumSpec` names no `syn::Type` variant, so the ledger never counted it. The five deleted `types_util` fixtures tested the declaration-order tag, which `flat/tests/acceptance.rs` already asserts on `Alternative::index`. Verified: 633 lib tests, clippy on 1.85.0 and stable, fmt, `cargo doc` clean of the two intra-doc links this orphaned, regen-check byte-identical after a forced rebuild, covertest-kotlin 49/49. * Review: one place turns an alternative's index into its wire tag Three sites did `alt.index as i32` — the leaf's `group`, the Kotlin `when` arm, and the Rust `match` arm — and a fourth formatted `alt.index` straight into a `when` arm. All four have to agree, and they agreed by coincidence rather than by construction. `sum_tag` is the one place. Review asked for a checked conversion. It is deliberately not one: the index counts alternatives of a single enum, and an enum with `i32::MAX` variants is not something rustc can be handed, so `try_from(..).expect(..)` would put a panic in the working path for a state the compiler cannot produce. The bound and that reasoning are on the function. The model keeps `usize`, which is the reason the conversion exists at all and belongs here. `i32` is `jint` / Kotlin `Int` — a destination-language width, and `core::flat` states language-neutral facts. cbindgen reads no model `.index`; the tag width is one adapter's concern, so it lives in that adapter, beside the `UnfoldLeaf::group: Option` it feeds. Cheap to have done otherwise — every other read of a model index is a doc invariant, an assertion or a `format!` — but it would have put a wire type in the model to save one cast. Also from review: the `enum_discriminant_values` rustdoc link said `Alternative::index` and pointed at the struct. Verified: 633 lib tests, clippy on 1.85.0 and stable, fmt, `cargo doc`, regen-check byte-identical after a forced rebuild, covertest-kotlin 49/49. --- prebindgen/src/api/core/types_util.rs | 112 +----------------- prebindgen/src/api/core/types_util/tests.rs | 111 ----------------- prebindgen/src/api/gen/kotlin/expr.rs | 4 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 27 ++--- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 91 ++++++-------- prebindgen/src/api/lang/jnigen/jni/mod.rs | 6 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 56 +++++---- 7 files changed, 91 insertions(+), 316 deletions(-) diff --git a/prebindgen/src/api/core/types_util.rs b/prebindgen/src/api/core/types_util.rs index 0429feba..5057c5e1 100644 --- a/prebindgen/src/api/core/types_util.rs +++ b/prebindgen/src/api/core/types_util.rs @@ -154,8 +154,8 @@ pub(crate) fn ident(s: &str) -> syn::Ident { /// Convert a `PascalCase` / `camelCase` identifier to `snake_case` /// (`ZKeyExpr` → `z_key_expr`). The single implementation behind the -/// public `prebindgen::lang::snake_case` re-export and the sum-variant -/// leaf naming in [`SumSpec`]. +/// public `prebindgen::lang::snake_case` re-export, and behind cbindgen's +/// type-name mangling. pub fn pascal_to_snake(s: &str) -> String { let mut out = String::new(); for (i, c) in s.chars().enumerate() { @@ -213,109 +213,6 @@ pub fn first_payload_variant(e: &syn::ItemEnum) -> Option<&syn::Variant> { .find(|v| !matches!(v.fields, syn::Fields::Unit)) } -/// The language-neutral description of a data-carrying enum: a **tag** — -/// which alternative is live — plus one **leaf group per variant**. -/// -/// Core describes the sum; adapters decide what its leaves look like on the -/// wire (`JniGenBuilder` overlays the groups in the signature, `CbindgenBuilder` overlays -/// them in memory as a `#[repr(C)]` union). Nothing here names a wire -/// detail — in particular a payload enum carries no `repr`, so tags are -/// declaration order and never an explicit discriminant. -/// The neutral description lands before either lowering, so both adapters -/// read one definition instead of growing a private one each; the -/// `dead_code` allow covers that gap and goes away with the first adapter -/// that reads a sum. -#[allow(dead_code)] -pub struct SumSpec { - /// Canonical key of the enum type. - pub key: crate::api::core::registry::TypeKey, - /// The enum's ident as declared in the source crate — the spelling - /// adapters use to build `Enum::Variant` constructor paths. - pub source: syn::Ident, - /// Variants in declaration order; `variants[i].tag == i as i32`. - pub variants: Vec, -} - -/// One alternative of a [`SumSpec`]. -#[allow(dead_code)] -pub struct SumVariant { - /// The variant ident as declared (`PeriodicQueries`). - pub ident: syn::Ident, - /// Declaration-order tag, `0..N-1`. - pub tag: i32, - /// The variant's payload, in declaration order. Empty for a unit - /// variant — the group that contributes nothing but its tag. - pub fields: Vec, -} - -/// One payload field of a [`SumVariant`]. -#[allow(dead_code)] -pub struct SumField { - /// How the field is addressed in a pattern: `Named(ident)` for a - /// struct variant, `Unnamed(index)` for a tuple variant. - pub member: syn::Member, - /// Leaf name, following the existing nested-prefix convention: - /// `_` for a named field, `_` - /// for a tuple field. - pub name: String, - /// The field's declared type. - pub ty: syn::Type, -} - -#[allow(dead_code)] -impl SumSpec { - /// Describe `e` as a sum. Every enum has a description — a - /// [`Unit`](EnumShape::Unit) enum yields all-empty groups, which is - /// exactly the "tag only" lowering — so this never fails and never - /// consults [`enum_shape`]. - pub fn from_item_enum(e: &syn::ItemEnum) -> Self { - let variants = e - .variants - .iter() - .enumerate() - .map(|(i, v)| { - let prefix = pascal_to_snake(&v.ident.to_string()); - let fields = v - .fields - .iter() - .enumerate() - .map(|(fi, f)| match &f.ident { - Some(id) => SumField { - member: syn::Member::Named(id.clone()), - name: format!("{prefix}_{id}"), - ty: f.ty.clone(), - }, - None => SumField { - member: syn::Member::Unnamed(syn::Index::from(fi)), - name: format!("{prefix}_{fi}"), - ty: f.ty.clone(), - }, - }) - .collect(); - SumVariant { - ident: v.ident.clone(), - tag: i as i32, - fields, - } - }) - .collect(); - Self { - key: crate::api::core::registry::TypeKey::from_ident(&e.ident), - source: e.ident.clone(), - variants, - } - } -} - -#[allow(dead_code)] -impl SumVariant { - /// True when this variant carries no payload — its leaf group is empty - /// and it contributes only its tag. - pub fn is_unit(&self) -> bool { - self.fields.is_empty() - } -} - /// Resolve each enum variant to its discriminant value following Rust's own /// assignment rule: an explicit `= N` sets the value, an implicit variant /// takes the previous value plus one (starting at 0). @@ -329,8 +226,9 @@ impl SumVariant { /// expressions at codegen time. /// /// This describes the **unit** enum's wire numbering. A payload enum's -/// alternatives are identified by the declaration-order tag of -/// [`SumSpec`], never by a discriminant. +/// alternatives are identified by +/// [`Alternative::index`](crate::api::core::flat::Alternative::index) — +/// declaration order — never by a discriminant. pub fn enum_discriminant_values(e: &syn::ItemEnum) -> Vec<(syn::Ident, i64)> { let mut out = Vec::with_capacity(e.variants.len()); let mut next: i64 = 0; diff --git a/prebindgen/src/api/core/types_util/tests.rs b/prebindgen/src/api/core/types_util/tests.rs index f41fa6d6..640d337f 100644 --- a/prebindgen/src/api/core/types_util/tests.rs +++ b/prebindgen/src/api/core/types_util/tests.rs @@ -1,5 +1,3 @@ -use quote::ToTokens; - use super::*; /// Lifetimes and const-generic args are fixed pattern structure — they must @@ -43,115 +41,6 @@ fn first_payload_variant_is_the_first_in_declaration_order() { ); } -/// Tags are declaration order `0..N-1` and never an explicit discriminant — -/// a payload enum carries no `repr`, so a discriminant is a wire detail the -/// neutral tier must not name. -#[test] -fn sum_spec_tags_are_declaration_order() { - let e: syn::ItemEnum = syn::parse_quote! { - enum E { A(u32), B, C { x: u8 } } - }; - let spec = SumSpec::from_item_enum(&e); - assert_eq!(spec.source, "E"); - assert_eq!(spec.key.as_str(), "E"); - assert_eq!( - spec.variants - .iter() - .map(|v| (v.ident.to_string(), v.tag)) - .collect::>(), - vec![ - ("A".to_string(), 0), - ("B".to_string(), 1), - ("C".to_string(), 2) - ] - ); - // A unit variant is the empty group. - assert!(spec.variants[1].is_unit()); - assert!(!spec.variants[0].is_unit()); -} - -/// Explicit discriminants on a payload enum do not move the tags — those -/// are two independent numberings, and only `enum_discriminant_values` -/// reads the discriminant. -#[test] -fn sum_spec_tags_ignore_explicit_discriminants() { - let e: syn::ItemEnum = syn::parse_quote! { enum E { A = 5, B = 9 } }; - let spec = SumSpec::from_item_enum(&e); - assert_eq!( - spec.variants.iter().map(|v| v.tag).collect::>(), - vec![0, 1] - ); - assert_eq!( - enum_discriminant_values(&e) - .into_iter() - .map(|(_, v)| v) - .collect::>(), - vec![5, 9] - ); -} - -/// Leaf names follow the existing nested-prefix convention: -/// `_`, tuple fields `_`. -#[test] -fn sum_spec_leaf_names_and_members() { - let e: syn::ItemEnum = syn::parse_quote! { - enum RecoveryMode { - PeriodicQueries(Duration), - Heartbeat, - Windowed { size: u32, ratio: f64 }, - Pair(u8, u8), - } - }; - let spec = SumSpec::from_item_enum(&e); - - let names: Vec> = spec - .variants - .iter() - .map(|v| v.fields.iter().map(|f| f.name.clone()).collect()) - .collect(); - assert_eq!( - names, - vec![ - vec!["periodic_queries_0".to_string()], - vec![], - vec!["windowed_size".to_string(), "windowed_ratio".to_string()], - vec!["pair_0".to_string(), "pair_1".to_string()], - ] - ); - - // Members address the field in a pattern: named by ident, tuple by index. - let named = &spec.variants[2].fields[0].member; - assert!(matches!(named, syn::Member::Named(id) if id == "size")); - let unnamed = &spec.variants[3].fields[1].member; - assert!(matches!(unnamed, syn::Member::Unnamed(i) if i.index == 1)); - - // Payload types survive verbatim. - assert_eq!( - spec.variants[0].fields[0].ty.to_token_stream().to_string(), - "Duration" - ); -} - -/// A unit-only enum is the degenerate sum: every group is empty, so the -/// lowering collapses to "a tag". That is why existing enums are -/// unaffected by the sum machinery. -#[test] -fn sum_spec_of_unit_enum_is_all_empty_groups() { - let e: syn::ItemEnum = syn::parse_quote! { enum E { A, B, C } }; - let spec = SumSpec::from_item_enum(&e); - assert_eq!(spec.variants.len(), 3); - assert!(spec.variants.iter().all(|v| v.is_unit())); -} - -#[test] -fn sum_spec_single_variant() { - let e: syn::ItemEnum = syn::parse_quote! { enum E { Only(String) } }; - let spec = SumSpec::from_item_enum(&e); - assert_eq!(spec.variants.len(), 1); - assert_eq!(spec.variants[0].tag, 0); - assert_eq!(spec.variants[0].fields[0].name, "only_0"); -} - #[test] fn pascal_to_snake_basics() { assert_eq!(pascal_to_snake("ZKeyExpr"), "z_key_expr"); diff --git a/prebindgen/src/api/gen/kotlin/expr.rs b/prebindgen/src/api/gen/kotlin/expr.rs index f7da026c..83e7609d 100644 --- a/prebindgen/src/api/gen/kotlin/expr.rs +++ b/prebindgen/src/api/gen/kotlin/expr.rs @@ -43,8 +43,8 @@ // This tier lands before its consumers: Stage 3 (#193) rewrites the emitters // that produce plan-carried expressions, and #199 migrates the rest. Until // then the AST is exercised by its own tests and by nothing else — the same gap -// `SumSpec` and Tier 0 carry, and it closes with the first emitter that builds -// a tree instead of a string. +// Tier 0 carries, and it closes with the first emitter that builds a tree +// instead of a string. #![allow(dead_code)] use std::collections::{BTreeSet, HashMap}; diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index 0781af20..5dbae936 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -45,15 +45,8 @@ pub(crate) fn synth_sum_leaves( sum_cfg: &SumConfig, sum: &crate::api::core::flat::Variant, ) -> Vec { - use crate::api::core::{ - types_util::SumSpec, - unfold::{LeafSource, UnfoldLeaf}, - }; + use crate::api::core::unfold::{LeafSource, UnfoldLeaf}; - // `SumSpec` still reads the item — it owns the leaf-NAMING convention, which - // is jnigen's own. The payload TYPES come from the element beside it, whose - // fields are already readings, so nothing here has to compose or look one up. - let spec = SumSpec::from_item_enum(&sum.origin.syntax); // The selector rides ahead of the groups it chooses between, and carries // **which sum** it selects over as its `out_ty` — that is how the emitter // finds the enum to `match` when the sum is a field rather than the whole @@ -74,21 +67,21 @@ pub(crate) fn synth_sum_leaves( source: LeafSource::SumTag, group: None, }]; - for (variant, alt) in spec.variants.iter().zip(&sum.alternatives) { - let kotlin_name = ext.sum_variant_class_name(sum_cfg, &variant.ident); - for (field, alt_field) in variant.fields.iter().zip(&alt.fields) { - let prop = sum_field_prop_name(&field.member); + for alt in &sum.alternatives { + let kotlin_name = ext.sum_variant_class_name(sum_cfg, &alt.name); + for field in &alt.fields { + let prop = sum_field_prop_name(&field.member()); leaves.push(UnfoldLeaf { name: sum_slot_fragment(&kotlin_name, &prop), path: Vec::new(), - out_ty: alt_field.ty.clone(), + out_ty: field.ty.clone(), identity: false, nullable: false, source: LeafSource::VariantField { - variant: variant.ident.clone(), - member: field.member.clone(), + variant: alt.name.clone(), + member: field.member(), }, - group: Some(variant.tag), + group: Some(sum_tag(alt)), }); } } @@ -251,7 +244,7 @@ pub(crate) fn encode_sum_group( .alternatives .iter() .map(|alt| { - let tag = alt.index as i32; + let tag = sum_tag(alt); let group: Vec = leaves .iter() .enumerate() diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index e3525c18..71033480 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -423,17 +423,6 @@ impl Declarations { } } -/// Kotlin property name of one sum payload field: a named field keeps its -/// (camelCased) name, a tuple field becomes `v0`, `v1`, …. Derived from the -/// neutral [`SumField`](crate::api::core::types_util::SumField)'s `member`, -/// so core's leaf naming and the Kotlin surface cannot drift apart. -fn sum_field_property_name(field: &crate::api::core::types_util::SumField) -> String { - match &field.member { - syn::Member::Named(id) => mangle_kotlin_ident(&kt_snake_to_camel(&id.to_string())), - syn::Member::Unnamed(i) => format!("v{}", i.index), - } -} - /// Slot name of one variant field in the flattened `fromParts` signature: /// `_` (`periodicQueries_period`, `pair_v0`) — the /// existing nested-prefix convention, with `_` marking the variant boundary @@ -513,8 +502,8 @@ impl Declarations { /// Emit one Kotlin `sealed interface` per `sealed_class`-declared type — /// the surface of a sum where the target language has sums natively. /// - /// The shape follows the neutral - /// [`SumSpec`](crate::api::core::types_util::SumSpec) directly: a variant + /// The shape follows the model's own + /// [`Variant`](crate::api::core::flat::Variant) directly: an alternative /// with an empty leaf group becomes a `data object`, one with a payload a /// `data class`, both **nested inside** the interface so variant names /// cannot collide package-wide. The `fromParts(tag, …slots)` companion is @@ -529,8 +518,6 @@ impl Declarations { &self, registry: &Registry, ) -> Result, WriteKotlinError> { - use crate::api::core::types_util::SumSpec; - let mut written = Vec::new(); // Deterministic order by canonical Rust type-key. let mut keys: Vec<&TypeKey> = self.types.keys().collect(); @@ -568,12 +555,11 @@ impl Declarations { unreachable!("asserted just above") }; - let spec = SumSpec::from_item_enum(&sum.origin.syntax); // Every declared `.variant(...)` must name a real variant — // a typo would otherwise silently do nothing. for declared in sum_cfg.variant_names.keys() { assert!( - spec.variants.iter().any(|v| v.ident == declared), + sum.alternatives.iter().any(|a| a.name == *declared), "sealed_class!({ident}): variant!({declared}) does not name a variant of \ `{ident}`" ); @@ -583,7 +569,7 @@ impl Declarations { Some((p, c)) => (p.to_string(), c.to_string()), None => (String::new(), kotlin_fqn.clone()), }; - let mut class = self.build_sealed_class(registry, &class_name, sum, &spec, sum_cfg); + let mut class = self.build_sealed_class(registry, &class_name, sum, sum_cfg); let mut file = kt::KtFile::new(package); if let Some(iface) = self.apply_class_interface(key, &mut class, &class_name, &[], Vec::new(), true) @@ -605,12 +591,11 @@ impl Declarations { registry: &Registry, class_name: &str, sum: &crate::api::core::flat::Variant, - spec: &crate::api::core::types_util::SumSpec, sum_cfg: &SumConfig, ) -> KtClass { - // `SumSpec` owns the leaf-NAMING convention, which is jnigen's own; the - // payload TYPES come from the element beside it. Same split #278 drew - // in `synth_sum_leaves`. + // Everything below comes off the element: `alternatives` for the + // classes, `Field::member()` for the property names. The docs and the + // framework line are spelling, so they read `origin.syntax`. let item_enum = &sum.origin.syntax; let framework_line = format!( "JVM-side surface for the native Rust `{}` sum: exactly one alternative is live.", @@ -625,9 +610,9 @@ impl Declarations { .kdoc(kdoc); // Nested variant classes, in declaration (tag) order. - for (variant, alt) in spec.variants.iter().zip(&sum.alternatives) { - let vname = self.sum_variant_class_name(sum_cfg, &variant.ident); - let mut vclass = if variant.is_unit() { + for alt in &sum.alternatives { + let vname = self.sum_variant_class_name(sum_cfg, &alt.name); + let mut vclass = if alt.is_empty() { KtClass::new(ClassKind::DataObject, &vname) } else { KtClass::new(ClassKind::Data, &vname) @@ -639,10 +624,9 @@ impl Declarations { vclass = vclass.kdoc(doc); } let mut vprops: Vec<(String, KtType)> = Vec::new(); - for (field, alt_field) in variant.fields.iter().zip(alt.fields.iter()) { - let prop = sum_field_property_name(field); - let ty = - self.sum_payload_kt_type(registry, &sum.name, &variant.ident, &prop, alt_field); + for field in &alt.fields { + let prop = sum_field_prop_name(&field.member()); + let ty = self.sum_payload_kt_type(registry, &sum.name, &alt.name, &prop, field); vprops.push((prop.clone(), ty.clone())); vclass = vclass.ctor_param(KtCtorParam::new(&prop, ty).val().vis(Vis::Public)); } @@ -665,30 +649,31 @@ impl Declarations { .annotation("JvmStatic") .param(KtParam::new("tag", KtType::int())) .returns(KtType::cls(class_name)); - for (variant, alt) in spec.variants.iter().zip(&sum.alternatives) { - let vname = self.sum_variant_class_name(sum_cfg, &variant.ident); - for (field, alt_field) in variant.fields.iter().zip(alt.fields.iter()) { - let prop = sum_field_property_name(field); - let ty = - self.sum_payload_kt_type(registry, &sum.name, &variant.ident, &prop, alt_field); + for alt in &sum.alternatives { + let vname = self.sum_variant_class_name(sum_cfg, &alt.name); + for field in &alt.fields { + let prop = sum_field_prop_name(&field.member()); + let ty = self.sum_payload_kt_type(registry, &sum.name, &alt.name, &prop, field); factory = factory.param(KtParam::new(sum_slot_name(&vname, &prop), ty)); } } let mut body = Code::new(); body = body.blk("when (tag) {", |mut w| { - for variant in &spec.variants { - let vname = self.sum_variant_class_name(sum_cfg, &variant.ident); - let args: Vec = variant + for alt in &sum.alternatives { + let vname = self.sum_variant_class_name(sum_cfg, &alt.name); + let args: Vec = alt .fields .iter() - .map(|f| sum_slot_name(&vname, &sum_field_property_name(f))) + .map(|f| sum_slot_name(&vname, &sum_field_prop_name(&f.member()))) .collect(); - let ctor = if variant.is_unit() { + let ctor = if alt.is_empty() { vname } else { format!("{vname}({})", args.join(", ")) }; - w = w.line(format!("{} -> {ctor}", variant.tag)); + // The same tag the selector leaf carries — a `when` arm that + // disagreed with the wire value would simply never match. + w = w.line(format!("{} -> {ctor}", sum_tag(alt))); } w.line(format!( "else -> throw IllegalArgumentException(\"{class_name}: invalid tag $tag\")" @@ -1421,8 +1406,6 @@ impl Declarations { names: &[String], imports: &mut BTreeSet, ) -> (String, String) { - use crate::api::core::types_util::SumSpec; - let key = TypeKey::from_type(source); let iface_fqn = self .kotlin_fqn(&key) @@ -1430,32 +1413,36 @@ impl Declarations { let iface_short = register_fqn(&iface_fqn, imports); let ident = bare_path_ident(source) .unwrap_or_else(|| panic!("sum builder: `{key}` is not a path type")); - let item_enum = registry - .flat() - .enum_item(&ident) - .unwrap_or_else(|| panic!("sum builder: no indexed enum `{ident}`")); + let Some(crate::api::core::flat::Type::Variant(sum)) = + registry.flat().declared_type(&ident) + else { + panic!("sum builder: `{ident}` is not an indexed sum") + }; let sum_cfg = self.types[&key] .sum() .unwrap_or_else(|| panic!("sum builder: `{ident}` is not a sealed class")); - let spec = SumSpec::from_item_enum(item_enum); let tag = &names[0]; let mut arms: Vec = Vec::new(); - for variant in &spec.variants { - let vname = self.sum_variant_class_name(sum_cfg, &variant.ident); + for alt in &sum.alternatives { + let group = sum_tag(alt); + let vname = self.sum_variant_class_name(sum_cfg, &alt.name); let args: Vec = leaves .iter() .zip(params) .zip(names) - .filter(|((l, _), _)| l.group == Some(variant.tag)) + .filter(|((l, _), _)| l.group == Some(group)) .map(|((l, p), n)| self.sum_ctor_arg(registry, l, p, n, imports)) .collect(); + // Kotlin has no `B()` / `B {}` distinction to keep: a payload-less + // alternative is a `data object`, named bare. The Rust side is where + // the delimiters matter, and `Alternative::spell` owns them there. let ctor = if args.is_empty() { format!("{iface_short}.{vname}") } else { format!("{iface_short}.{vname}({})", args.join(", ")) }; - arms.push(format!("{} -> {ctor}", variant.tag)); + arms.push(format!("{group} -> {ctor}")); } // A NULLABLE selector carries the absent case of a conditional value // form: null in means null out. Without this arm the `when` would fall diff --git a/prebindgen/src/api/lang/jnigen/jni/mod.rs b/prebindgen/src/api/lang/jnigen/jni/mod.rs index 5284299d..34fb8693 100644 --- a/prebindgen/src/api/lang/jnigen/jni/mod.rs +++ b/prebindgen/src/api/lang/jnigen/jni/mod.rs @@ -107,9 +107,9 @@ pub(crate) struct EnumConfig {} /// of [`DeclaredKind::Sealed`], which is what marks a `#[prebindgen]` /// **data-carrying** enum as mirrored by a /// Kotlin `sealed interface`. The tag/leaf-group structure itself is read -/// from the source enum through the neutral -/// [`SumSpec`](crate::api::core::types_util::SumSpec) — only what the -/// declaration adds lives here. +/// from the model's [`Variant`](crate::api::core::flat::Variant) — its +/// `alternatives` in declaration order, indexed as they are tagged — and only +/// what the declaration adds lives here. #[derive(Clone, Default)] pub(crate) struct SumConfig { /// Per-variant Kotlin class-name overrides, keyed by the Rust variant diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index da6ebb0d..08d85ca7 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -483,8 +483,6 @@ fn sum_plan_kind( optional: bool, depth: usize, ) -> Option { - use crate::api::core::types_util::SumSpec; - // Sum expansion needs its OWN depth guard. A sum whose payload is a sum // never passes through `build_struct_plan`, so that function's assert — // the only one on this recursion before now — cannot see a chain made @@ -501,15 +499,13 @@ fn sum_plan_kind( let ident = bare_path_ident(ty).unwrap_or_else(|| { panic!("fromParts bridge: sealed-class field `{owner}` is not a path type") }); - let item_enum = registry.flat().enum_item(&ident).unwrap_or_else(|| { - panic!("fromParts bridge: sealed-class field `{owner}` has no indexed enum `{ident}`") - }); // The sum as the MODEL holds it: its alternatives' payloads are `TypeRef`s // already, so classifying one asks nothing and cannot be asked about a type - // the model never saw. + // the model never saw. One lookup, not two — the `enum_item` that used to + // sit beside this only fed a `SumSpec` of what the element already says. let Some(crate::api::core::flat::Type::Variant(sum)) = registry.flat().declared_type(&ident) else { - panic!("fromParts bridge: sealed-class field `{owner}`: `{ident}` is not a sum") + panic!("fromParts bridge: sealed-class field `{owner}`: `{ident}` is not an indexed sum") }; let key = TypeKey::from_ident(&ident); let cfg = ext @@ -525,26 +521,22 @@ fn sum_plan_kind( .map(|s| ext.fqn_of(s)) .unwrap_or_else(|| panic!("fromParts bridge: sealed class `{ident}` has no Kotlin name")); - let spec = SumSpec::from_item_enum(item_enum); let mut variants: Vec = Vec::new(); - for (v, alt) in spec.variants.iter().zip(&sum.alternatives) { - let kotlin_name = ext.sum_variant_class_name(sum_cfg, &v.ident); + for alt in &sum.alternatives { + let kotlin_name = ext.sum_variant_class_name(sum_cfg, &alt.name); let mut fields: Vec = Vec::new(); - for (f, alt_field) in v.fields.iter().zip(alt.fields.iter()) { - let prop = sum_field_prop_name(&f.member); + for field in &alt.fields { + let member = field.member(); + let prop = sum_field_prop_name(&member); let slot = sum_slot_fragment(&kotlin_name, &prop); - let owner = format!("{ident}::{}.{prop}", v.ident); + let owner = format!("{ident}::{}.{prop}", alt.name); // `?` — a payload whose converter has not resolved yet defers the // whole plan to the next iteration, it does not fail the build. - let kind = classify_field(ext, registry, &alt_field.ty, &owner, depth + 1)?; - fields.push(SumPlanField { - member: f.member.clone(), - slot, - kind, - }); + let kind = classify_field(ext, registry, &field.ty, &owner, depth + 1)?; + fields.push(SumPlanField { member, slot, kind }); } variants.push(SumPlanVariant { - rust_ident: v.ident.clone(), + rust_ident: alt.name.clone(), kotlin_name, fields, }); @@ -565,10 +557,10 @@ fn sum_plan_kind( /// camelCased name, a tuple field becomes `v0`, `v1`. Must agree with the /// sealed-interface emitter, which is why both call this. /// -/// Takes the **member**, which is the whole of what the name depends on, so a -/// caller holding a `flat::Field` asks `Field::member()` and a caller holding a -/// `SumField` reads its own — one derivation for both, rather than a second -/// convention that could drift from this one (#289). +/// Takes the **member**, which is the whole of what the name depends on: every +/// caller holds a `flat::Field` and asks `Field::member()`. It took a +/// `types_util::SumField` when a second description of a sum still existed +/// beside the model's (#289). pub(crate) fn sum_field_prop_name(member: &syn::Member) -> String { match member { syn::Member::Named(id) => mangle_kotlin_ident(&kt_snake_to_camel(&id.to_string())), @@ -576,6 +568,22 @@ pub(crate) fn sum_field_prop_name(member: &syn::Member) -> String { } } +/// The wire tag of one alternative: its declaration-order index, as the `jint` +/// the selector leaf carries. +/// +/// One place, because the tag has to agree in three: the leaf's `group`, the +/// Kotlin `when` arm, and the Rust `match` arm. Three separate `as i32` casts +/// agreed by coincidence rather than by construction. +/// +/// Deliberately **not** a checked conversion. `usize` → `i32` can truncate in +/// general, but not here: the index counts alternatives of one enum, and an +/// enum with `i32::MAX` variants is not a thing rustc can be handed. A +/// `try_from(..).expect(..)` would put a panic in the working path for a state +/// the compiler cannot produce, which is the shape this crate avoids. +pub(crate) fn sum_tag(alt: &crate::api::core::flat::Alternative) -> i32 { + alt.index as i32 +} + /// Slot-name fragment for one variant field: `_`. Keyed /// on the **Kotlin** variant name so a `variant!(V).name(...)` rename carries /// through to the slots. From 9047275f0c8b41671a2ac558d24b7ae2b567d7ac Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 11:03:10 +0200 Subject: [PATCH 49/52] core: a `SumTag` selector registers the sum it names (#282) (#306) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * core: a `SumTag` selector registers the sum it names (#282) #282 asked one thing: is the `SumTag` leaf boundary-crossing data, so its `out_ty` should be registered, or adapter-only metadata that stays deliberately unregistered? Decision: it gets a cell. Most of the issue had already landed and its text is stale — `sum_out.rs` stopped composing `TypeRef::named(enum_ident)` when #280 sealed the composers, and the leaf now carries `Variant::type_ref()`, the reading the DECLARATION stored. What was open is registration. `require_output` could not serve it. That is `register_type_recursive(.., root = true)`, and a root DEMANDS a converter — which a sum has no whole-value output form of, so requiring one fails resolution. Pulling the tag's `i32` in that way is the original reason `has_converter()` exists. So registration and demand needed separating: `Registry::reference_output` registers without demanding, and both leaf loops now split on `has_converter()` instead of filtering by it. **The invariant, now stated where it is checkable.** Every leaf's `out_ty` has a table cell; only a converter-bearing leaf is a root. A cell says the type entered the pipeline, a root says the binding asked for it directly, an entry says one resolved — three claims, and a `SumTag` leaf makes only the first. Written on `has_converter`, on `LeafSource::SumTag`, and on `TypeRef`, whose doc already said a reading claims no converter and now says it claims no cell either. **What this buys, which the old test proves.** The invariant held before only because jnigen happens to declare the sum through `export_type`. `unfold/tests.rs`'s assertion read `!...is_some_and(|c| c.root)`, which is also true when the cell is ABSENT — and absent is what it was, since that fixture's registry declares nothing. It passed for the wrong reason and could state neither half. It now asserts the invariant over every leaf of the plan, and fails against the old filter. The end-to-end claim the acceptance asks for is a new test against a real `Registry`: for a declared sum, a cell both ways, root cleared both ways, an input entry (the whole-`JObject` decoder) and no output entry. That asymmetry is the design — Rust → Kotlin is flattened, always. Goldens byte-identical, as predicted: `crossings()` seeds from every table key, so a NEW cell would add a crossing — but jnigen's sum already had one from its declaration, so nothing entered the order. Ledger and census do not move; this adds no `syn::Type` match and no spelling-helper call. Verified: 634 lib tests, clippy on 1.85.0 and stable, fmt, `cargo doc`, regen-check after a forced rebuild, covertest-kotlin 49/49. * Review: the fixture's selector carries the sum, so the test pins #282 `reading_sum_decon` said it mirrors the JNI synthesis and gave the tag leaf `out_ty: i32` — the tag's WIRE type, where `synth_sum_leaves` stores the sum itself. So the registration assertion proved only that *some* converter-free leaf gets a cell, not that the selector registers the sum it names, which is the whole of #282. I noticed that divergence and wrote around it instead of fixing it, and the review is right that the acceptance test cannot cover for it: `sealed_class!(Reading)` creates the `Reading` output cell through `export_type` whether or not the leaf registers anything. Measured — it passes against the old filtered loop. With the fixture carrying `tref(Reading)`, `sum_return_is_a_fixed_builder_plan` fails against that loop with `leaf `tag` registers its out_ty`. That is the behaviour #282 decided, pinned. The acceptance test keeps its own claim — a declared sum's three-part registry state, including the input/output entry asymmetry — and its doc now says what it cannot claim, so it is not mistaken for the guard later. Verified: 634 lib tests, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, covertest-kotlin 49/49. --- prebindgen/src/api/core/flat/ty.rs | 9 +++ prebindgen/src/api/core/registry/scan.rs | 21 +++++ prebindgen/src/api/core/unfold.rs | 37 +++++++-- prebindgen/src/api/core/unfold/plan.rs | 17 ++++ prebindgen/src/api/core/unfold/tests.rs | 34 ++++++-- .../src/api/lang/jnigen/jni/tests/sealed.rs | 77 +++++++++++++++++++ 6 files changed, 182 insertions(+), 13 deletions(-) diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index 61fddc6e..c36504c2 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -59,6 +59,15 @@ use crate::SourceLocation; /// false by design for stored readings — `unrequire_output` leaves a cell whose /// converter genuinely cannot resolve, and a `SumTag` leaf never has one — so /// converter existence stays a lookup that answers `Option`. +/// +/// It does **not** claim a registry cell either, and the two are separate +/// questions. Holding a `TypeRef` means the model classified the type; whether +/// it is in a type table is the registry's business, and the registry states it +/// in three parts — a **cell** (the type entered the pipeline), a **root** (the +/// binding asked for it directly), an **entry** (a converter resolved). A +/// `SumTag` leaf's type makes the first and not the second, deliberately +/// (#282); see +/// [`Registry::reference_output`](crate::api::core::registry::Registry::reference_output). #[derive(Clone, Debug)] pub struct TypeRef { /// What the type means — the closed, destination-neutral classification. diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index ac920cb5..cb95e997 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -550,6 +550,27 @@ impl Registry { self.register_type_recursive(Direction::Output, reading, true); } + /// Register `reading` (and its nested positions) as an **output cell without + /// demanding a converter** — a type some plan *names* rather than one that + /// crosses. + /// + /// The third thing a table cell can mean, now said out loud. A cell records + /// that a type **entered the pipeline**; `root` records that the binding + /// asked for it *directly*; `entry` records that a converter resolved. This + /// makes the first without the second, which is exactly what a + /// [`SumTag`](crate::api::core::unfold::LeafSource::SumTag) selector needs: + /// it names *which* sum it chooses between, and that sum has no whole-value + /// output converter at all, so requiring one would fail resolution (#282). + /// + /// **Not [`require_output`](Self::require_output) with a flag.** That one is + /// `root = true` by definition — its whole job is to say a converter must + /// exist. Registration and demand are separable facts and this is the door + /// for the first alone; `ensure_entry`'s `root |= root` means calling it for + /// a type the binding did declare cannot weaken anything. + pub(crate) fn reference_output(&mut self, reading: &crate::api::core::flat::TypeRef) { + self.register_type_recursive(Direction::Output, reading, false); + } + /// Drop `ty` from the required-output scan set. The type's table entry is /// left intact (so [`crate::api::core::resolve`]'s PASS A still resolves it /// if it can, and emits it when resolved), but a `None` resolution no longer diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index efc8b7da..f343098b 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -556,6 +556,35 @@ pub struct SumDecon { /// including the `Option` / `Vec` layers, which the boundary-only pass /// does not reach — is dropped here as the plan takes over. /// +/// Put every leaf's `out_ty` in the table, and demand a converter for the ones +/// that need one. +/// +/// **Every leaf is registered; only a converter-bearing leaf is a root** (#282). +/// The two are separate facts and this is the one place a sum plan states both: +/// a cell says the type entered the pipeline, a root says the binding needs its +/// conversion to resolve. The `SumTag` selector is registered and not required — +/// it names *which* sum it chooses between, and a sum has no whole-value output +/// converter, so requiring one would fail resolution over a type that never +/// crosses whole. +/// +/// This used to `filter` the selector out entirely, which left its `out_ty` +/// with a cell only when the adapter happened to declare the sum separately — +/// true for jnigen via `export_type`, and not true at all for a registry +/// assembled without declarations. The invariant holds by construction now +/// rather than by declaration order. +fn register_leaves( + registry: &mut crate::api::core::registry::Registry, + leaves: &[UnfoldLeaf], +) { + for leaf in leaves { + if leaf.has_converter() { + registry.require_output(&leaf.out_ty); + } else { + registry.reference_output(&leaf.out_ty); + } + } +} + /// Runs in `write_rust` right after [`apply_value_structs`] and before `resolve`. pub fn apply_sum_returns( registry: &mut Registry, @@ -642,9 +671,7 @@ fn wire_fixed_returns( registry.unrequire_output(layer); } } - for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty); - } + register_leaves(registry, &vd.leaves); let plan = UnfoldPlan { source: vd.source.clone(), decon: Some(decon.clone()), @@ -709,9 +736,7 @@ fn wire_fixed_callbacks( if registry.callback_arg_plans.contains_key(&key) { continue; } - for leaf in vd.leaves.iter().filter(|l| l.has_converter()) { - registry.require_output(&leaf.out_ty); - } + register_leaves(registry, &vd.leaves); let plan = UnfoldPlan { source: vd.source.clone(), decon: Some(decon.clone()), diff --git a/prebindgen/src/api/core/unfold/plan.rs b/prebindgen/src/api/core/unfold/plan.rs index 52ba7225..8c7da082 100644 --- a/prebindgen/src/api/core/unfold/plan.rs +++ b/prebindgen/src/api/core/unfold/plan.rs @@ -192,6 +192,15 @@ pub enum LeafSource { /// assigns it per `match` arm — so it has no path. Emitted once, ahead of /// the groups it selects between (see /// [`crate::api::core::unfold::apply_sum_returns`]). + /// + /// Its [`out_ty`](UnfoldLeaf::out_ty) is **the sum**, not the `i32` — it + /// carries *which* sum it chooses between, which is how the emitter finds + /// the enum to `match`. That type is **registered and not required** (#282): + /// it gets a table cell like every other leaf's, but no root, because a sum + /// has no whole-value output converter and demanding one would fail + /// resolution over a type that never crosses whole. The reading comes from + /// the declaration — [`Variant::type_ref`](crate::api::core::flat::Variant::type_ref) + /// — never from an adapter composing one out of a name. SumTag, /// A payload field of ONE alternative of a decomposed sum, reached through /// a **variant pattern** rather than an accessor chain or a field chain: @@ -339,6 +348,14 @@ impl UnfoldLeaf { /// selector: it is assigned per `match` arm, never converted, so requiring /// a converter for it would make every sum depend on an unrelated `i32` /// crossing existing in the binding. + /// + /// **This is the root question, not the registration question.** Every + /// leaf's `out_ty` gets a table cell; this decides which of them the + /// binding additionally *demands* a converter for. A cell says the type + /// entered the pipeline, a root says the binding asked for it directly, and + /// an entry says one resolved — three separate claims, and a `SumTag` leaf + /// makes only the first (#282). See + /// [`Registry::reference_output`](crate::api::core::registry::Registry::reference_output). pub fn has_converter(&self) -> bool { self.source != LeafSource::SumTag } diff --git a/prebindgen/src/api/core/unfold/tests.rs b/prebindgen/src/api/core/unfold/tests.rs index 11846a87..9d3a4e18 100644 --- a/prebindgen/src/api/core/unfold/tests.rs +++ b/prebindgen/src/api/core/unfold/tests.rs @@ -1689,11 +1689,17 @@ fn duplicate_declarations_collected() { /// The tag + group leaves a sum decomposition is made of. Mirrors what the /// JNI adapter synthesizes: a selector followed by one group per alternative. +/// +/// The selector's `out_ty` is **the sum**, as `synth_sum_leaves` stores it — it +/// names *which* sum it chooses between, which is how the emitter finds the enum +/// to `match`. It said `i32` here, the tag's wire type, which made the +/// registration test prove only that *some* converter-free leaf gets a cell +/// rather than that the selector registers the sum it names (#282). fn reading_sum_decon() -> SumDecon { let tag = UnfoldLeaf { name: "tag".to_string(), path: vec![], - out_ty: tref(syn::parse_quote!(i32)), + out_ty: tref(syn::parse_quote!(Reading)), identity: false, nullable: false, source: LeafSource::SumTag, @@ -1756,12 +1762,26 @@ fn sum_return_is_a_fixed_builder_plan() { // requirement into a binding that has no `i32` crossing of its own. assert!(!plan.leaves[0].has_converter()); assert!(plan.leaves[1].has_converter()); - assert!( - !reg.output_types - .get(&TypeKey::from_type(&syn::parse_quote!(Reading))) - .is_some_and(|c| c.root), - "a sum has no whole-value converter, so its return must not require one" - ); + // #282's invariant, stated over the plan rather than over one type name: + // EVERY leaf's `out_ty` is registered, and only a converter-bearing leaf is + // a root. The assertion this replaced could state neither half — it read + // `!...is_some_and(|c| c.root)` on `Reading`, which is also true when the + // cell is ABSENT, and absent is what it was: this fixture's registry + // declares nothing, so it passed for the wrong reason. + for leaf in &plan.leaves { + let cell = reg + .output_types + .get(&leaf.out_ty.key()) + .unwrap_or_else(|| panic!("leaf `{}` registers its out_ty", leaf.name)); + assert_eq!( + cell.root, + leaf.has_converter(), + "leaf `{}`: a cell says the type entered the pipeline, a root says \ + the binding demands its converter — the selector makes only the \ + first, because a sum has no whole-value output converter", + leaf.name + ); + } } /// `Option` and `Vec` layers ride the existing shape fold — a sum needs diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs index b09d23f0..d8e16529 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/sealed.rs @@ -659,6 +659,83 @@ fn sum_is_its_own_type_kind() { assert!(cfg.special_decl()); } +/// What the registry holds for a `sealed_class!` sum, in all three parts (#282). +/// +/// The `SumTag` selector's `out_ty` is the **sum**, not the `i32` — it names +/// which sum it chooses between. That type is *registered* and *not required*, +/// and those are separate claims a single predicate cannot state: +/// +/// * a **cell** says the type entered the pipeline; +/// * a **root** says the binding demands its converter — cleared here, because +/// a sum crosses decomposed and has no whole-value output converter; +/// * an **entry** says one resolved — present INPUT-side (`sum_input_body` +/// decodes a whole `JObject`), absent OUTPUT-side, and that asymmetry is the +/// design, not an accident. +/// +/// Asserted against a real `Registry` rather than a hand-assembled one, because +/// the claim is about what a *declaration* produces — and that is the limit of +/// what it can claim. It does **not** pin #282: `sealed_class!(Reading)` creates +/// the output cell through `export_type` whether or not the selector registers +/// anything, so this passes against the filtered loop too. The selector's own +/// half is pinned in `core::unfold`'s `sum_return_is_a_fixed_builder_plan`, +/// whose fixture carries the sum as the tag's `out_ty` exactly so it can fail +/// when the leaf stops registering it. +#[test] +fn a_sums_registry_cells_are_registered_but_not_required() { + let loc = myflat_loc(); + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum Reading { + Missing, + Exact(i64), + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn read_one() -> Reading { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::sealed_class!(Reading)) + .fun(crate::fun!(read_one)), + ); + let gen = jni.build_with(registry).expect("resolve"); + let reg = gen.registry(); + let key = TypeKey::from_type(&syn::parse_quote!(Reading)); + + let input = reg.input_types.get(&key).expect("input cell"); + let output = reg.output_types.get(&key).expect("output cell"); + + // Registered both ways — the declaration put them there. + // Required neither way — `boundary_only_types` clears the root, because the + // sum crosses in pieces. + assert!(!input.root, "a declared sum crosses decomposed, not whole"); + assert!(!output.root, "a declared sum crosses decomposed, not whole"); + + // The asymmetry: Kotlin → Rust decodes a whole `JObject`; Rust → Kotlin is + // always flattened, so there is nothing to resolve. + assert!( + input.entry.is_some(), + "the input direction has a whole-object decoder" + ); + assert!( + output.entry.is_none(), + "the output direction has none — a sum crosses flattened, always" + ); +} + /// A `Vec` of tag-gated groups has variable arity, so it cannot ride the /// fixed-layout `fromParts` bridge — the same reason `Vec` is /// rejected. The guard has to peel `Vec` before asking `type_kind`, which From 8319db51aaa54cdeeed046a0282efaa5afbddcaa Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 14:14:57 +0200 Subject: [PATCH 50/52] jnigen: one reading of the movability rule (#223 item 1) (#307) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * jnigen: one reading of the movability rule (#223 item 1) `plan.rs` says of `steps_are_movable`: > This is the one place the rule is written… Two readings of it would > drift, and the disagreement would be a borrow handed to an owning > converter. `reach_leaf_flat` was the second reading, spelled `path.iter().all(PathStep::is_plain_field)`, against `encode_plan_leaves`' `steps_are_movable(&path)`. They already disagreed: the rule permits a TRAILING optional field — a `None` arm still hands the whole `Option` over by value — and the restatement did not. So where `place_is_owned` granted an owned `out_ty` on the strength of the rule, the emitter projected `(&(&__src.a).b).clone()` and handed a borrow to the owning converter that `out_ty` had selected. PR#221's P1, exactly, one path shape away. Its comment defended the restatement: a trailing optional cannot reach return delivery, because a nullable leaf is routed to callback delivery in `single_return`. True — and that is the shape of the hazard, not a defence against it. A local restatement can disagree with the rule for as long as an invariant somewhere else keeps the disagreement unreachable. The optional-step guard beside it had the same shape of hole. It asked its `path` PARAMETER, and `wrapper.rs` rebases onto a hoisted local and hands over the remaining suffix — `Hoisted::innermost` having stripped the prefix that bound it, optional step and all. So the guard passed exactly when the hoist was the conditional one, which is the case that cannot compose: an `Option` local with a field read hung off it. It asks `leaf.path` now. Both were unreachable, and neither was unreachable for a reason the emitter states. **No test called a reach function directly** — every pin was an end-to-end string match on generated Rust, which is why a latent divergence had no failing test. Three now do, and each fails against the code it replaced; the movability one reports the defect verbatim: `got '(& (& __src . a) . b) . clone ()'`. They ask `test_util::reading` rather than `Flat::classify`, which #280 sealed to `api::core` — a test under `api::lang` meeting that boundary is the boundary working. Goldens byte-identical: neither divergence is reachable today. Verified: 637 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild, covertest-kotlin 49/49. * Review: the fixtures build leaves the resolver can produce `leaf()` set `LeafSource::Field` for every fixture, including the two identity leaves and the one with a `Call` step. Production pairs the source with the path shape: an identity leaf is `Accessor` (`unfold.rs`'s `DeconRecord::Identity` arm), and `Field` belongs only to the synthesized by-value `data_class` decomposition, whose paths are field idents and never calls. So the fixtures exercised a leaf the resolver cannot build. Not cosmetic, because `source` decides the terminal treatment: a `Field` leaf is CLONED out of the place it reached. That clone was landing in the movability test's failure output, which reported got `(& (& __src . a) . b) . clone ()` for a defect that, on the accessor leaf this actually models, produces got `& (& __src . a) . b` Same assertion, same discrimination — both tests still fail against both old implementations — but the evidence now shows what the divergence really emits rather than a clone the shape would not have. `source` also stops being a value every fixture happens to share: `a_field_leaf_is_cloned_out_of_its_place` covers what `Field` means, so the parameter is load-bearing in the tests as well as in the emitter. Verified: 638 lib tests, clippy on 1.85.0 and stable, fmt, regen-check byte-identical after a forced rebuild, covertest-kotlin 49/49. * jnigen: one name, one enum question (#223 cheap wins) Three places where one question had two implementations — #223's thesis, in the naming and classification layer it does not list. **`sum_slot_name` was a byte-for-byte copy** of `sum_slot_fragment`, same lower-first-char plus `_` join. `kotlin_emit` called the copy at one site and the shared `sum_field_prop_name` at two others, so a slot name and the property inside it came from different files. Deleted. **`classify_field` asked the spelling where the model has the answer.** It called `is_kotlin_enum` twice, on two spellings, where `flat_input` asks `is_kotlin_enum_reading`. `builder.rs` documents the difference: a `Box` field is `false` for the first and `true` for the second, so a wrapped enum field would classify as a plain leaf and render as its wire rather than the Kotlin enum class — the #273 family, output-side. It asks once now, of `bare_ref`, the already-peeled reading sitting beside it. Optionality stays the caller's fact: `enum_probe` peels `Option` as well as borrows, so probing the unpeeled reading would make `Priority` and `Option` indistinguishable and collapse two arms into one. I tried to prove this with a `Box` field on `WrappedFields`, the fixture #294 added for exactly this pairing. **It does not resolve** — `TypeKey("Box < Priority >")` is unresolved output-side — and reverting the classification change leaves it failing identically, so that is a PRE-EXISTING capability gap and not this change's to fix. Reported separately; the fixture is not in this commit. The change stands on the question being the right one to ask, not on a demonstration it cannot yet have. **Two camel-casers named one Kotlin property.** `render_data_class_source` DECLARED it with `kt_snake_to_camel`; `flat_input`'s access expression and its `GetFieldID` slot name used `util::snake_to_camel`, which additionally lower-cases the first character. They agree for a conventional lower-snake field and only for that — a field spelled `Xyz` is declared `Xyz` and read as `xyz`, and `GetFieldID` for a name that is not the declared one fails at runtime. One `kotlin_property_name` now serves the declaration and both readers. `snake_to_camel` stays where it names PARAMETERS, a namespace with no declaration to match; `symbols.rs`'s mangling warning already used the kept caser and passes pre- and post-mangle names deliberately, so it needed no change. Goldens byte-identical for all three, as expected: the two functions were textually identical, no in-tree field name is unconventional, and the enum divergence needs a shape that does not currently resolve. Verified: 637 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild, covertest-kotlin 49/49. --- .../src/api/lang/jnigen/jni/emit/delivery.rs | 165 +++++++++++++++++- .../api/lang/jnigen/jni/emit/flat_input.rs | 7 +- .../src/api/lang/jnigen/jni/kotlin_emit.rs | 24 +-- prebindgen/src/api/lang/jnigen/jni/render.rs | 19 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 37 ++-- 5 files changed, 208 insertions(+), 44 deletions(-) diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs index 5312f056..f52da0b2 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/delivery.rs @@ -456,8 +456,16 @@ pub(crate) fn reach_leaf_flat( // somewhere to go. This derivation has none — it yields a plain Rust value, // not a `JObject` that could be null — so the shape is refused here rather // than composed into code that cannot type-check in the consumer's crate. + // + // Asked of the leaf's OWN path, not of `path`. The caller may hand a + // suffix: `wrapper.rs` rebases onto a hoisted local, and `Hoisted::innermost` + // strips the prefix that bound it — including any optional step inside it. + // Checking the parameter would therefore pass exactly when the hoist is the + // conditional one, which is the case that cannot compose (an `Option` + // local with a field read hung off it). The full path is what the shape + // question is about. assert!( - !path.iter().rev().skip(1).any(PathStep::is_optional), + !leaf.path.iter().rev().skip(1).any(PathStep::is_optional), "jnigen unfold: leaf `{}` reaches through an optional step but is \ delivered as a single return value, which has no `None` arm — this \ shape needs callback delivery", @@ -475,17 +483,21 @@ pub(crate) fn reach_leaf_flat( // so ownership is the enclosing form's: only a consuming one gives its // fields away. // - // A trailing `Option` step cannot arrive here at all: return delivery has - // no `None` arm for the absent case, so a nullable leaf is routed to - // callback delivery when the plan picks its `Delivery` — see - // `single_return` in `core/unfold.rs`. `is_plain_field` is what that rules - // out, and it stays as the local statement of the same fact. + // How to project that place is `steps_are_movable`'s question, and it is + // asked there rather than restated here. This used to spell it + // `all(is_plain_field)`, defending the restatement on the grounds that a + // trailing `Option` cannot reach return delivery anyway — true, and enforced + // in `single_return` (`core/unfold.rs`), which is precisely why a local + // restatement could disagree with the rule for as long as the invariant held + // somewhere else. `plan.rs` says two readings would drift and the + // disagreement would be a borrow handed to an owning converter; this is the + // second reading, removed. let reached_is_ours = if leaf.identity { !matches!(leaf.out_ty.syntax(), syn::Type::Reference(_)) } else { consuming }; - if reached_is_ours && path.iter().all(PathStep::is_plain_field) { + if reached_is_ours && steps_are_movable(path) { let segs: Vec<&syn::Ident> = path.iter().map(PathStep::ident).collect(); return quote!(#base #(.#segs)*); } @@ -1402,3 +1414,142 @@ pub(crate) fn leaf_ty_is_prim(registry: &impl Conversions, out_ty: & }; proj_ok && matches!(jni_field_access(&entry.destination), Some((_, _, false))) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::core::unfold::{LeafSource, UnfoldLeaf}; + /// A `TypeRef` through the model. `Flat::classify` is sealed to `api::core` + /// (#280), so a test under `api::lang` asks the sanctioned probe helper + /// rather than reaching around the seal — which is the seal working. + use crate::api::test_util::reading as tref; + + /// A leaf as the resolver builds one. `source` is not decoration: it + /// decides the terminal treatment (a `Field` leaf is CLONED out of the + /// place it reached), and production pairs it with the path shape — + /// `Accessor` for identity leaves and accessor chains (`unfold.rs`'s + /// `DeconRecord::Identity` arm), `Field` only for the synthesized + /// by-value `data_class` decomposition, whose paths are field idents and + /// never calls. A fixture that mixed them would exercise a leaf the + /// resolver cannot produce. + fn leaf( + out_ty: syn::Type, + path: Vec, + identity: bool, + source: LeafSource, + ) -> UnfoldLeaf { + UnfoldLeaf { + name: "probe".to_string(), + path, + out_ty: tref(out_ty), + identity, + nullable: false, + source, + group: None, + } + } + + fn qualify(id: &syn::Ident) -> syn::Path { + syn::parse_quote!(myflat::#id) + } + + /// `reach_leaf_flat` projects the place `steps_are_movable` says is movable + /// — the two are one rule, not two readings of one. + /// + /// The trailing-optional path is the case that discriminates: it IS movable + /// (a `None` arm still hands the whole `Option` over by value), and the + /// `all(is_plain_field)` restatement this replaced called it not-movable. + /// Where the plan had already granted an owned `out_ty` on the strength of + /// `steps_are_movable`, that disagreement is a borrow reaching an owning + /// converter — `plan.rs`'s stated hazard, and PR#221's P1. + #[test] + fn a_movable_place_is_projected_as_a_move() { + for path in [ + vec![PathStep::field(syn::parse_quote!(a), false)], + vec![ + PathStep::field(syn::parse_quote!(a), false), + PathStep::field(syn::parse_quote!(b), false), + ], + // Movable by `steps_are_movable`; NOT by `all(is_plain_field)`. + vec![ + PathStep::field(syn::parse_quote!(a), false), + PathStep::field(syn::parse_quote!(b), true), + ], + ] { + assert!(steps_are_movable(&path), "fixture must be movable"); + let l = leaf( + syn::parse_quote!(Owned), + path.clone(), + true, + LeafSource::Accessor, + ); + let got = reach_leaf_flat(&qualify, &l, &path, quote!(__src), false, false).to_string(); + assert!( + !got.contains('&') && !got.contains("clone"), + "a movable place is moved, not borrowed or cloned — got `{got}`" + ); + } + } + + /// A borrow stays a borrow: an identity leaf whose `out_ty` is a reference + /// did not own what it reached, whatever the path shape says. + #[test] + fn a_borrowed_out_ty_is_never_moved() { + let path = vec![PathStep::field(syn::parse_quote!(a), false)]; + let l = leaf( + syn::parse_quote!(&Owned), + path.clone(), + true, + LeafSource::Accessor, + ); + let got = reach_leaf_flat(&qualify, &l, &path, quote!(__src), false, false).to_string(); + assert!( + got.contains('&'), + "a borrowed out_ty keeps its borrow — got `{got}`" + ); + } + + /// A `Field` leaf is cloned out of the place it reached, whatever the path + /// shape says — its converter takes the field type as written. + /// + /// The counterpart of the move above, and what keeps `source` load-bearing + /// in these fixtures rather than a value they all happen to share. + #[test] + fn a_field_leaf_is_cloned_out_of_its_place() { + let path = vec![ + PathStep::field(syn::parse_quote!(a), false), + PathStep::field(syn::parse_quote!(b), false), + ]; + let l = leaf( + syn::parse_quote!(Owned), + path.clone(), + false, + LeafSource::Field, + ); + let got = reach_leaf_flat(&qualify, &l, &path, quote!(__src), false, false).to_string(); + assert!( + got.contains("clone"), + "a non-consuming field leaf clones rather than moves — got `{got}`" + ); + } + + /// The optional-step guard asks the LEAF's path, not the caller's slice. + /// + /// `wrapper.rs` rebases onto a hoisted local and hands over the remaining + /// suffix, so checking the parameter would pass exactly when the hoist is + /// the conditional one — an `Option` local with a field read hung off it, + /// which cannot compose. Passing the suffix here mimics that rebase. + #[test] + #[should_panic(expected = "which has no `None` arm")] + fn an_optional_step_in_a_stripped_prefix_is_still_refused() { + let full = vec![ + PathStep::call(syn::parse_quote!(get_it), true, false), + PathStep::field(syn::parse_quote!(a), false), + ]; + let l = leaf(syn::parse_quote!(Owned), full, false, LeafSource::Accessor); + // The suffix a rebase would hand over — the optional call is gone from + // it, and used to take the guard with it. + let rest = vec![PathStep::field(syn::parse_quote!(a), false)]; + let _ = reach_leaf_flat(&qualify, &l, &rest, quote!(__vf0), false, false); + } +} diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 6f9c95fc..7bbcbfd4 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -35,8 +35,9 @@ pub(crate) fn struct_input_body( // field now, because the element models a field list rather than a // `syn::Fields` shape. let fname_ident = field.name.clone()?; - let fname = fname_ident.to_string(); - let camel = mangle_kotlin_ident(&snake_to_camel(&fname)); + // The name the property was DECLARED with — `GetFieldID` takes the + // slot's exact name, so this cannot be derived a second way. + let camel = kotlin_property_name(&fname_ident); let err_prefix = format!("{struct_name}.{camel}: {{}}"); let raw_ident = format_ident!("__{}_raw", fname_ident); @@ -1459,7 +1460,7 @@ fn build_flat_struct_node( "only named-field structs can flatten", )); }; - let fcamel = mangle_kotlin_ident(&snake_to_camel(&fident.to_string())); + let fcamel = kotlin_property_name(&fident); let child_native = format!("{native_prefix}_{}", fident); let field_ref = if nullable_context { format!("{access_prefix}?.{fcamel}") diff --git a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs index 71033480..bb266d3e 100644 --- a/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs +++ b/prebindgen/src/api/lang/jnigen/jni/kotlin_emit.rs @@ -423,26 +423,6 @@ impl Declarations { } } -/// Slot name of one variant field in the flattened `fromParts` signature: -/// `_` (`periodicQueries_period`, `pair_v0`) — the -/// existing nested-prefix convention, with `_` marking the variant boundary -/// exactly as core's `_` leaf names do. -/// -/// Keyed on the **Kotlin** variant class name, not the Rust ident, so a -/// `variant!(V).name(...)` rename carries through to the slots and the -/// emitted surface stays self-consistent. -fn sum_slot_name(kotlin_variant: &str, property: &str) -> String { - // The variant class name is PascalCase; lower its first character so the - // slot reads as an ordinary Kotlin parameter (`PeriodicQueries` → - // `periodicQueries_period`). - let mut chars = kotlin_variant.chars(); - let head: String = match chars.next() { - Some(c) => c.to_lowercase().collect(), - None => String::new(), - }; - format!("{head}{}_{property}", chars.as_str()) -} - /// Owned counterpart of [`TypedHandle`] — used internally so the /// `collect_typed_handles` helper doesn't have to hand out borrows of /// `self.types`. @@ -654,7 +634,7 @@ impl Declarations { for field in &alt.fields { let prop = sum_field_prop_name(&field.member()); let ty = self.sum_payload_kt_type(registry, &sum.name, &alt.name, &prop, field); - factory = factory.param(KtParam::new(sum_slot_name(&vname, &prop), ty)); + factory = factory.param(KtParam::new(sum_slot_fragment(&vname, &prop), ty)); } } let mut body = Code::new(); @@ -664,7 +644,7 @@ impl Declarations { let args: Vec = alt .fields .iter() - .map(|f| sum_slot_name(&vname, &sum_field_prop_name(&f.member()))) + .map(|f| sum_slot_fragment(&vname, &sum_field_prop_name(&f.member()))) .collect(); let ctor = if alt.is_empty() { vname diff --git a/prebindgen/src/api/lang/jnigen/jni/render.rs b/prebindgen/src/api/lang/jnigen/jni/render.rs index 1910f3c1..da834d1b 100644 --- a/prebindgen/src/api/lang/jnigen/jni/render.rs +++ b/prebindgen/src/api/lang/jnigen/jni/render.rs @@ -105,7 +105,7 @@ pub(crate) fn build_data_class( item_struct.name ) }); - let kotlin_field_name = mangle_kotlin_ident(&kt_snake_to_camel(&field_ident.to_string())); + let kotlin_field_name = kotlin_property_name(field_ident); let owner = format!("{}.{}", item_struct.name, field_ident); // The declaration reads ONE direction — output — because that is the @@ -2194,6 +2194,23 @@ pub(crate) fn kt_type_short(ty: &kt::KtType) -> String { ty.render(&mut kt::ImportSet::new("")) } +/// The Kotlin property name of one struct field — the single derivation, so the +/// site that DECLARES a property and the sites that ACCESS it cannot disagree. +/// +/// They did. `render_data_class_source` declared it through `kt_snake_to_camel`, +/// while `flat_input`'s access expression and JVM-slot name went through +/// `util::snake_to_camel`, which additionally lower-cases the first character. +/// The two agree for a conventional lower-snake field and only for that: a field +/// spelled `Xyz` was declared `Xyz` and read as `xyz`, and a JNI `GetFieldID` +/// for a name that is not the declared one fails at runtime. +/// +/// `kt_snake_to_camel` is the behaviour kept, because the declaration is what a +/// Kotlin property actually gets called; `snake_to_camel` stays where it names +/// PARAMETERS, which is a different namespace with no declaration to match. +pub(crate) fn kotlin_property_name(field: &syn::Ident) -> String { + mangle_kotlin_ident(&kt_snake_to_camel(&field.to_string())) +} + pub(crate) fn kt_snake_to_camel(s: &str) -> String { let mut out = String::new(); let mut upper = false; diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 08d85ca7..27005bfb 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -276,17 +276,32 @@ pub(crate) fn classify_field( let fqn = projection_leaf_kt(ext, &proj)?.to_string(); return Some(PlanFieldKind::Projection { conv, proj, fqn }); } - // Bare enum leaf. - if ext.is_kotlin_enum(&effective_ty) { - let kotlin = field_entry.metadata.kotlin_name.clone()?; - return Some(PlanFieldKind::Enum { conv, kotlin }); - } - // `Option` leaf. - if let Some(inner) = optional_inner { - if ext.is_kotlin_enum(inner.syntax()) { - let kotlin = registry.output_entry(inner)?.metadata.kotlin_name.clone()?; - return Some(PlanFieldKind::OptionEnum { conv, kotlin }); - } + // Enum leaf, bare or under `Option` — asked ONCE, of the model, and of + // the already-peeled reading beside us. + // + // It used to ask `is_kotlin_enum` twice, of two spellings. That answers + // about the WRAPPER: `builder.rs` documents `Box` as `false` + // for it and `true` for the reading form, so a wrapped enum field fell + // through to the plain-leaf arm and rendered as its wire instead of the + // Kotlin enum class — the #273 family, output-side. `flat_input.rs` had + // already moved to the reading; this is the other half of the same + // question finally giving the same answer. + // + // Optionality stays the CALLER's fact rather than the probe's: + // `enum_probe` peels `Option` as well as borrows, so asking it about + // the unpeeled reading would make `Priority` and `Option` + // indistinguishable and collapse the two arms into one. + if ext.is_kotlin_enum_reading(bare_ref) { + return match optional_inner { + None => { + let kotlin = field_entry.metadata.kotlin_name.clone()?; + Some(PlanFieldKind::Enum { conv, kotlin }) + } + Some(inner) => { + let kotlin = registry.output_entry(inner)?.metadata.kotlin_name.clone()?; + Some(PlanFieldKind::OptionEnum { conv, kotlin }) + } + }; } // Nested plain data-class (optionally under `Option`). let inner_ty = bare.clone(); From 63eeafd5cf5b110b5d9a6ac96c0e14140d7bec3a Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Mon, 3 Aug 2026 15:34:17 +0200 Subject: [PATCH 51/52] jnigen: the transparent bridge gets its outbound half (closes #309) (#310) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * jnigen: the transparent bridge gets its outbound half (#309) The model erases `Box` and `Cow` deliberately — `Box` IS `Priority` to every destination language. #294 gave the input selector a last resort for the case no layer arm claims, a wrapper over a TERMINAL. `select_output_type` never got the twin: arms 1-3 run and it returns `None`. So an erased wrapper resolved inbound and not outbound. `Box` was a parameter this binding could take and a return it could not give, for a wrapper the model exists to make invisible. The gap was invisible because a wrapper over something WITH a layer arm resolves both ways through that arm — `Box>` works, which looks like proof `Box` is handled — and `Box` works too, because the `Str` arm dispatches on `kind()`. Only a wrapper over a plain `TypeKind::Named` needs the bridge. One arm covers `Box`, `Box` and `Box` alike: `output_terminal` misses all three the same way, by keying on the SPELLING, so no config sits under `Box < Priority >`. The dual inverts two lines, because the wrappers come off rather than go on — `read_through_erased_wrappers` was already the operation, and is already used for this job in `emit/wrapper.rs`. Everything else is direction-independent: `subs`, `destination`, `niches` and `metadata` mean the same thing either way, and inheriting the inner's metadata is what keeps `Box` presenting as the Kotlin enum class rather than losing it behind the wrapper. Both guards carry over, the second with its own outbound reason: a borrow's output route is the clone-into-a-fresh-handle arm, which builds its wire from a reference and hands back no owned value to read the wrapper off. Inbound the same guard is about `E0106` — the shapes coincide, the reasons do not. Placement is symmetric with the input side and deliberate: step 4 sits after every layer arm, so nothing that resolves today changes route. It is not reached for `Optional`/`Sequence` failures, which return early exactly as they do inbound, because a wrapper over those is already bridged inside the arm. Measured against the shape that prompted #309: it now emits let __inner = *v; Priority_to_jint_447102d2(env, __inner)? Goldens byte-identical — this adds routes for shapes that previously reached `None`, and no in-tree example has one yet. The fixtures come in their own commit. Verified: 638 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild. * jnigen: the transparent bridge runs the inner's stages (#309) `input_transparent_bridge` called the inner converter's function directly and left `pre_stages` empty. Every other composing arm goes through `composed_inner_input` / `composed_inner_output` for one reason: a `convert!`-declared type reaches its Rust value through those stages. So a `Box` over one skipped them. Not a subtly wrong value — **the generated crate does not compile**: let __inner = jlong_to_u64_4384a5d6(env, v)?; ::std::boxed::Box::new(__inner) // ^^^^^^^ expected `Duration`, found `u64` [E0308] `boxed_duration_echo` is the fixture, and it is load-bearing rather than illustrative: reverting this commit with it in place fails the build with that error. `Duration` is `convert!`-declared with `jlong -> u64 -> Duration`, so the wrapper sits over a chain rather than a single call. Both directions now emit the full chain, and the Kotlin exercise runs it at JVM runtime — a `Box` crosses exactly as a bare `Duration` does, which is what the model erasing the wrapper is supposed to mean. The outbound half added in the previous commit was written this way from the start; this is its inbound peer, in its own commit because it is a bug fix rather than the new capability. Goldens move by the fixture alone (+117 lines, all additions). Verified: 638 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild, covertest-kotlin 49/49 including the new exercise. * core: a wrapped spelling is ORDERED after the spelling it delegates to (#309) Whoever converts `Box` does it by delegating to `T`'s converter and putting the wrapper back. That is a real dependency, and the `kind` walk cannot see it: `Box` classifies as whatever `T` is, so the two share a classification and differ only in spelling. `subs` said "this is required". Nothing said "this comes first" — and `convert_with` is a SINGLE pass in dependency order, so a delegating converter needs its inner already built. `immediate_edges` now yields the stripped spelling as an edge when the reading has erased wrappers. **The inbound bridge has been resolving by alphabetical luck since #294.** Roots are visited in key order, so `Box` resolved because some other root's fields happened to pull `Payload` in earlier. Measured on a fixture where that luck runs out: renaming `Priority` to `APriority` makes `Box` resolve and leaves `Box` unresolved, purely because "ZSample" sorts after "Box < ZSample >". A capability that depends on the alphabet is not one. `an_erased_wrapper_over_a_terminal_crosses_both_ways` is the acceptance test for #309 as a whole, and needs both this and the outbound arm: with either reverted it fails, naming the unresolved wrapped spellings. It asserts all three terminal kinds together — enum, handle, data class — because they miss the terminal lookup the same way, which is the claim that one arm covers them all. It also asserts the wrapped and bare enum fields present as the SAME Kotlin type, which is what erasing the wrapper is supposed to mean. Goldens byte-identical: every in-tree example already resolved, so this changes no output. It replaces luck with an edge. Verified: 639 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild. * jnigen: the fixture that could not be built (#309) `WrappedFields` gained `Box` beside `Priority`, the pairing it already carries for `Box>` / `Option`. That first pair rides the `Optional` layer arm and always worked; the second classifies as `Named`, no arm claims it, and outbound there was no route at all — which is what #309 is. This is the fixture whose failure to build FOUND the gap, while adding a demonstration for #308's `classify_field` fix. It now builds, and it proves both: public data class WrappedFields( val id: Long, val boxed: Long?, val plain: Long?, val boxedEnum: Priority, val plainEnum: Priority, ) `boxedEnum` presenting as `Priority` rather than as its `Int` wire is #308's change; that it presents at all is #309's. #308 landed correct and undemonstrable because no `Box` field could be built to show it — this is its first end-to-end evidence. The Kotlin exercise weighs the two enum fields against each other, so the claim under test is that a wrapped and a bare spelling of one type behave alike, not merely that the wrapped one compiles. Goldens move deliberately: +131 lines, the new capability. Verified: 639 lib tests, clippy on 1.85.0 and stable, fmt, regen-check after a forced rebuild, covertest-kotlin 49/49 including the new checks. * Review: a doc comment goes back to the function it describes `/// **Input** wrapper shape …` had been stranded since #294 inserted the transparent bridge between it and `input_wrapper_shape`, which has had no doc of its own ever since — while `output_wrapper_shape`'s doc calls itself "the dual of `input_wrapper_shape`", pointing at the undocumented one. Adding the OUTBOUND bridge moved that fragment onto an output converter, where a header reading "**Input** wrapper shape" is not merely stale but a contradiction. Review caught it there; the fix is to give it back to its owner rather than to relabel it. Also from review: the acceptance test claimed "each field type is wrapped and unwrapped in one struct", and only the enum is. The handle and the data class are wrapped only, and deliberately — what they show is that ONE arm serves every terminal kind, where a bare twin of each would test the terminal lookup instead of the bridge. The comment says that now. Verified: 639 lib tests, clippy on 1.85.0 and stable, fmt, `cargo doc`, regen-check byte-identical after a forced rebuild. --- examples/covertest-kotlin/build.rs | 1 + examples/covertest-kotlin/kotlin/REPORT.md | 1 + .../generated/io/prebindgen/covertest.kt | 15 +- .../io/prebindgen/covertest/model.kt | 18 ++ .../kotlin/io/prebindgen/covertest/Test.kt | 24 +- .../src/generated_bindings.rs | 218 +++++++++++++++++- examples/perftest-flat/src/ext.rs | 24 +- prebindgen/src/api/core/registry/scan.rs | 23 ++ .../src/api/lang/jnigen/jni/selector.rs | 6 +- .../api/lang/jnigen/jni/tests/value_form.rs | 121 ++++++++++ .../src/api/lang/jnigen/jni/trait_impl.rs | 95 +++++++- 11 files changed, 532 insertions(+), 14 deletions(-) diff --git a/examples/covertest-kotlin/build.rs b/examples/covertest-kotlin/build.rs index be7c3306..0d452a0e 100644 --- a/examples/covertest-kotlin/build.rs +++ b/examples/covertest-kotlin/build.rs @@ -577,6 +577,7 @@ fn main() { .fun(fun!(blob_value_echo)) .fun(fun!(arrays_echo)) .fun(fun!(duration_optional)) + .fun(fun!(boxed_duration_echo)) .fun(fun!(duration_boundary_echo)) // The converted analogue of `unsigned_emit`: a whole-value // callback argument, which encodes on its own path rather than diff --git a/examples/covertest-kotlin/kotlin/REPORT.md b/examples/covertest-kotlin/kotlin/REPORT.md index 2c636424..0b03679e 100644 --- a/examples/covertest-kotlin/kotlin/REPORT.md +++ b/examples/covertest-kotlin/kotlin/REPORT.md @@ -63,6 +63,7 @@ Base package: `io.prebindgen.covertest` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) - `blob_value_new` — `fun blobValueNew(secs: Long, id: ByteArray, chunks: List, onError: JniErrorHandler): BlobValue` - shaped by: return `BlobValue` decomposed → [stamp__secs, stamp__nanos, id, chunks] (Callback delivery) +- `boxed_duration_echo` — `fun boxedDurationEcho(value: ULong, onError: JniErrorHandler): ULong` - `boxed_elem_id_sum` — `fun boxedElemIdSum(ps: List, onError: JniErrorHandler): Long` - `boxed_latest` — `fun boxedLatest(a: SummaryVault, onError: JniErrorHandler, build: SummaryBuilder): R?` - shaped by: return `Summary` decomposed → [count, total] (Callback delivery) diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt index f2b1a914..c313ade2 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest.kt @@ -11,6 +11,7 @@ import io.prebindgen.covertest.model.Lookup import io.prebindgen.covertest.model.Marker import io.prebindgen.covertest.model.ObjectBoundary import io.prebindgen.covertest.model.Observation +import io.prebindgen.covertest.model.Priority import io.prebindgen.covertest.model.Stamp import io.prebindgen.covertest.model.Tagged import java.lang.ref.Cleaner @@ -248,10 +249,16 @@ public data class Payload(override val id: Long, override val seq: Int, override * `plain` is the control: the two fields must produce the same wire, since the * model says they are the same type. */ -public data class WrappedFields(val id: Long, val boxed: Long?, val plain: Long?) { +public data class WrappedFields(val id: Long, val boxed: Long?, val plain: Long?, val boxedEnum: Priority, val plainEnum: Priority) { public companion object { @JvmStatic - public fun fromParts(id: Long, boxed: Long?, plain: Long?): WrappedFields = WrappedFields(id, boxed, plain) + public fun fromParts( + id: Long, + boxed: Long?, + plain: Long?, + boxedEnum: Int, + plainEnum: Int, + ): WrappedFields = WrappedFields(id, boxed, plain, Priority.fromInt(boxedEnum), Priority.fromInt(plainEnum)) } } @@ -940,6 +947,8 @@ internal object CovNative { errorSink: Any, ): Any? + external fun boxedDurationEcho(value: Long, errorSink: Any): Long + external fun boxedElemIdSum(ps: List, errorSink: Any): Long external fun boxedLatest(a: Long, build: Any, errorSink: Any): Any? @@ -1334,6 +1343,8 @@ internal object CovNative { wBoxedValue: Long, wPlainPresent: Boolean, wPlainValue: Long, + wBoxedEnum: Int, + wPlainEnum: Int, errorSink: Any, ): Long diff --git a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt index 6388940d..1b14eb9f 100644 --- a/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt +++ b/examples/covertest-kotlin/kotlin/generated/io/prebindgen/covertest/model.kt @@ -1635,6 +1635,8 @@ public fun wrappedFieldsSum(w: WrappedFields, onError: JniErrorHandler): L w.boxed ?: 0L, w.plain != null, w.plain ?: 0L, + w.boxedEnum.value, + w.plainEnum.value, __bcap, ) if (__bcap.failed) return onError.run(__bcap.ze0) @@ -2057,6 +2059,22 @@ public fun durationOptional(value: ULong?, onError: JniErrorHandler): UL return __ret.let { if (it == -1L) null else it.toULong() } } +/** + * A transparent wrapper over a **`convert!`-declared** type, both directions. + * + * `Duration` reaches its Rust value through a staged chain + * (`jlong -> u64 -> Duration`), and the transparent bridge used to call the + * inner converter's function directly and leave `pre_stages` empty — so the + * stages were skipped and the rebuild put `Box::new` around a `u64`. Not a + * silent wrong value: `E0308` in the generated crate (#309). + */ +public fun boxedDurationEcho(value: ULong, onError: JniErrorHandler): ULong { + val __bcap = JniErrorHandlerCapture.acquire() + val __ret = CovNative.boxedDurationEcho(value.toLong(), __bcap) + if (__bcap.failed) return onError.run(__bcap.ze0) + return __ret.toULong() +} + /** * Round-trip [`DurationBoundary`] through the explicit object-input bridge. * diff --git a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt index 14f32315..d67b8001 100644 --- a/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt +++ b/examples/covertest-kotlin/kotlin/io/prebindgen/covertest/Test.kt @@ -59,6 +59,7 @@ import io.prebindgen.covertest.model.plainNoteEcho import io.prebindgen.covertest.model.blobValueNew import io.prebindgen.covertest.model.annotatedAlternateValue import io.prebindgen.covertest.model.celsiusDouble +import io.prebindgen.covertest.model.boxedDurationEcho import io.prebindgen.covertest.model.durationOptional import io.prebindgen.covertest.model.durationBoundaryEcho import io.prebindgen.covertest.model.durationEmit @@ -252,6 +253,13 @@ fun main() { check(durationOptional(0uL, boom) == 0uL) check(durationOptional(86_400_000uL, boom) == 86_400_000uL) + // A `Box` crosses as a bare `Duration` does — the wrapper is + // invisible to Kotlin, which is why the model erases it. Both + // directions run the full staged chain; skipping it put `Box::new` + // around a `u64` and did not compile (#309). + check(boxedDurationEcho(86_400_000uL, boom) == 86_400_000uL) + check(boxedDurationEcho(0uL, boom) == 0uL) + // The data-class properties are semantic `ULong` / `ULong?`, while the // native output factory receives primitive Longs (the optional one // niche-encoded). The echo's explicit object input also executes the @@ -1477,10 +1485,18 @@ fun main() { // are one type to the model, so both cross as `Long?` on the decoupled // `(present, value)` pair — the boxed one used to be read by path // segment as "not optional" and crossed as one boxed object. - check(wrappedFieldsSum(WrappedFields(1L, 2L, 4L), boom) == 7L) - check(wrappedFieldsSum(WrappedFields(1L, null, 4L), boom) == 5L) - check(wrappedFieldsSum(WrappedFields(1L, 2L, null), boom) == 3L) - check(wrappedFieldsSum(WrappedFields(1L, null, null), boom) == 1L) + // `Priority.LOW` weighs 1, `HIGH` weighs 10 — see `priority_weight`. + check(wrappedFieldsSum(WrappedFields(1L, 2L, 4L, Priority.LOW, Priority.LOW), boom) == 9L) + check(wrappedFieldsSum(WrappedFields(1L, null, 4L, Priority.LOW, Priority.LOW), boom) == 7L) + check(wrappedFieldsSum(WrappedFields(1L, 2L, null, Priority.LOW, Priority.LOW), boom) == 5L) + check(wrappedFieldsSum(WrappedFields(1L, null, null, Priority.LOW, Priority.LOW), boom) == 3L) + + // And over a TERMINAL (#309): `Box` had no outbound route at + // all, where `Box>` above rode the `Optional` layer arm. + // Both enum fields are declared `Priority` in Kotlin — the wrapper is + // invisible — and the pair differing only in spelling must weigh alike. + check(wrappedFieldsSum(WrappedFields(0L, null, null, Priority.HIGH, Priority.LOW), boom) == 11L) + check(wrappedFieldsSum(WrappedFields(0L, null, null, Priority.LOW, Priority.HIGH), boom) == 11L) // An absent `Option` must deliver `None`, not an error. Its // leaves are inert placeholders when the object is null, and a required diff --git a/examples/covertest-kotlin/src/generated_bindings.rs b/examples/covertest-kotlin/src/generated_bindings.rs index 8eb7c7f9..0c098364 100644 --- a/examples/covertest-kotlin/src/generated_bindings.rs +++ b/examples/covertest-kotlin/src/generated_bindings.rs @@ -684,6 +684,34 @@ pub(crate) unsafe fn Box_Box_Option_String_to_JString_299999e0<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn Box_Duration_to_jlong_0776c1ca<'a>( + env: &mut jni::JNIEnv<'a>, + v: Box, +) -> ::core::result::Result { + Ok({ + let __inner = *v; + { + let __inner_s0 = Duration_to_u64_e3980876(env, __inner) + .map_err(|__e| <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()))?; + u64_to_jlong_4384a5d6(env, __inner_s0)? + } + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn Box_Option_Summary_to_jlong_75560ba9<'a>( env: &mut jni::JNIEnv<'a>, v: Box>, @@ -744,6 +772,28 @@ pub(crate) unsafe fn Box_Option_i64_to_JObject_cf5a3724<'a>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn Box_Priority_to_jint_a16653ae<'a>( + env: &mut jni::JNIEnv<'a>, + v: Box, +) -> ::core::result::Result { + Ok({ + let __inner = *v; + Priority_to_jint_447102d2(env, __inner)? + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn Box_String_to_JString_027f6250<'a>( env: &mut jni::JNIEnv<'a>, v: Box, @@ -3583,10 +3633,38 @@ pub(crate) unsafe fn JObject_to_WrappedFields_f14f08c1<'env, 'v>( String, >>::from(format!("WrappedFields.plain: {}", e)))?; let plain = JObject_to_Option_i64_2ba9a5ed(env, &__plain_raw)?; + let __boxed_enum_jobj: jni::objects::JObject = env + .get_field(v, "boxedEnum", "Lio/prebindgen/covertest/model/Priority;") + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("WrappedFields.boxedEnum: {}", e)))?; + let __boxed_enum_raw: jni::sys::jint = env + .call_method(&__boxed_enum_jobj, "getValue", "()I", &[]) + .and_then(|val| val.i()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("WrappedFields.boxedEnum: {}", e)))?; + let boxed_enum = jint_to_Box_Priority_a16653ae(env, &__boxed_enum_raw)?; + let __plain_enum_jobj: jni::objects::JObject = env + .get_field(v, "plainEnum", "Lio/prebindgen/covertest/model/Priority;") + .and_then(|val| val.l()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("WrappedFields.plainEnum: {}", e)))?; + let __plain_enum_raw: jni::sys::jint = env + .call_method(&__plain_enum_jobj, "getValue", "()I", &[]) + .and_then(|val| val.i()) + .map_err(|e| <__JniErr as ::core::convert::From< + String, + >>::from(format!("WrappedFields.plainEnum: {}", e)))?; + let plain_enum = jint_to_Priority_447102d2(env, &__plain_enum_raw)?; perftest_flat::WrappedFields { id, boxed, plain, + boxed_enum, + plain_enum, } }) } @@ -9471,15 +9549,25 @@ pub(crate) unsafe fn WrappedFields_to_JObject_f14f08c1<'a>( env, v.plain.clone(), )?; + let ___boxed_enum: jni::sys::jint = Box_Priority_to_jint_a16653ae( + env, + v.boxed_enum.clone(), + )?; + let ___plain_enum: jni::sys::jint = Priority_to_jint_447102d2( + env, + v.plain_enum.clone(), + )?; let __obj = env .call_static_method( "io/prebindgen/covertest/WrappedFields", "fromParts", - "(JLjava/lang/Long;Ljava/lang/Long;)Lio/prebindgen/covertest/WrappedFields;", + "(JLjava/lang/Long;Ljava/lang/Long;II)Lio/prebindgen/covertest/WrappedFields;", &[ jni::objects::JValue::from(___id), jni::objects::JValue::Object(&___boxed), jni::objects::JValue::Object(&___plain), + jni::objects::JValue::from(___boxed_enum), + jni::objects::JValue::from(___plain_enum), ], ) .and_then(|__v| __v.l()) @@ -9866,6 +9954,28 @@ pub(crate) unsafe fn jdouble_to_f64_9e4a8f70<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn jint_to_Box_Priority_a16653ae<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::sys::jint, +) -> ::core::result::Result, __JniErr> { + Ok({ + let __inner = jint_to_Priority_447102d2(env, v)?; + ::std::boxed::Box::new(__inner) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn jint_to_Priority_447102d2<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::sys::jint, @@ -9995,6 +10105,35 @@ pub(crate) unsafe fn jlong_to_Archive_cd73502c<'env, 'v>( clippy::nonminimal_bool, clippy::eq_op )] +pub(crate) unsafe fn jlong_to_Box_Duration_0776c1ca<'env, 'v>( + env: &mut jni::JNIEnv<'env>, + v: &jni::sys::jlong, +) -> ::core::result::Result, __JniErr> { + Ok({ + let __inner = { + let __inner_s0 = jlong_to_u64_4384a5d6(env, v)?; + let __inner_s1 = u64_to_Duration_7c0845f9(env, __inner_s0) + .map_err(|__e| <__JniErr as ::core::convert::From< + String, + >>::from(__e.to_string()))?; + __inner_s1 + }; + ::std::boxed::Box::new(__inner) + }) +} +#[allow( + non_snake_case, + unused_mut, + unused_variables, + unused_braces, + unused_parens, + dead_code, + clippy::useless_conversion, + clippy::needless_question_mark, + clippy::let_and_return, + clippy::nonminimal_bool, + clippy::eq_op +)] pub(crate) unsafe fn jlong_to_EscapeProbe_416aab42<'env, 'v>( env: &mut jni::JNIEnv<'env>, v: &jni::sys::jlong, @@ -13183,6 +13322,48 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_blobValueNew<'a> } #[no_mangle] #[allow(non_snake_case, unused_mut, unused_variables, dead_code)] +pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedDurationEcho<'a>( + mut env: jni::JNIEnv<'a>, + _class: jni::objects::JClass<'a>, + value: jni::sys::jlong, + __error_sink: jni::objects::JObject<'a>, +) -> jni::sys::jlong { + #[allow(non_upper_case_globals)] + static __SINK_MID: ::prebindgen::lang::CachedIfaceMethod = ::prebindgen::lang::CachedIfaceMethod::new(); + const __SINK_FQN: &str = "io/prebindgen/covertest/JniErrorHandler"; + const __SINK_DESCR: &str = "(Ljava/lang/String;)Ljava/lang/Object;"; + let value = match jlong_to_Box_Duration_0776c1ca(&mut env, &value) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __out = perftest_flat::boxed_duration_echo(value); + match Box_Duration_to_jlong_0776c1ca(&mut env, __out) { + ::core::result::Result::Ok(__w) => __w, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + 0 as jni::sys::jlong + } + } +} +#[no_mangle] +#[allow(non_snake_case, unused_mut, unused_variables, dead_code)] pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_boxedElemIdSum<'a>( mut env: jni::JNIEnv<'a>, _class: jni::objects::JClass<'a>, @@ -22818,6 +22999,8 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_wrappedFieldsSum w_boxed_value: jni::sys::jlong, w_plain_present: jni::sys::jboolean, w_plain_value: jni::sys::jlong, + w_boxed_enum: jni::sys::jint, + w_plain_enum: jni::sys::jint, __error_sink: jni::objects::JObject<'a>, ) -> jni::sys::jlong { #[allow(non_upper_case_globals)] @@ -22884,10 +23067,43 @@ pub unsafe extern "C" fn Java_io_prebindgen_covertest_CovNative_wrappedFieldsSum } else { ::core::option::Option::None }; + let __flat_w_boxed_enum = match jint_to_Box_Priority_a16653ae( + &mut env, + &w_boxed_enum, + ) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; + let __flat_w_plain_enum = match jint_to_Priority_447102d2(&mut env, &w_plain_enum) { + ::core::result::Result::Ok(__v) => __v, + ::core::result::Result::Err(__e) => { + signal_binding_error( + &mut env, + &__error_sink, + &__SINK_MID, + __SINK_FQN, + __SINK_DESCR, + &__e.to_string(), + ); + return 0 as jni::sys::jlong; + } + }; let __flat_w = perftest_flat::WrappedFields { id: __flat_w_id, boxed: __flat_w_boxed, plain: __flat_w_plain, + boxed_enum: __flat_w_boxed_enum, + plain_enum: __flat_w_plain_enum, }; let w = __flat_w; let __out = perftest_flat::wrapped_fields_sum(w); diff --git a/examples/perftest-flat/src/ext.rs b/examples/perftest-flat/src/ext.rs index 788a48d1..3fedf6b0 100644 --- a/examples/perftest-flat/src/ext.rs +++ b/examples/perftest-flat/src/ext.rs @@ -732,6 +732,18 @@ pub fn duration_optional(value: Option) -> Option { value } +/// A transparent wrapper over a **`convert!`-declared** type, both directions. +/// +/// `Duration` reaches its Rust value through a staged chain +/// (`jlong -> u64 -> Duration`), and the transparent bridge used to call the +/// inner converter's function directly and leave `pre_stages` empty — so the +/// stages were skipped and the rebuild put `Box::new` around a `u64`. Not a +/// silent wrong value: `E0308` in the generated crate (#309). +#[prebindgen] +pub fn boxed_duration_echo(value: Box) -> Box { + value +} + /// Deliberately violate the binding's declared output domain so the Kotlin /// covertest can verify outbound validation and error routing. #[prebindgen] @@ -1681,12 +1693,22 @@ pub struct WrappedFields { pub id: i64, pub boxed: Box>, pub plain: Option, + /// The same pairing over a **terminal**, which is the shape that had no + /// outbound route at all: `Box>` above rides the `Optional` layer + /// arm, while `Box` classifies as `Named` and no arm claims it + /// (#309). Both must present as `Priority` in Kotlin — the wrapper is + /// invisible there, which is why the model erases it. + pub boxed_enum: Box, + pub plain_enum: Priority, } /// Round-trip a [`WrappedFields`] so both field spellings cross in one call. #[prebindgen] pub fn wrapped_fields_sum(w: WrappedFields) -> i64 { - w.id + w.boxed.unwrap_or(0) + w.plain.unwrap_or(0) + w.id + w.boxed.unwrap_or(0) + + w.plain.unwrap_or(0) + + i64::from(priority_weight(*w.boxed_enum)) + + i64::from(priority_weight(w.plain_enum)) } /// Transparent wrappers on the **input** side, one per specialized lowering. diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index cb95e997..e076edf5 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -438,6 +438,29 @@ impl Registry { out.push((child_dir, child.clone())); } } + // A spelling the model **erased wrappers from** depends on the stripped + // spelling: whoever converts `Box` does it by delegating to `T`'s own + // converter and putting the wrapper back. That is a real edge and the + // `kind` walk above cannot see it — `Box` classifies as whatever `T` + // is, so the two share a classification and differ only in spelling. + // + // Without it the dependency existed but the ORDER did not: a converter + // that delegates is built in one pass, so it needs its inner already + // built, and `subs` says "this is required" rather than "this comes + // first". `Box` resolved only because some other root's fields + // happened to pull `Payload` in earlier — alphabetical luck, which + // `Box` did not have. + if let Some(cell) = self.type_table(dir).get(key) { + let reading = &cell.subject; + if !reading.erased_wrappers().is_empty() { + let stripped = reading.stripped_key(); + if stripped != *key { + if let Some(inner) = self.type_table(dir).get(&stripped) { + out.push((dir, (*inner.subject).clone())); + } + } + } + } // A declared type's own fields, read off the element rather than off its // `syn::Fields`: a positional field is an ordinary `Field` there, so the // named-only asymmetry the syntax walk had does not arise. An `Enum` has diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index b6f18c63..710f5793 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -245,7 +245,11 @@ impl Declarations { return Some(c); } } - None + // 4. Last resort: the spelling differs from something convertible only + // by the wrappers the model erased. Dual of the input side's step 4, + // and reached the same way — after every layer arm, so nothing that + // resolves today changes route (#309). + self.output_transparent_bridge(ty, registry) } } diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs index eec09938..ddd7760f 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/value_form.rs @@ -2586,6 +2586,127 @@ fn a_transparent_wrapper_is_bridged_only_where_it_can_be() { ); } +/// An erased wrapper over a **terminal** crosses in BOTH directions, and one +/// selector arm serves every terminal kind (#309). +/// +/// The layer arms bridge a wrapper as part of handling their own layer, so +/// `Box>` and `Box>` resolved all along — which is what made +/// the gap hard to see. A wrapper over a plain `TypeKind::Named` has no arm, and +/// the terminal lookup keys on the SPELLING: no config sits under +/// `Box < Priority >`. Inbound that was a last resort added by #294; outbound it +/// was nothing at all, so the same field was a parameter this binding could take +/// and a return it could not give. +/// +/// The three terminal kinds are asserted together because they miss the terminal +/// lookup the same way, and one arm therefore covers them all — a claim worth +/// showing rather than arguing. +#[test] +fn an_erased_wrapper_over_a_terminal_crosses_both_ways() { + let loc = myflat_loc(); + // The enum is carried wrapped AND bare, so the Kotlin assertion below can + // say "these present alike" rather than merely "the wrapped one compiles". + // The handle and the data class are wrapped only: what they are here to + // show is that ONE arm serves every terminal kind, and a bare twin of each + // would test the terminal lookup rather than the bridge. + let items: Vec<(syn::Item, SourceLocation)> = vec![ + ( + syn::Item::Enum(syn::parse_quote!( + pub enum Priority { + Low = 0, + High = 1, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct Leaf { + pub v: i64, + } + )), + loc.clone(), + ), + ( + syn::Item::Struct(syn::parse_quote!( + pub struct Wrapped { + pub boxed_enum: Box, + pub plain_enum: Priority, + pub boxed_handle: Box, + pub boxed_data: Box, + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_sample_to_wrapped(s: &ZSample) -> Wrapped { + unimplemented!() + } + )), + loc.clone(), + ), + ( + syn::Item::Fn(syn::parse_quote!( + pub fn z_wrapped_take(w: Wrapped) { + unimplemented!() + } + )), + loc, + ), + ]; + let registry = + crate::api::test_util::reg_from_items(declare_referenced(items)).expect("index items"); + let jni = JniGenBuilder::new() + .set_package_prefix("io.test.jni") + .package( + crate::package!() + .class(crate::ptr_class!(ZSample)) + .class(crate::enum_class!(Priority)) + .class(crate::data_class!(Leaf)) + .class(crate::data_class!(Wrapped)) + .fun(crate::fun!(z_wrapped_take)), + ) + .expand(crate::expand_return!(ZSample).fields(crate::fields!(z_sample_to_wrapped))); + let dir = unique_test_dir("jnigen_terminal_bridge"); + let _ = std::fs::remove_dir_all(&dir); + std::fs::create_dir_all(&dir).unwrap(); + // Resolving at all is the claim: every one of these reached `None` outbound + // before, and the build failed naming the wrapped spelling. + let gen = jni + .build_with(registry) + .expect("an erased wrapper over a terminal resolves in both directions"); + let rust = + std::fs::read_to_string(gen.write_rust(dir.join("g.rs")).expect("write_rust")).unwrap(); + let rc: String = rust.split_whitespace().collect(); + + // Outbound: the wrapper comes off, then the inner converter runs. Inbound + // is the mirror — the inner converter runs, then the wrapper goes back on. + for kind in ["Priority", "ZSample", "Leaf"] { + assert!( + rc.contains(&format!("Box_{kind}_to_")), + "`Box<{kind}>` needs an OUTBOUND converter:\n{rust}" + ); + assert!( + rc.contains(&format!("_to_Box_{kind}_")), + "`Box<{kind}>` needs an inbound converter:\n{rust}" + ); + } + // The wrapper is invisible to Kotlin, so the bare and wrapped enum fields + // present as the same type — the point of the model erasing it. + let kotlin = gen + .write_kotlin(&dir.join("kotlin")) + .expect("write_kotlin") + .iter() + .map(|p| std::fs::read_to_string(p).unwrap()) + .collect::>() + .join("\n"); + let kc: String = kotlin.split_whitespace().collect(); + assert!( + kc.contains("valboxedEnum:Priority") && kc.contains("valplainEnum:Priority"), + "a wrapped enum presents as the enum class, exactly as the bare one does:\n{kotlin}" + ); +} + /// A wrapper cannot be bridged where the converter does not produce the spelled /// type at all — the **borrow** shapes — so those refuse rather than resolve. /// diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index d9b2484f..a6c0dbd8 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -2113,9 +2113,80 @@ impl Declarations { None } - /// **Input** wrapper shape (`pat` = the reconstructed canonical pattern, - /// `t1` = its captured inner): the built-in `&`/`Option<&>`/`Vec`/`Option` - /// handlers. + /// The **outbound** half of [`Self::input_transparent_bridge`], and the same + /// last resort: a spelling whose only difference from something this adapter + /// can already convert is the transparent wrappers over it. + /// + /// It had no twin, so an erased wrapper resolved inbound and not outbound — + /// `Box` was a parameter this binding could take and a return it + /// could not give, for a wrapper the model exists to make invisible (#309). + /// The one arm covers `Box`, `Box` and `Box` alike, + /// because [`Self::output_terminal`] misses all three the same way: it keys + /// on the SPELLING, and no config sits under `Box < Priority >`. + /// + /// The wrappers come **off** here rather than going on, which is the whole + /// difference: + /// + /// ```text + /// input : let __inner = (env, v)?; build_through_erased_wrappers(__inner) + /// output: let __inner = read_through(v); (env, __inner) + /// ``` + /// + /// Everything else is direction-independent — `subs`, `destination`, + /// `niches`, `metadata` all mean the same thing either way, and inheriting + /// the inner's metadata is what keeps `Box` presenting as the + /// Kotlin enum class instead of losing it behind the wrapper. + pub(crate) fn output_transparent_bridge( + &self, + reading: &crate::api::core::flat::TypeRef, + registry: &impl Conversions, + ) -> Option> { + if reading.erased_wrappers().is_empty() { + return None; + } + let produced = reading.syntax(); + let stripped = reading.stripped_syntax(); + // A wrapper over a **borrow** is refused here too, and outbound the + // reason is its own: a borrow's output route is the clone-into-a-fresh- + // handle arm, which hands back a wire built from a reference — there is + // no owned value to read the wrapper off. Inbound the same guard is + // about `E0106`; the shapes coincide, the reasons do not. + // + // Asked of the MODEL: an erasure is transparent, so `Box<&T>` already + // classifies as `Ref` and nothing here matches a `syn` variant. + if matches!(reading.kind(), crate::api::core::flat::TypeKind::Ref { .. }) { + return None; + } + // It has to be a type this binding already crosses; if it is not, the + // ordinary "unresolved" diagnostic names it, which is the better error. + let inner = registry.reading_of(&stripped)?; + let entry = registry.output_entry(&inner)?; + let wire = entry.destination.clone(); + // Take the wrappers off what the caller handed us. `None` is `Cow`'s + // policy refusal — the crossing then stays unresolved and names the + // type, rather than resolving and emitting Rust the consumer cannot + // build. + let read = read_through_erased_wrappers(reading, quote!(v))?; + // The inner's COMPLETE chain, stages included: a `convert!` type reaches + // its wire through them. + let inner_call = + crate::api::lang::jnigen::jni::emit::composed_inner_output(entry, quote!(__inner)); + let body: syn::Expr = syn::parse_quote!({ + let __inner = #read; + #inner_call + }); + Some(ConverterImpl { + subs: vec![stripped], + pre_stages: vec![], + function: self.build_output_fn(produced, &wire, &body, None), + destination: wire, + niches: entry.niches.clone(), + // The surface is the inner type's — a wrapper is invisible to the + // destination language, which is why the model erases it. + metadata: entry.metadata.clone(), + }) + } + /// **Last resort**: a spelling whose only difference from something this /// adapter can already convert is the transparent wrappers over it. /// @@ -2166,13 +2237,20 @@ impl Declarations { let inner = registry.reading_of(&stripped)?; let entry = registry.input_entry(&inner)?; let wire = entry.destination.clone(); - let inner_fn = &entry.function.sig.ident; // Wrap what the inner converter produced. `None` here is `Cow`'s policy // refusal — the crossing then stays unresolved and names the type, // rather than resolving and emitting Rust the consumer cannot build. let built = build_through_erased_wrappers(reading, quote!(__inner))?; + // The inner's COMPLETE chain, stages included. This called + // `entry.function` directly and left `pre_stages` empty, which SKIPPED + // them: a `convert!`-declared type reaches its Rust value through those + // stages (`jlong -> u64 -> Duration`), so a `Box` over one arrived + // un-staged. Every other composing arm goes through this helper for + // exactly that reason (#309). + let inner_call = + crate::api::lang::jnigen::jni::emit::composed_inner_input(entry, quote!(v)); let body: syn::Expr = syn::parse_quote!({ - let __inner = #inner_fn(env, v)?; + let __inner = #inner_call; #built }); Some(ConverterImpl { @@ -2189,6 +2267,13 @@ impl Declarations { }) } + /// **Input** wrapper shape (`pat` = the reconstructed canonical pattern, + /// `t1` = its captured inner): the built-in `&`/`Option<&>`/`Vec`/`Option` + /// handlers. The dual of [`Self::output_wrapper_shape`], whose own doc has + /// said so all along — this had been stranded above a different function + /// since the transparent bridge was inserted between them (#294), and + /// adding the outbound bridge moved it onto an OUTPUT converter, where it + /// read as an outright contradiction. pub(crate) fn input_wrapper_shape( &self, shape: WrapperShape, From f72d9a8d6d40c4df5090b1707f26cdd74b4c9fb1 Mon Sep 17 00:00:00 2001 From: Michael Ilyin Date: Tue, 4 Aug 2026 11:05:42 +0200 Subject: [PATCH 52/52] =?UTF-8?q?core:=20a=20type=20is=20its=20syntax=20?= =?UTF-8?q?=E2=80=94=20`TypeKind`=20stops=20classifying=20(#312)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * core: a type is its syntax — `TypeKind` stops classifying `TypeKind` was a **destination-neutral classification**: one variant per concept a target language would act on, several Rust spellings folding into each. `String` and `str` were one `Str`; `Vec` and `[T]` one `Sequence`; `Box` and `Cow<'_, T>` disappeared into what they wrapped. It leaked, and not at the edges: * `&T` earned a layer of its own while `Box` was declared transparent — two wrappers, opposite treatments, on no principle either adapter shared; * `Cbindgen` picked its C type off the Rust spelling regardless, so the neutrality the kind claimed was not what any adapter used; * every fold had to be **undone** somewhere. `erased_wrappers()` and `stripped_syntax()` exist because lowering dropped something a consumer needed back. So `TypeKind` is now the subset of `syn::Type` the flat API accepts, and nothing else. One variant per accepted form: Scalar Str String Unit Optional Vec Slice Fallible Boxed Cow Uninit Array Ref { lifetime, mutable } Named { id, args } Callback `RefMode` is gone: `&mut MaybeUninit` is `Ref { mutable: true }` over `Uninit`, the two forms the source wrote. A lifetime and a generic argument are kept, because they are what the source wrote. ## The folds did not disappear — they moved to where they are decided Each is one shared reading, taken on purpose at a call site rather than baked into every classification: TypeRef::unwrapped() Box/Cow peeled <- the erasure in lower_path TypeRef::sequence_elem() Vec, [T], either <- TypeKind::Sequence behind a wrapper TypeRef::borrow_target() past an out-param <- RefMode::Out's absorption slot TypeRef::is_exclusive_borrow() &mut T, not <- RefMode::Exclusive &mut MaybeUninit Old `x.kind()` is exactly new `x.unwrapped().kind()`, which is what made the ~30 consumer sites in `registry/scan`, `unfold`, `cbindgen` and `jnigen` a mechanical rewrite rather than a re-reading of each one. ## What it buys > **The syntax is recoverable from the kind.** `TypeKind::to_syn()`, checked against `TypeRef::origin.syntax` over the acceptance corpus by `syntax_is_recoverable_from_kind` — 27 spellings, token-exact, including `[u8; TAG_LEN]`, `&'a T`, `Cow<'_, [u8]>` and `Sample<'a, u8, Vec>`. Two exemptions, each named in a test of its own: a callback's bound *order*, and a `Group`/`Paren` the lowering sees through. The slice still rides along and generated Rust still spells it — it is exact and free. It is no longer **load-bearing**, and that is the whole difference: a fact missing from `kind` used to be invisible, because the syntax was there to cover for it. ## Also One new refusal falls out: mid-path generic arguments (`a::B::C`) are `UnsupportedForm`, since `Named` holds the last segment's arguments and a spelling this model cannot give back must not be accepted. No flat API writes that shape. `peel_transparent` stays — the syntax-side peer of `unwrapped`, for an adapter comparing a spelling it composed against one it has a converter for. It lives in `flat` so taking a `syn::Type` apart stays inside the model. **Did not move**: every generated artifact byte-identical (`examples/regen-check.sh`), boundary ledger unchanged, 641 lib tests and 22 doctests green, clippy + fmt clean on 1.85.0 and stable. * core: a `Cow` is a lifetime and a type, in that order Review (#312): the `Cow` arm checked the number of **type** arguments and then plucked the first lifetime out of the list, so three spellings that are not `Cow`s were accepted and reconstructed as `Cow<'a, u8>`: Cow no lifetime — not Rust at all Cow the arguments, in the wrong order Cow<'a, 'b, u8> two lifetimes, where `Cow` takes one Each has exactly one type argument, so `arity(1)` passed on all three. That made "every accepted form spells back what was written" false outside the corpus — and easy to miss, because `GenericArg` had faithfully retained the whole list right up until the builtin fold dropped it. `Cow` is the one builtin with a lifetime in its own signature, so it is the one whose **whole argument list** has to be validated: it is now matched as exactly `[Lifetime, Type]`, refused as `WrongGenericArguments { expected: "Cow<'a, T>" }` otherwise. Which lets `TypeKind::Cow::lifetime` be a `syn::Lifetime` rather than an `Option`, and `to_syn` emit the one accepted shape rather than choose between two. Recoverability is an acceptance rule here, not only a property: a spelling the model cannot give back is refused where it is read. The other instance is a generic argument on any but the last path segment. Also from review: `immediate_edges`' `Ref` arm took its child through `borrow_target().into_iter().collect()`, which would silently truncate the graph walk if the accessor and the kind ever disagreed. It is an `expect` now — the invariant fails loudly or not at all. Tests: the four refused shapes and the two accepted ones, the latter asserting the round-trip they exist to protect. --- docs/language-integration.md | 119 ++- prebindgen/src/api/core/flat/mod.rs | 57 +- .../src/api/core/flat/tests/acceptance.rs | 320 +++++---- prebindgen/src/api/core/flat/tests/mod.rs | 26 + .../src/api/core/flat/tests/roundtrip.rs | 111 ++- prebindgen/src/api/core/flat/ty.rs | 679 +++++++++++++----- prebindgen/src/api/core/registry/scan.rs | 54 +- prebindgen/src/api/core/registry/tests.rs | 2 +- prebindgen/src/api/core/unfold.rs | 20 +- prebindgen/src/api/lang/cbindgen/emit.rs | 15 +- prebindgen/src/api/lang/jnigen/jni/builder.rs | 6 +- .../api/lang/jnigen/jni/emit/flat_input.rs | 22 +- .../src/api/lang/jnigen/jni/emit/sum_out.rs | 3 +- .../src/api/lang/jnigen/jni/emit/vec_build.rs | 15 +- .../src/api/lang/jnigen/jni/selector.rs | 36 +- .../src/api/lang/jnigen/jni/struct_plan.rs | 4 +- .../src/api/lang/jnigen/jni/tests/values.rs | 18 +- .../src/api/lang/jnigen/jni/trait_impl.rs | 23 +- 18 files changed, 1054 insertions(+), 476 deletions(-) diff --git a/docs/language-integration.md b/docs/language-integration.md index 2f585c94..44e3a967 100644 --- a/docs/language-integration.md +++ b/docs/language-integration.md @@ -20,9 +20,10 @@ Source(s) ──items──> Flat ──Elements──> Registry ──> adapter An `Element` is two things at once, and the pairing is the whole point: -* a **closed classification** — `TypeKind`, the field list, which of the two enum - shapes an item is — that says what the source *means*, in terms every - destination language shares; +* a **closed model** — `TypeKind`, the field list, which of the two enum shapes + an item is. For a **type** that model is the accepted subset of `syn::Type` + and nothing more (see *A type is its syntax* below); above the type level it + is the concept — a field list, a sum's alternatives; * one `Origin`, carrying the **exact syntax** the node was built from and the source it arrived in. Every node has one, at every level — item, parameter, field, alternative, type, array extent. @@ -41,6 +42,65 @@ pub struct Variant { pub name: syn::Ident, pub alternatives: Vec, . pub struct Enum { pub name: syn::Ident, pub values: Vec, .. } // C-style ``` +### A type is its syntax + +`TypeKind` began as a **destination-neutral classification**: one variant per +concept a target language would act on, several Rust spellings folding into +each. `String` and `str` were one `Str`; `Vec` and `[T]` one `Sequence`; +`Box` and `Cow<'_, T>` disappeared into whatever they wrapped. + +It leaked, and the leak was not at the edges: + +* `&T` earned a layer of its own while `Box` was declared transparent — two + wrappers, opposite treatments, on no principle either adapter shared; +* `Cbindgen` picked its C type off the Rust spelling regardless, so the + neutrality the kind claimed was not what any adapter used; +* every fold had to be *undone* somewhere. `erased_wrappers()` and + `stripped_syntax()` exist because the model dropped something a consumer + needed back. + +So `TypeKind` is now the **subset of `syn::Type` the flat API accepts**, and +nothing else. One variant per accepted form: `Str` and `String`, `Vec` and +`Slice`, `Boxed`, `Cow`, `Uninit`, `Ref { lifetime, mutable }`, `Named { id, +args }`. A lifetime and a generic argument are kept, because they are what the +source wrote. + +**The folds did not disappear — they moved to where they are decided**, and are +one shared reading each rather than a property of the classification: + +| The reading | Answers | Replaces | +|---|---|---| +| `TypeRef::unwrapped()` | `Box`/`Cow` peeled to the value | the erasure in `lower_path` | +| `TypeRef::sequence_elem()` | the element of `Vec`, `[T]`, or either behind a wrapper | `TypeKind::Sequence` | +| `TypeRef::borrow_target()` | what a borrow points at, past an out-parameter's slot | `RefMode::Out`'s absorption | +| `TypeRef::is_exclusive_borrow()` | `&mut T`, and not `&mut MaybeUninit` | `RefMode::Exclusive` | + +What this buys is one property, and it is the point: + +> **The syntax is recoverable from the kind.** `TypeKind::to_syn()`, checked +> against `TypeRef::origin.syntax` over the whole acceptance corpus +> (`syntax_is_recoverable_from_kind`), with two named exemptions: a callback's +> bound *order*, and a `Group`/`Paren` the lowering sees through. + +The slice still rides along and generated Rust still spells it — it is exact and +free. It is no longer *load-bearing*, and that is the difference: a fact missing +from `kind` used to be invisible, because the syntax was there to cover for it. + +**Recoverability is an acceptance rule, not just a property.** A spelling the +model could not give back is refused where it is read, rather than accepted and +reconstructed as something else: + +* a generic argument on any but the last path segment (`a::B::C`) — + `Named` holds the last segment's arguments; +* a `Cow` whose argument list is not `['a, T]` — `Cow` (not Rust at all), + `Cow`, `Cow<'a, 'b, u8>`. Checking the *type-argument count* alone + accepted all three, and each then spelled back as `Cow<'a, u8>`. `Cow` is the + one builtin with a lifetime in its own signature, so it is the one whose whole + list has to be checked — which is also what lets its `lifetime` be a + `syn::Lifetime` and not an `Option`. + +**Did not move**: every generated artifact byte-identical. + ### Why the syntax rides along The predecessor design ([#215](https://github.com/milyin/prebindgen/issues/215)) @@ -65,7 +125,7 @@ classification stays small and genuinely neutral: | `Foo<'a, T>` | `TypeRef::origin.syntax` | generated Rust only | | "it is a `Foo`" | `TypeKind::Named` | every adapter | | `[u8; TAG_LEN]` — spelling / number / const identity | `TypeRef::origin.syntax` / `ArrayExtent::value` / `ExtentSource::Const` | C header / Kotlin / both | -| the `Box` in `Box>`, and the `Option` under it | `TypeRef::erased_wrappers()` / `stripped_syntax()` — derived from the syntax, not stored | an emitter that **rebuilds or destructures** a Rust value | +| the `Box` in `Box>`, and the `Option` under it | `TypeKind::Boxed` — kept, and read through by `TypeRef::unwrapped()` / `erased_wrappers()` | everyone: a classifier unwraps, an emitter that **rebuilds or destructures** puts it back | | where an item came from | `Origin::location` — **absent for a synthesized one** | diagnostics | ### The rule @@ -91,13 +151,13 @@ The weaker-sounding half is the important one. It is tempting to write "same | Rust | `kind` | Kotlin type | wire | |---|---|---|---| -| `&[Payload]` | `Sequence` | `List` | `Long` — a jlong handle to a Rust-side `Vec` | -| `Vec>` | `Sequence` | `List` | `List` — a `JObject` | +| `&[Payload]` | `Ref(Slice)` | `List` | `Long` — a jlong handle to a Rust-side `Vec` | +| `Vec>` | `Vec(Boxed)` | `List` | `List` — a `JObject` | Two wires, one surface. Choosing a wire is exactly the generator's job, and the destination-language wrapper absorbs the difference; a caller cannot tell. What -a caller *can* tell — and what the model's erasure promises will not happen — is -the **type** changing because the source spelled a `Box`. +a caller *can* tell — and what the shared `unwrapped()` reading is there to +prevent — is the **type** changing because the source spelled a `Box`. The rule scopes to **converted** positions, which is where a converter stands between the Rust value and the destination and is free to bridge. It cannot apply @@ -208,18 +268,19 @@ a model, and takes two bullets off L1 in the process. `Element::Unsupported` with `ItemError::UnresolvedType` — so a dangling name is reported here, by name, instead of surfacing downstream as an unresolved *converter* from whichever adapter looked first -- [x] `&mut MaybeUninit` becomes `RefMode::Out` — an out-parameter is a - property of the **borrow**, not a wrapper type, and it is a boundary concept - every destination language has (C's `T *out`) +- [x] `&mut MaybeUninit` is modelled — first as `RefMode::Out`, then (see + *A type is its syntax*) as the two forms the source wrote, with + `borrow_target()` as the reading that sees past the slot - [x] The example flat APIs are closed, and covertest-kotlin's build script asserts they stay closed across both its sources - [x] **Did not move**: every generated artifact byte-identical -`Cow<'_, T>` needed neither an alias nor a grammar addition in the end: it is -transparent, exactly like `Box`, so it lowers to whatever `T` is -([#236](https://github.com/milyin/prebindgen/pull/236)). Both adapters already -treated it as `Vec`, which is what made the transparency the honest reading -rather than a convenience. +`Cow<'_, T>` needed neither an alias nor a grammar addition in the end: both +adapters already treat it as the `Vec` it borrows +([#236](https://github.com/milyin/prebindgen/pull/236)). It first landed *as* +that reading — lowered to whatever `T` is — and is now a `TypeKind::Cow` that +`unwrapped()` reads through, which is the same behaviour with the fold moved to +where it is decided. **Still open**: `zenoh-flat` and its two consumers are separate repos. Their unmarked types — the 26 zenoh aliases, plus `Duration`, which is not in the @@ -494,22 +555,26 @@ The long pole — 97 sites, down from 106 because #248 took `jni/builder` from 1 #### What L4 taught: an erasure sits outside the layer it wraps -The model erases `Box` and `Cow`, and that erasure is right — `Box>` -is one optional to every destination. But **conversion follows the syntax**, and -the two facts a rebuild needs were not on the model: what was taken off, and what -is left under it. #292 added them as derived readings, `TypeRef::erased_wrappers()` +Reading through `Box` and `Cow` is right — `Box>` is one optional to +every destination. But **conversion follows the syntax**, and the two facts a +rebuild needs were not on the model at the time: what was taken off, and what is +left under it. #292 added them as derived readings, `TypeRef::erased_wrappers()` and `stripped_syntax()`, defined by an invariant rather than by a loop — the -stripped spelling is *the one whose own lowering yields exactly this `kind`*, so -the peel runs to a fixed point (`Box>` classifies as `T`, and one strip -leaves a `Box` that does not match). +stripped spelling is *the one whose own lowering yields the kind `unwrapped()` +reaches*, so the peel runs to a fixed point (`Box>` unwraps to `T`, and +one strip leaves a `Box` that does not match). + +Both readings survived *A type is its syntax* unchanged, computed off the kind +rather than off the spelling. The lesson below is the reason the fold had to +become a reading in the first place. The rule, which outlives the stage: -> **`kind` is precisely the thing the wrapper is missing from, so interpreting -> `kind` before checking for a wrapper always discards one.** +> **The unwrapped reading is precisely the thing the wrapper is missing from, so +> taking it before checking for a wrapper always discards one.** -`Box<&Vec>` classifies as `Ref`; peel that first and the wrapper is gone from -everywhere a consumer will look. `&Box>` hides it on the referent, where a +`Box<&Vec>` *reads* as a `Ref`; take that reading first and the wrapper is +gone from everywhere a consumer will look. `&Box>` hides it on the referent, where a question asked of the outer `syn::Type::Reference` cannot see it. Neither check subsumes the other, so a walk must ask at **every layer, on the way down** — which is also why the wrapper is a *list*, gathered as the walk descends. diff --git a/prebindgen/src/api/core/flat/mod.rs b/prebindgen/src/api/core/flat/mod.rs index ce64246f..5b793b5f 100644 --- a/prebindgen/src/api/core/flat/mod.rs +++ b/prebindgen/src/api/core/flat/mod.rs @@ -19,9 +19,9 @@ //! //! Two things at once, and that pairing is the whole design: //! -//! * a **closed classification** — [`TypeKind`], the field list, which of the two -//! enum shapes an item is — that says what the source *means*, in terms every -//! destination language shares; +//! * 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. //! @@ -44,22 +44,34 @@ //! //! # What earns a variant //! -//! A concept, not a Rust spelling. The test is whether a *destination* language -//! would act on the distinction; if only Rust can see it, it is spelling, and -//! the slice already carries it: +//! 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`](TypeKind::String), [`Str`](TypeKind::Str) | the adapter's, at its own site | +//! | `Vec`, `[T]` | [`Vec`](TypeKind::Vec), [`Slice`](TypeKind::Slice) | [`TypeRef::sequence_elem`] — one run of `T` | +//! | `Box`, `Cow<'_, T>` | [`Boxed`](TypeKind::Boxed), [`Cow`](TypeKind::Cow) | [`TypeRef::unwrapped`] — a `T` either way | +//! | `&mut MaybeUninit` | `Ref` over [`Uninit`](TypeKind::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** +//! ([`TypeKind::to_syn`], checked over the whole acceptance corpus). 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 | //! |---|---|---| -//! | `String`, `str` | [`TypeKind::Str`] | one concept, two Rust types | -//! | `Vec`, `[T]` | [`TypeKind::Sequence`] | a run of `T`; owned vs borrowed is the [`Ref`](TypeKind::Ref) layer's fact | -//! | `Box` | whatever `T` is | an owned `T` either way | //! | `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 | -//! | `&mut MaybeUninit` | [`RefMode::Out`] | an out-param slot the caller supplies | -//! | no `->`, `-> ()` | [`TypeKind::Unit`] | the same function | -//! | `*const T` | *rejected* | a source crate is idiomatic Rust; the adapter owns pointers | //! //! 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 @@ -84,17 +96,24 @@ //! //! 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`, `Foo<'a>` is not `Foo`. A model that carries no syntax has -//! to become *lossless* to serve it — which is how a language-neutral IR turns -//! back into a second `syn`. Carrying the original slice costs nothing and lets -//! the classification stay small: a lifetime, a delimiter and a literal's base -//! are simply not modelled facts. +//! 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 [`TypeKind::to_syn`] 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. +//! acceptance list to drift from it. One rule cannot be stated that way and is +//! stated in the lowering instead: [`Uninit`](TypeKind::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. @@ -182,7 +201,7 @@ pub use self::{ origin::Origin, spelling::{canonical_spelling, canonical_type, type_from_ident}, ty::{ - peel_transparent, RefMode, ScalarKind, TypeId, TypeKind, TypeRef, UnsupportedType, + peel_transparent, GenericArg, ScalarKind, TypeId, TypeKind, TypeRef, UnsupportedType, UnsupportedTypeReason, TRANSPARENT_WRAPPERS, }, }; diff --git a/prebindgen/src/api/core/flat/tests/acceptance.rs b/prebindgen/src/api/core/flat/tests/acceptance.rs index 17001a00..ecc9ebbf 100644 --- a/prebindgen/src/api/core/flat/tests/acceptance.rs +++ b/prebindgen/src/api/core/flat/tests/acceptance.rs @@ -5,32 +5,6 @@ use super::*; -/// Lower one type by putting it in a struct field, and report what the language -/// made of it. The field path is used because a field is the position every -/// consumer already agrees is a boundary surface. -fn lower(ty: proc_macro2::TokenStream) -> Result { - let item: syn::Item = syn::parse_quote!( - pub struct S { - pub f: #ty, - } - ); - // The fixture types stand in for a declared type wherever the grammar needs - // a nominal one, so references resolve and the test is about the grammar. - let mut items = fixture_types(); - items.push(tag_len_const()); - items.push(opaque("Sample")); - let n = items.len(); - items.push(item); - match parse(items).remove(n) { - Element::Type(Type::Struct(s)) => Ok(s.fields[0].ty.clone()), - Element::Unsupported(u) => match *u.error { - ItemError::FieldType { source, .. } => Err(source), - other => panic!("expected a field-type diagnosis, got {other}"), - }, - other => panic!("expected a struct, got {}", describe(&other)), - } -} - fn kind(ty: proc_macro2::TokenStream) -> TypeKind { lower(ty).expect("in the language").kind } @@ -55,23 +29,27 @@ fn scalars_and_strings() { kind(quote::quote!(f64)), TypeKind::Scalar(ScalarKind::F64) )); - assert!(matches!(kind(quote::quote!(String)), TypeKind::Str)); + assert!(matches!(kind(quote::quote!(String)), TypeKind::String)); assert!(matches!(kind(quote::quote!(())), TypeKind::Unit)); } -/// `String` and `str` are one concept, and the borrow is the `Ref` layer's -/// fact. Every adapter already treats `&str` as a borrowed string by hand; -/// classifying `str` as a nominal type would send them all looking for an item -/// named `str` to resolve. +/// `str` and `String` are two Rust types, so they are two kinds. That they are +/// one *string* to every destination language is a destination's reading, and +/// the adapters make it — the model reports what the source wrote. +/// +/// Neither is a nominal type: both are in the grammar, so no adapter goes +/// looking for a declared item named `str` to resolve. #[test] -fn a_string_is_a_string_however_it_is_spelled() { +fn the_two_string_types_stay_two() { assert!(matches!(kind(quote::quote!(str)), TypeKind::Str)); - for spelling in [quote::quote!(&str), quote::quote!(&String)] { - let TypeKind::Ref { mode, inner } = kind(spelling) else { + assert!(matches!(kind(quote::quote!(String)), TypeKind::String)); + for (spelling, owned) in [(quote::quote!(&str), false), (quote::quote!(&String), true)] { + let TypeKind::Ref { mutable, inner, .. } = kind(spelling) else { panic!("a borrow"); }; - assert_eq!(mode, RefMode::Shared); - assert!(matches!(inner.kind, TypeKind::Str)); + assert!(!mutable); + assert_eq!(matches!(inner.kind, TypeKind::String), owned); + assert_eq!(matches!(inner.kind, TypeKind::Str), !owned); } } @@ -81,32 +59,34 @@ fn the_builtin_generics() { kind(quote::quote!(Option)), TypeKind::Optional(_) )); - assert!(matches!( - kind(quote::quote!(Vec)), - TypeKind::Sequence(_) - )); + assert!(matches!(kind(quote::quote!(Vec)), TypeKind::Vec(_))); assert!(matches!( kind(quote::quote!(Result)), TypeKind::Fallible { .. } )); } -/// `Box` **is** `T` — an owned value either way, and no destination language -/// can tell them apart, so it carries no kind of its own. The `Box` survives -/// where it matters: in the syntax generated Rust spells. +/// A `Box` **is a `Box`** in the model. That no destination language can +/// tell it from `T` is true and is the adapters' to act on: `unwrapped` is where +/// that reading is taken, and it is taken on purpose, at a call site. #[test] -fn a_box_classifies_as_what_it_wraps() { +fn a_box_is_a_box_until_a_consumer_unwraps_it() { let ty = lower(quote::quote!(Box)).expect("in the language"); - assert!(matches!(ty.kind, TypeKind::Str)); + let TypeKind::Boxed(inner) = &ty.kind else { + panic!("a box"); + }; + assert!(matches!(inner.kind, TypeKind::String)); assert_eq!(tokens(&ty.origin.syntax), "Box < String >"); + // The reading a destination takes. + assert!(matches!(ty.unwrapped().kind(), TypeKind::String)); // And it composes: the nullable heap string of a `#[repr(C)]` struct field - // is an optional string, spelled with its `Box`. + // is an optional string, spelled with its `Box`. `optional_inner` reads + // through the wrapper, so the layer accessors answer as they always did. let ty = lower(quote::quote!(Option>)).expect("in the language"); - let TypeKind::Optional(inner) = &ty.kind else { - panic!("an option"); - }; - assert!(matches!(inner.kind, TypeKind::Str)); + let inner = ty.optional_inner().expect("an option"); + assert!(matches!(inner.kind, TypeKind::Boxed(_))); + assert!(matches!(inner.unwrapped().kind(), TypeKind::String)); assert_eq!(tokens(&inner.origin.syntax), "Box < String >"); } @@ -114,11 +94,11 @@ fn a_box_classifies_as_what_it_wraps() { /// off, and what is left under it. /// /// The invariant is not "the loop peels until it stops" — it is that -/// [`TypeRef::stripped_syntax`] is *the spelling whose own lowering yields -/// exactly this type's `kind`*. That is what makes it a safe base for a +/// [`TypeRef::stripped_syntax`] is *the spelling whose own lowering yields the +/// kind [`TypeRef::unwrapped`] reaches*. That is what makes it a safe base for a /// reconstruction, and it is why the peel must run to a **fixed point**: -/// `Box>` classifies as `T`, so one strip leaves a `Box` that does -/// not match. +/// `Box>` unwraps to `T`, so one strip leaves a `Box` that does not +/// match. #[test] fn the_stripped_spelling_is_the_one_that_lowers_to_this_kind() { // The property, asserted as a property: strip, lower again, get the same @@ -137,7 +117,7 @@ fn the_stripped_spelling_is_the_one_that_lowers_to_this_kind() { let stripped = ty.stripped_syntax(); assert_eq!( format!("{:?}", kind(quote::quote!(#stripped))), - format!("{:?}", ty.kind), + format!("{:?}", ty.unwrapped().kind()), "`{}` strips to `{}`, which must classify identically", tokens(&ty.origin.syntax), tokens(&stripped), @@ -206,11 +186,11 @@ fn a_wrapper_is_found_only_at_the_layer_that_spells_it() { // alone is enough: the difference between them lives in a spelling, and the // classification is exactly the thing it is missing from. for ty in [&outside, &inside] { - let TypeKind::Ref { mode, inner } = &ty.kind else { + let TypeKind::Ref { mutable, inner, .. } = ty.unwrapped().kind() else { panic!("a borrow"); }; - assert_eq!(*mode, RefMode::Shared); - let TypeKind::Sequence(elem) = &inner.kind else { + assert!(!mutable); + let TypeKind::Vec(elem) = inner.unwrapped().kind() else { panic!("a run"); }; assert!(matches!(elem.kind, TypeKind::Named { .. })); @@ -268,15 +248,18 @@ fn the_prelude_reaches_every_builtin_by_either_spelling() { )); assert!(matches!( kind(quote::quote!(alloc::string::String)), - TypeKind::Str + TypeKind::String )); // The bug: qualified `MaybeUninit` used to fall through to an unresolvable // nominal type, so an out-parameter worked only if the source `use`d it. - let TypeKind::Ref { mode, .. } = kind(quote::quote!(&mut std::mem::MaybeUninit)) else { + let TypeKind::Ref { mutable, inner, .. } = + kind(quote::quote!(&mut std::mem::MaybeUninit)) + else { panic!("a borrow"); }; - assert_eq!(mode, RefMode::Out); + assert!(mutable); + assert!(matches!(inner.kind, TypeKind::Uninit(_))); } /// A `#[prebindgen] pub type` is a **one-way road**: it brings a foreign type into @@ -307,10 +290,8 @@ fn an_alias_is_a_declaration_not_an_equivalence() { // The declared name works. let f = flat.function("by_name").expect("declared"); - let TypeKind::Ref { inner, .. } = &f.params[0].ty.kind else { - panic!("a borrow"); - }; - let TypeKind::Named { id } = &inner.kind else { + let inner = f.params[0].ty.borrow_target().expect("a borrow"); + let TypeKind::Named { id, .. } = &inner.kind else { panic!("a nominal type"); }; assert_eq!(id.name, "Session"); @@ -391,7 +372,7 @@ fn an_alias_never_retypes_a_spelling() { // unrelated alias happens to target. for f in ["strings", "bytes"] { assert!( - matches!(param(f), TypeKind::Sequence(_)), + matches!(param(f), TypeKind::Vec(_)), "`{f}`: the grammar's spelling stays canonical" ); } @@ -399,7 +380,7 @@ fn an_alias_never_retypes_a_spelling() { // Each alias is usable by its own name, and they cannot collide: a bare path is // never reduced, so the name IS the identity. for (f, expected) in [("by_name", "Bytes"), ("small", "Small"), ("big", "Big")] { - let TypeKind::Named { id } = param(f) else { + let TypeKind::Named { id, .. } = param(f) else { panic!("{f}: a nominal type"); }; assert_eq!(id.name, expected, "{f}"); @@ -439,66 +420,94 @@ fn a_qualified_builtin_is_a_named_type() { fn references() { assert!(matches!( kind(quote::quote!(&Sample)), - TypeKind::Ref { - mode: RefMode::Shared, - .. - } + TypeKind::Ref { mutable: false, .. } )); assert!(matches!( kind(quote::quote!(&mut Sample)), - TypeKind::Ref { - mode: RefMode::Exclusive, - .. - } + TypeKind::Ref { mutable: true, .. } )); + // The lifetime is part of the type, so the model keeps it. + let TypeKind::Ref { lifetime, .. } = kind(quote::quote!(&'a Sample)) else { + panic!("a borrow"); + }; + assert_eq!(lifetime.expect("a lifetime").ident, "a"); } -/// `Vec` and `[T]` are one concept — a run of `T` — and ownership is the -/// `Ref` layer's fact, not a second variant. That is already how the pipeline -/// behaves: one `Shape::Iterable` covers both, and jnigen rewrites a `&[T]` -/// input into the `Vec<_>` pattern outright. +/// `Vec` and `[T]` are two Rust forms, so two kinds — and one *run of values* +/// to a consumer, which is what [`TypeRef::sequence_elem`] answers. That reading +/// is what the pipeline is built on (one `Shape::Iterable` covers both, and +/// jnigen rewrites a `&[T]` input into the `Vec<_>` pattern outright); the model +/// no longer has to lose the difference to provide it. #[test] -fn a_sequence_is_a_sequence_borrowed_or_owned() { - assert!(matches!( - kind(quote::quote!(Vec)), - TypeKind::Sequence(_) - )); +fn a_run_of_values_is_read_through_either_spelling() { + assert!(matches!(kind(quote::quote!(Vec)), TypeKind::Vec(_))); // Bare, as a callback argument is written: `impl Fn([T])`. - assert!(matches!(kind(quote::quote!([u8])), TypeKind::Sequence(_))); + assert!(matches!(kind(quote::quote!([u8])), TypeKind::Slice(_))); let TypeKind::Ref { inner, .. } = kind(quote::quote!(&[u8])) else { panic!("a reference"); }; - assert!(matches!(inner.kind, TypeKind::Sequence(_))); + assert!(matches!(inner.kind, TypeKind::Slice(_))); + + // One reading over both, plus a wrapper over either. + for spelling in [ + quote::quote!(Vec), + quote::quote!([u8]), + quote::quote!(Box>), + quote::quote!(Cow<'_, [u8]>), + ] { + let ty = lower(spelling).expect("in the language"); + assert!( + matches!( + ty.sequence_elem().expect("a run").kind(), + TypeKind::Scalar(ScalarKind::U8) + ), + "`{}` is a run of `u8`", + tokens(&ty.origin.syntax) + ); + } } -/// `Cow<'_, T>` **is** `T`, the same treatment `Box` gets: borrowed or owned, and -/// no destination language can tell. +/// A `Cow<'_, T>` keeps its own kind, its lifetime included, and reads as the +/// `T` it borrows — the same treatment `Box` gets, taken at the consumer +/// rather than during lowering. /// -/// Both adapters already behave that way — cbindgen lowers `Cow<'_, [T]>` "just like +/// Both adapters act on that reading — cbindgen lowers `Cow<'_, [T]>` "just like /// `Vec` outputs", and jnigen's converter is `byte_array_from_slice(&v)`, which -/// works by deref and is identical to the `Vec` one — so this classification -/// predicts their behaviour rather than leaving it a special case. +/// works by deref and is identical to the `Vec` one — and now both can also +/// see the `Cow` they are seeing through. #[test] -fn a_cow_is_what_it_borrows() { - // The property the whole treatment rests on: indistinguishable from the owned - // spelling of the same thing. +fn a_cow_reads_as_what_it_borrows() { + // The reading the whole treatment rests on: indistinguishable from the owned + // spelling of the same thing, once a consumer says it does not care. + let cow = lower(quote::quote!(Cow<'_, [u8]>)).expect("in the language"); assert_eq!( - format!("{:?}", kind(quote::quote!(Cow<'_, [u8]>))), - format!("{:?}", kind(quote::quote!(Vec))), - "a byte Cow classifies exactly as a byte Vec" + format!("{:?}", cow.unwrapped().kind()), + format!("{:?}", kind(quote::quote!([u8]))), + "a byte Cow reads exactly as the byte slice it borrows" ); - assert!(matches!(kind(quote::quote!(Cow<'_, str>)), TypeKind::Str)); + let TypeKind::Cow { lifetime, .. } = &cow.kind else { + panic!("a cow"); + }; + assert_eq!(lifetime.ident, "_"); + assert!(matches!( + lower(quote::quote!(Cow<'_, str>)) + .expect("in the language") + .unwrapped() + .kind(), + TypeKind::Str + )); // The `Cow` survives where codegen reads it: a generated signature must spell // `Cow<'_, [u8]>`, which is not interchangeable with `Vec` in Rust. - let ty = lower(quote::quote!(Cow<'_, [u8]>)).expect("in the language"); - assert_eq!(tokens(&ty.origin.syntax), "Cow < '_ , [u8] >"); + assert_eq!(tokens(&cow.origin.syntax), "Cow < '_ , [u8] >"); // Transparent for any target, as `Box` is: whether it can actually cross is the // adapter's call, and both already restrict which elements they accept. - let TypeKind::Sequence(elem) = kind(quote::quote!(Cow<'_, [Sample]>)) else { - panic!("a sequence"); - }; + let elem = lower(quote::quote!(Cow<'_, [Sample]>)) + .expect("in the language") + .sequence_elem() + .expect("a run") + .clone(); assert!(matches!(elem.kind, TypeKind::Named { .. })); // A lifetime argument is expected on `Cow` alone. On any other builtin it is @@ -520,6 +529,48 @@ fn a_cow_is_what_it_borrows() { )); } +/// `Cow` is the one builtin whose signature carries a lifetime, so it is the one +/// whose **whole argument list** is checked rather than its type-argument count. +/// +/// Counting types alone accepts three spellings that are not `Cow`s: the review +/// case `Cow`, a second lifetime, and no lifetime at all. Each has +/// exactly one type argument, so each passed — and then reconstructed as +/// `Cow<'a, u8>`, quietly breaking the property +/// [`syntax_is_recoverable_from_kind`] asserts. A model that keeps only the +/// first lifetime cannot spell any of them back, which is the reason to refuse +/// them rather than the consequence of doing so. +#[test] +fn a_cow_takes_a_lifetime_and_a_type_in_that_order() { + // The accepted shape, either way the lifetime is written. + for spelling in [quote::quote!(Cow<'_, [u8]>), quote::quote!(Cow<'a, str>)] { + let ty = lower(spelling).expect("in the language"); + assert!(matches!(ty.kind, TypeKind::Cow { .. })); + // And it spells back, which is what the refusals below protect. + assert_eq!(tokens(&ty.kind().to_syn()), tokens(ty.syntax())); + } + + // Everything else is refused by shape, and named as such. + for spelling in [ + // No lifetime: `Cow` is not Rust, so no source crate compiles it. + quote::quote!(Cow), + // The review case: the arguments are there, in the wrong order. + quote::quote!(Cow), + // Two lifetimes, where `Cow` takes one. + quote::quote!(Cow<'a, 'b, u8>), + // Two types. + quote::quote!(Cow<'a, u8, u8>), + ] { + let rendered = spelling.to_string(); + assert_eq!( + reason(spelling), + UnsupportedTypeReason::WrongGenericArguments { + expected: "Cow<'a, T>" + }, + "`{rendered}` is not a `Cow`" + ); + } +} + /// The signature that motivated this: zenoh-flat's `zbytes_to_bytes`. It was refused /// under the closed API because a lifetime argument sent `Cow` to an undeclared /// nominal type. @@ -543,7 +594,8 @@ fn a_cow_returning_accessor_resolves() { assert_eq!(flat.unsupported().count(), 0, "no longer refused"); let f = flat.function("zbytes_to_bytes").expect("survives"); - assert!(matches!(f.ret.kind, TypeKind::Sequence(_))); + assert!(matches!(f.ret.kind, TypeKind::Cow { .. })); + assert!(f.ret.sequence_elem().is_some(), "and it reads as a run"); // And the return still spells its `Cow`, so an adapter can emit the signature. assert_eq!(tokens(&f.ret.origin.syntax), "Cow < '_ , [u8] >"); } @@ -573,7 +625,7 @@ fn a_raw_pointer_is_not_in_the_language() { #[test] fn generic_arguments_are_spelling_only() { let ty = lower(quote::quote!(Foo<'a, u8>)).expect("in the language"); - let TypeKind::Named { id } = &ty.kind else { + let TypeKind::Named { id, .. } = &ty.kind else { panic!("a named type"); }; assert_eq!(id.name, "Foo"); @@ -1472,37 +1524,47 @@ fn an_undeclared_reference_refuses_the_referencing_item() { } } -/// An out-parameter is a **mode of borrowing**, not a type. `&mut MaybeUninit` -/// says the caller supplies the slot and the callee fills it; the `MaybeUninit` is -/// absorbed into the mode, so `inner` is the value's own type. +/// An out-parameter is `&mut MaybeUninit` — the two forms the source wrote, +/// each with its own kind. What it *means* (the caller supplies the slot, the +/// callee fills it) is a reading, and the model provides the two that consumers +/// need: [`TypeRef::borrow_target`] sees past the slot to the `T` that actually +/// crosses, and [`TypeRef::is_exclusive_borrow`] is false for it, because a +/// callee may not read the slot first. /// -/// Uninitialized storage anywhere else promises nothing a destination language can -/// use, so the combinations that mean nothing cannot be written down. +/// Uninitialized storage anywhere else promises nothing a destination language +/// can use, so it is refused — an acceptance rule about a **position**, which is +/// the one thing a variant set cannot state. #[test] -fn an_out_parameter_is_a_borrow_mode() { - let TypeKind::Ref { mode, inner } = kind(quote::quote!(&mut MaybeUninit)) else { +fn an_out_parameter_is_a_mutable_borrow_of_a_slot() { + let out = lower(quote::quote!(&mut MaybeUninit)).expect("in the language"); + let TypeKind::Ref { mutable, inner, .. } = &out.kind else { panic!("a borrow"); }; - assert_eq!(mode, RefMode::Out); - // The `MaybeUninit` is gone from the type: it described the borrow. - let TypeKind::Named { id, .. } = &inner.kind else { + assert!(mutable); + let TypeKind::Uninit(slot) = &inner.kind else { + panic!("the slot the source wrote"); + }; + let TypeKind::Named { id, .. } = &slot.kind else { panic!("the value's own type"); }; assert_eq!(id.name, "Sample"); - // The three modes are one axis. - assert_eq!( - [ - quote::quote!(&Sample), - quote::quote!(&mut Sample), - quote::quote!(&mut MaybeUninit), - ] - .map(|t| match kind(t) { - TypeKind::Ref { mode, .. } => mode, - other => panic!("a borrow, got {other:?}"), - }), - [RefMode::Shared, RefMode::Exclusive, RefMode::Out] - ); + // The readings: the target is the value, and it is not an exclusive borrow. + let target = out.borrow_target().expect("a borrow"); + assert!(matches!(&target.kind, TypeKind::Named { id, .. } if id.name == "Sample")); + assert!(!out.is_exclusive_borrow()); + + // Against the other two borrows, which differ only where they should. + for (spelling, exclusive) in [ + (quote::quote!(&Sample), false), + (quote::quote!(&mut Sample), true), + ] { + let ty = lower(spelling).expect("in the language"); + assert_eq!(ty.is_exclusive_borrow(), exclusive); + assert!( + matches!(&ty.borrow_target().expect("a borrow").kind, TypeKind::Named { id, .. } if id.name == "Sample") + ); + } // Owned, or shared-borrowed, it means nothing. assert_eq!( @@ -1956,7 +2018,7 @@ fn a_raw_identifier_survives_typeid() { assert_eq!(raw.to_string(), "r#type", "the hash is part of the name"); let t = TypeRef::named(&raw); - let TypeKind::Named { id } = &t.kind else { + let TypeKind::Named { id, .. } = &t.kind else { panic!("named") }; // Recovered, and it spells itself back the way it was written. diff --git a/prebindgen/src/api/core/flat/tests/mod.rs b/prebindgen/src/api/core/flat/tests/mod.rs index ecf9e2a4..41330d25 100644 --- a/prebindgen/src/api/core/flat/tests/mod.rs +++ b/prebindgen/src/api/core/flat/tests/mod.rs @@ -16,6 +16,32 @@ use super::*; mod acceptance; mod roundtrip; +/// Lower one type by putting it in a struct field, and report what the language +/// made of it. The field path is used because a field is the position every +/// consumer already agrees is a boundary surface. +fn lower(ty: proc_macro2::TokenStream) -> Result { + let item: syn::Item = syn::parse_quote!( + pub struct S { + pub f: #ty, + } + ); + // The fixture types stand in for a declared type wherever the grammar needs + // a nominal one, so references resolve and the test is about the grammar. + let mut items = fixture_types(); + items.push(tag_len_const()); + items.push(opaque("Sample")); + let n = items.len(); + items.push(item); + match parse(items).remove(n) { + Element::Type(Type::Struct(s)) => Ok(s.fields[0].ty.clone()), + Element::Unsupported(u) => match *u.error { + ItemError::FieldType { source, .. } => Err(source), + other => panic!("expected a field-type diagnosis, got {other}"), + }, + other => panic!("expected a struct, got {}", describe(&other)), + } +} + /// Parse one item, stamped with an origin crate so array extents can name /// `#[prebindgen]` consts from "their own" crate. /// diff --git a/prebindgen/src/api/core/flat/tests/roundtrip.rs b/prebindgen/src/api/core/flat/tests/roundtrip.rs index b1ee44ab..e4a140ed 100644 --- a/prebindgen/src/api/core/flat/tests/roundtrip.rs +++ b/prebindgen/src/api/core/flat/tests/roundtrip.rs @@ -1,11 +1,16 @@ -//! The round-trip property: an element's syntax slices are the source's own -//! tokens, sliced — never a reconstruction. +//! The round-trip property, in both directions: //! -//! Every test here would also pass against a model that rebuilt syntax from its -//! classification *for the easy cases*. The ones that matter are the cases where -//! a reconstruction loses: an empty tuple variant, a hex discriminant, a -//! lifetime, an aliased path, a doc comment. Those are the reason the slices -//! ride along at all. +//! * an element's syntax slices are the source's own tokens, sliced — never a +//! reconstruction. A delimiter, a hex discriminant, a doc comment survive +//! because the slice was kept, and that is what generated Rust re-emits; +//! * and the type grammar can spell them back. [`TypeKind`] is the accepted +//! subset of `syn::Type` and nothing less, so a kind that could not reproduce +//! the tokens it was lowered from would have dropped something — +//! `syntax_is_recoverable_from_kind` is where that is checked. +//! +//! The second is what keeps the first honest. Slices ride along because they +//! are exact and free, not because the classification needs them to be +//! complete. use super::*; @@ -113,7 +118,7 @@ fn an_item_and_its_components_share_one_location() { assert!(Rc::ptr_eq(item, &field.ty.origin.location), "field type"); } // And down through a nested type's arguments. - let TypeKind::Sequence(elem) = &s.fields[1].ty.kind else { + let TypeKind::Vec(elem) = &s.fields[1].ty.kind else { panic!("a sequence"); }; assert!(Rc::ptr_eq(item, &elem.origin.location), "element type"); @@ -464,3 +469,93 @@ fn an_unsupported_item_keeps_its_tokens() { assert!(matches!(element, Element::Unsupported(_))); assert_eq!(tokens(&element.syntax()), tokens(&source)); } + +/// **Every accepted form spells back exactly what was written.** +/// +/// The property the pivot rests on: [`TypeKind`] is the accepted subset of +/// `syn::Type`, so the tokens are recoverable from the kind alone. It is checked +/// rather than relied on — generated Rust still emits the slice — because a kind +/// that has quietly stopped carrying a lifetime, a wrapper or a path prefix is +/// exactly the drift the old design shipped, and it was invisible while the +/// slice was there to cover for it. +/// +/// One row per accepted form, plus the compositions where a lost fact would hide +/// under an outer layer. +#[test] +fn syntax_is_recoverable_from_kind() { + for spelling in [ + // Scalars, the unit, the two string types. + quote::quote!(u8), + quote::quote!(bool), + quote::quote!(f64), + quote::quote!(()), + quote::quote!(String), + quote::quote!(&str), + // The builtin generics. + quote::quote!(Option), + quote::quote!(Vec), + quote::quote!(Result), + quote::quote!(Box), + quote::quote!(Cow<'_, [u8]>), + quote::quote!(Cow<'a, str>), + // Runs and borrows, with the lifetime and the mutability the source wrote. + quote::quote!(&[u8]), + quote::quote!(&'a Sample), + quote::quote!(&mut Sample), + quote::quote!(&mut MaybeUninit), + // Arrays, by literal and by named const — the extent keeps its own + // spelling, so `TAG_LEN` does not come back as `4`. + quote::quote!([u8; 4]), + quote::quote!([u8; TAG_LEN]), + quote::quote!([[u8; 4]; TAG_LEN]), + // Nominal types, carrying arguments a classification has no other place + // to put. Bare only: a path-qualified name cannot name a flat-API item, + // so no surviving element ever holds one. + quote::quote!(Sample), + quote::quote!(Sample<'a>), + quote::quote!(Sample<'a, u8, Vec>), + // Compositions, where a lost inner fact hides under an outer layer. + quote::quote!(Option>), + quote::quote!(Box>), + quote::quote!(Result>, Error>), + quote::quote!(Vec<&'a Sample>), + ] { + let ty = lower(spelling).expect("in the language"); + assert_eq!( + tokens(&ty.kind().to_syn()), + tokens(ty.syntax()), + "`{}` must spell back as itself", + tokens(ty.syntax()), + ); + } +} + +/// The two forms that reconstruct up to their own freedom rather than token for +/// token, each because the model deliberately keeps *what* was written and not +/// *how*. +/// +/// Stated as a test so the exemptions are a short, named list rather than a +/// silent gap in the one above. +#[test] +fn the_two_forms_that_do_not_spell_back_verbatim() { + // A callback's bounds are a set. `Send + Sync` and `Sync + Send` are one + // accepted form and nothing reads the order, so `to_syn` emits the canonical + // one — but the arguments, which everything reads, must survive exactly. + let cb = lower(quote::quote!(impl Fn(&Sample, u8) + Sync + Send + 'static)) + .expect("in the language"); + assert_eq!( + tokens(&cb.kind().to_syn()), + "impl Fn (& Sample , u8) + Send + Sync + 'static" + ); + + // A `Paren` (or an invisible `Group` from macro capture) wraps the same + // type, and the lowering sees through it: the node classifies as the inner + // type and reconstructs the inner spelling. + let parens = lower(quote::quote!((u8))).expect("in the language"); + assert_eq!(tokens(&parens.kind().to_syn()), "u8"); + assert_eq!( + tokens(parens.syntax()), + "u8", + "the slice is the inner node's" + ); +} diff --git a/prebindgen/src/api/core/flat/ty.rs b/prebindgen/src/api/core/flat/ty.rs index c36504c2..5f44af52 100644 --- a/prebindgen/src/api/core/flat/ty.rs +++ b/prebindgen/src/api/core/flat/ty.rs @@ -1,14 +1,25 @@ -//! Types: a closed classification paired with the syntax it was read from. +//! Types: the accepted syntax, paired with the tokens it was read from. //! -//! [`TypeRef`] is the pattern the whole element model follows — `kind` says what -//! the type *means*, `syntax` is the tokens the source wrote. Consumers -//! **classify off `kind` and spell off `syntax`**; see the [module docs](super) -//! for why that split is the point. +//! [`TypeKind`] is the subset of [`syn::Type`] a `#[prebindgen]` crate may +//! write — one variant per accepted **form**, nothing folded together, nothing +//! interpreted. What `&str` and `String` have in common is a *destination* +//! language's business, and the adapters are where that decision belongs. +//! +//! [`TypeRef`] pairs that kind with the tokens the source wrote. The pairing +//! survives the pivot because the two answer different questions — the kind is +//! the grammar an adapter may rely on, the syntax is what generated Rust must +//! spell — but the syntax is no longer *load-bearing*: nothing is recoverable +//! only from it. [`TypeKind::to_syn`] is what checks that, and +//! `syntax_is_recoverable_from_kind` is what runs it over the whole acceptance +//! corpus. //! //! [`TypeKind`] is total over the accepted grammar: a form with no variant here -//! is a form the language does not accept, so acceptance is a consequence of -//! lowering rather than a second list that can drift from it. Same contract, and -//! for the same reason, as [`lower_array_len`]. +//! is a form the language does not accept, so acceptance is mostly a +//! consequence of lowering rather than a second list that can drift from it. +//! Mostly: [`Uninit`](TypeKind::Uninit) is accepted in one **position** only, +//! which no variant set can express — see +//! [`OwnedUninit`](UnsupportedTypeReason::OwnedUninit). Same contract otherwise, +//! and for the same reason, as [`lower_array_len`]. use std::{fmt, rc::Rc}; @@ -20,12 +31,14 @@ use super::{ }; use crate::SourceLocation; -/// A type as the language decided it, plus the exact syntax it came from. +/// A type as the language accepted it, plus the exact syntax it came from. /// -/// The [`Origin::syntax`] slice is what removes the pressure to make `kind` -/// lossless: a lifetime, an elided argument, a `Box` that changes nothing -/// outside Rust all survive there at zero modelling cost, so `kind` can stay -/// language-neutral and small. +/// The [`Origin::syntax`] slice is what generated Rust spells. It is **not** +/// where facts go to survive a lossy classification any more — `kind` keeps the +/// lifetime, the wrapper and the argument it used to drop, and +/// [`TypeKind::to_syn`] proves it. Keeping the slice anyway is cheap, exact +/// (nothing has to reconstruct token for token what the source already wrote), +/// and it is what makes the proof possible at all. /// /// # The invariant /// @@ -70,16 +83,17 @@ use crate::SourceLocation; /// [`Registry::reference_output`](crate::api::core::registry::Registry::reference_output). #[derive(Clone, Debug)] pub struct TypeRef { - /// What the type means — the closed, destination-neutral classification. + /// The accepted syntax this type is — the closed grammar, not an + /// interpretation of it. pub(super) kind: TypeKind, /// The type as generated Rust must spell it — the source's own tokens, /// normalized to the flat namespace the generated crate can name (see /// [`Flat::parse`](super::Flat::parse)) — plus the source they came /// from. /// - /// The syntax can say strictly more than `kind` does — `Box` is a - /// `Str` here — which is the point: what Rust needs and no destination - /// language can see lives in the tokens, not in the classification. + /// It says exactly what `kind` says — that is the invariant + /// [`TypeKind::to_syn`] checks — and it says it in the source's own tokens, + /// which is why generated Rust re-emits this rather than a reconstruction. pub(super) origin: Origin, } @@ -165,13 +179,16 @@ impl TypeRef { // which reads the inner optional as a boundary layer when it is part of // the element, and `Option>` as two nullable layers when the // boundary has one way to say absent. + // Through the transparent wrappers, never past the node: a layer is read + // off `unwrapped`, while the type this returns is the one the source + // spelled — `Box` is a `Base` whose core still spells the `Box`. let mut core = self; - let optional = matches!(core.kind, TypeKind::Optional(_)); - if let TypeKind::Optional(inner) = &core.kind { + let optional = matches!(core.unwrapped().kind, TypeKind::Optional(_)); + if let TypeKind::Optional(inner) = &core.unwrapped().kind { core = inner; } - let iterable = matches!(core.kind, TypeKind::Sequence(_)); - if let TypeKind::Sequence(inner) = &core.kind { + let iterable = matches!(core.unwrapped().kind, TypeKind::Vec(_) | TypeKind::Slice(_)); + if let TypeKind::Vec(inner) | TypeKind::Slice(inner) = &core.unwrapped().kind { core = inner; } @@ -196,23 +213,46 @@ impl TypeRef { // un-require types the shape says are part of the element. let mut out = vec![self]; let mut cur = self; - if let TypeKind::Optional(inner) = &cur.kind { + if let TypeKind::Optional(inner) = &cur.unwrapped().kind { out.push(inner); cur = inner; } - if let TypeKind::Sequence(inner) = &cur.kind { + if let TypeKind::Vec(inner) | TypeKind::Slice(inner) = &cur.unwrapped().kind { out.push(inner); } out } + /// This type with every [transparent wrapper](TRANSPARENT_WRAPPERS) peeled + /// off — `Box>` → the `[T]` node, an unwrapped type → itself. + /// + /// **The fold, made explicit.** [`kind`](Self::kind) is the syntax the source + /// wrote, wrappers and all; a consumer that does not care which of them stand + /// over a type says so here, at its own call site, and the ones that must put + /// them back in generated Rust ask [`erased_wrappers`](Self::erased_wrappers) + /// instead. That split is why the wrapper is no longer erased during + /// lowering: the model reports, the consumer decides. + /// + /// Per layer, and only this one: a wrapper under a borrow or inside an + /// `Option` belongs to that inner node, which answers for itself. + pub fn unwrapped(&self) -> &TypeRef { + match &self.kind { + TypeKind::Boxed(inner) | TypeKind::Cow { inner, .. } => inner.unwrapped(), + _ => self, + } + } + /// What an `Option` wraps, else `None`. /// /// One layer, named. [`layer_stack`](Self::layer_stack) reads the whole /// arity stack; these three read exactly the layer a caller asks for, which /// is what a consumer wants when it can only *represent* some of them. + /// + /// Read through [`unwrapped`](Self::unwrapped), like every layer accessor + /// here: `Box>` is an optional to a destination language, and the + /// `Box` is still on the node for whoever has to spell it. pub fn optional_inner(&self) -> Option<&TypeRef> { - match &self.kind { + match &self.unwrapped().kind { TypeKind::Optional(inner) => Some(inner), _ => None, } @@ -220,18 +260,28 @@ impl TypeRef { /// The element of a run of values (`Vec`, `[T]`), else `None`. pub fn sequence_elem(&self) -> Option<&TypeRef> { - match &self.kind { - TypeKind::Sequence(elem) => Some(elem), + match &self.unwrapped().kind { + TypeKind::Vec(elem) | TypeKind::Slice(elem) => Some(elem), _ => None, } } /// What a borrow points at, else `None`. + /// + /// Through an out-parameter's [`Uninit`](TypeKind::Uninit): `&mut + /// MaybeUninit` points at a `T`'s storage, and the slot is not a type + /// anything converts, registers or crosses with. A consumer that needs to + /// tell the two borrows apart reads the [`kind`](Self::kind), where the + /// `MaybeUninit` the source wrote is still standing. pub fn borrow_target(&self) -> Option<&TypeRef> { - match &self.kind { - TypeKind::Ref { inner, .. } => Some(inner), - _ => None, - } + let inner = match &self.unwrapped().kind { + TypeKind::Ref { inner, .. } => inner, + _ => return None, + }; + Some(match &inner.kind { + TypeKind::Uninit(slot) => slot, + _ => inner, + }) } // ── Composition ─────────────────────────────────────────────── @@ -258,7 +308,8 @@ impl TypeRef { let inner = &self.origin.syntax; TypeRef { kind: TypeKind::Ref { - mode: RefMode::Shared, + lifetime: None, + mutable: false, inner: Box::new(self.clone()), }, origin: self.origin.with(syn::parse_quote!(&#inner)), @@ -303,6 +354,7 @@ impl TypeRef { id: TypeId { name: ident.to_string(), }, + args: Vec::new(), }, origin: Origin::new( syn::parse_quote!(#ident), @@ -383,12 +435,21 @@ impl TypeRef { /// they are missing from. pub fn erased_wrappers(&self) -> Vec<&'static str> { let mut names = Vec::new(); - let mut ty = std::borrow::Cow::Borrowed(&self.origin.syntax); - while let Some((name, inner)) = peel_transparent(&ty) { + let mut ty = self; + loop { + let name = match &ty.kind { + TypeKind::Boxed(inner) => { + ty = inner; + "Box" + } + TypeKind::Cow { inner, .. } => { + ty = inner; + "Cow" + } + _ => return names, + }; names.push(name); - ty = std::borrow::Cow::Owned(inner); } - names } /// This type's identity as a table key with every transparent wrapper @@ -434,16 +495,26 @@ impl TypeRef { /// a wrapper under a borrow or inside an `Option` belongs to that inner /// node's own spelling. pub fn stripped_syntax(&self) -> syn::Type { - let mut ty = self.origin.syntax.clone(); - while let Some((_, inner)) = peel_transparent(&ty) { - ty = inner; - } - ty + self.unwrapped().origin.syntax.clone() + } + + /// True when this is `&mut T` over a **value** — not `&mut MaybeUninit`. + /// + /// The one distinction an out-parameter's form makes to a converter: an + /// exclusive borrow may be read before it is written and an out-parameter + /// may not, so the two cannot share a conversion. Everything else about the + /// slot — that it points at a `T`, that the `T` is what crosses — is + /// [`borrow_target`](Self::borrow_target)'s answer. + pub fn is_exclusive_borrow(&self) -> bool { + matches!( + &self.unwrapped().kind, + TypeKind::Ref { mutable: true, inner, .. } if !matches!(inner.kind, TypeKind::Uninit(_)) + ) } /// The `Ok` and `Err` sides when this is a `Result`, else `None`. pub fn fallible_parts(&self) -> Option<(&TypeRef, &TypeRef)> { - match &self.kind { + match &self.unwrapped().kind { TypeKind::Fallible { ok, err } => Some((ok, err)), _ => None, } @@ -468,7 +539,7 @@ impl TypeRef { /// callback but was refused (a missing `Send`, an `impl Fn() -> u8`): the /// acceptance already happened, and asking again is how the two drift. pub fn callback_args(&self) -> Option<&[TypeRef]> { - match &self.kind { + match &self.unwrapped().kind { TypeKind::Callback { args } => Some(args), _ => None, } @@ -476,7 +547,7 @@ impl TypeRef { /// The extent of this type when it is an array, else `None`. pub fn array_extent(&self) -> Option<&ArrayExtent> { - match &self.kind { + match &self.unwrapped().kind { TypeKind::Array { extent, .. } => Some(extent), _ => None, } @@ -503,16 +574,24 @@ impl TypeRef { declared: &std::collections::HashSet, ) -> Option { match &self.kind { - TypeKind::Named { id } => (!declared.contains(&id.name)).then(|| id.name.clone()), - TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => { - t.first_unresolved(declared) - } + // The name resolves; the arguments do not. No declaration takes type + // parameters, so `Foo` is one reference to `Foo` — requiring + // `Bar` to be declared as well would refuse a reference the source + // crate compiles. + TypeKind::Named { id, .. } => (!declared.contains(&id.name)).then(|| id.name.clone()), + TypeKind::Optional(t) + | TypeKind::Vec(t) + | TypeKind::Slice(t) + | TypeKind::Boxed(t) + | TypeKind::Uninit(t) + | TypeKind::Cow { inner: t, .. } + | TypeKind::Ref { inner: t, .. } => t.first_unresolved(declared), TypeKind::Array { elem, .. } => elem.first_unresolved(declared), TypeKind::Fallible { ok, err } => ok .first_unresolved(declared) .or_else(|| err.first_unresolved(declared)), TypeKind::Callback { args } => args.iter().find_map(|a| a.first_unresolved(declared)), - TypeKind::Scalar(_) | TypeKind::Str | TypeKind::Unit => None, + TypeKind::Scalar(_) | TypeKind::Str | TypeKind::String | TypeKind::Unit => None, } } @@ -531,10 +610,20 @@ impl TypeRef { out } + // Both walks descend through [`unwrapped`](Self::unwrapped): a transparent + // wrapper is not a type of its own to a consumer that indexes or converts, + // so `Box>` reaches `Foo` and yields no node in between. fn collect_refs<'a>(&'a self, out: &mut Vec<&'a TypeRef>) { out.push(self); - match &self.kind { - TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => { + match &self.unwrapped().kind { + // Through [`borrow_target`](Self::borrow_target), so an + // out-parameter reaches the value and not its slot. + TypeKind::Ref { .. } => { + if let Some(t) = self.borrow_target() { + t.collect_refs(out) + } + } + TypeKind::Optional(t) | TypeKind::Vec(t) | TypeKind::Slice(t) | TypeKind::Uninit(t) => { t.collect_refs(out) } TypeKind::Array { elem, .. } => elem.collect_refs(out), @@ -543,79 +632,98 @@ impl TypeRef { err.collect_refs(out); } TypeKind::Callback { args } => args.iter().for_each(|t| t.collect_refs(out)), - TypeKind::Named { .. } | TypeKind::Scalar(_) | TypeKind::Str | TypeKind::Unit => {} + TypeKind::Named { .. } + | TypeKind::Scalar(_) + | TypeKind::Str + | TypeKind::String + | TypeKind::Unit => {} + // `unwrapped` peeled these off, so reaching one is impossible. + TypeKind::Boxed(_) | TypeKind::Cow { .. } => unreachable!(), } } fn collect_extents<'a>(&'a self, out: &mut Vec<&'a ArrayExtent>) { - match &self.kind { + match &self.unwrapped().kind { TypeKind::Array { elem, extent } => { out.push(extent); elem.collect_extents(out); } - TypeKind::Optional(t) | TypeKind::Sequence(t) | TypeKind::Ref { inner: t, .. } => { - t.collect_extents(out) - } + TypeKind::Optional(t) + | TypeKind::Vec(t) + | TypeKind::Slice(t) + | TypeKind::Uninit(t) + | TypeKind::Ref { inner: t, .. } => t.collect_extents(out), TypeKind::Fallible { ok, err } => { ok.collect_extents(out); err.collect_extents(out); } TypeKind::Callback { args } => args.iter().for_each(|t| t.collect_extents(out)), - TypeKind::Named { .. } => {} - TypeKind::Scalar(_) | TypeKind::Str | TypeKind::Unit => {} + TypeKind::Named { .. } + | TypeKind::Scalar(_) + | TypeKind::Str + | TypeKind::String + | TypeKind::Unit => {} + TypeKind::Boxed(_) | TypeKind::Cow { .. } => unreachable!(), } } } -/// What a [`TypeRef`] means. The variants are the accepted type grammar. +/// The **accepted syntax** of a [`TypeRef`]: the subset of [`syn::Type`] a +/// `#[prebindgen]` crate may write, and nothing more. +/// +/// One variant per accepted Rust **form**, not per destination concept. `str` +/// and `String` are two forms and get two variants; `Box` is a form of its +/// own and does not disappear into `T`. Nothing here folds two spellings +/// together, which is what makes [`TypeRef::syntax`] recoverable from this — +/// see [`TypeKind::to_syn`], the round-trip that checks it. /// -/// One Rust spelling per concept is **not** the rule here — several are. A -/// concept earns a variant when a destination language would act on it; a -/// spelling that changes nothing outside Rust folds into the concept it carries -/// and survives in [`TypeRef::syntax`]: +/// # Why it is only syntax /// -/// | Spelling | Kind | Why | -/// |---|---|---| -/// | `String`, `str` | [`Str`](TypeKind::Str) | one concept, two Rust types | -/// | `Vec`, `[T]` | [`Sequence`](TypeKind::Sequence) | a run of `T`; owned vs borrowed is the [`Ref`](TypeKind::Ref) layer's fact, not a second variant | -/// | `Box` | *whatever `T` is* | an owned `T` either way; nothing outside Rust can tell | +/// It was a *destination-neutral classification* once, and that leaked: `&T` +/// earned a layer while `Box` was declared transparent, on no principle +/// either adapter shared, and `Cbindgen` went on picking its C type from the +/// Rust spelling anyway. Deciding that `&str` and `String` are both "a string" +/// is a **destination** decision, so it belongs to the destination — the model +/// hands over what the source wrote and stays out of it. +/// +/// Where two adapters want the same fold, it is a *reading*, not a variant: +/// [`TypeRef::unwrapped`] peels `Box`/`Cow` for the consumers that want them +/// gone, and the ones that must rebuild the Rust value ask +/// [`TypeRef::erased_wrappers`] instead. One helper, visible at the call site, +/// rather than a fold baked into every classification. #[derive(Clone, Debug)] pub enum TypeKind { - /// A primitive with a fixed C/JVM counterpart. - Scalar(ScalarKind), - /// A UTF-8 string — `String` owned, `str` behind a [`Ref`](TypeKind::Ref). + /// A primitive with a fixed C/JVM counterpart — `u8`, `bool`, `f64`. /// - /// Both spellings are one concept: `&str` and `&String` are each a borrowed - /// string and classify identically, which is what every adapter already - /// does by hand. + /// A closed set of bare idents, so recognising one is reading the syntax + /// rather than interpreting it — and it keeps every adapter off a name + /// table of its own. + Scalar(ScalarKind), + /// `str` — unsized, so it is only ever reached through a + /// [`Ref`](TypeKind::Ref) or a wrapper. Str, + /// `String`. + String, /// `Option`. Optional(Box), - /// A run of `T` — `Vec` owned, `[T]` behind a [`Ref`](TypeKind::Ref). - /// - /// One variant, because ownership is already the [`Ref`](TypeKind::Ref) - /// layer's fact: `&[T]` is `Ref(Sequence)`, `Vec` is `Sequence`. A - /// second variant would encode ownership twice and let the two copies - /// disagree. `[T; N]` is *not* this — a fixed extent is a different - /// concept, see [`Array`](TypeKind::Array). - Sequence(Box), + /// `Vec`. + Vec(Box), + /// `[T]` — the unsized run, reached through a [`Ref`](TypeKind::Ref) or a + /// wrapper. Not the same form as [`Vec`](TypeKind::Vec), so not the same + /// variant. + Slice(Box), /// `Result`. Fallible { ok: Box, err: Box }, /// Any other named type: a `#[prebindgen]` struct or enum, or a foreign /// path. /// - /// `id` is the type's **identity** — a name, not syntax, so nothing outside - /// this module has to take a path apart to learn what a type is. The last - /// segment's generic arguments live in `args`, and only the *type* - /// arguments: a lifetime argument says nothing a destination language can - /// act on. The full spelling is in [`TypeRef::syntax`] for whoever re-emits it. - Named { id: TypeId }, + /// `id` is the type's **identity** — a name, not a `syn::Path`, so nothing + /// downstream has to take a path apart to learn what a type is. `args` is + /// the last segment's generic arguments, in the order they were written and + /// including lifetimes, because dropping either would make the spelling + /// unrecoverable. + Named { id: TypeId, args: Vec }, /// `[T; N]` — a run of `T` whose length is known at compile time. - /// - /// Deliberately not a [`Sequence`](TypeKind::Sequence) with an optional - /// extent: a fixed array crosses by value as a primitive array, a `Vec` - /// crosses as a heap collection, and every adapter branches between the two - /// at every site. Array { elem: Box, /// Boxed: an extent carries an [`Origin`] over the length expression, which @@ -623,43 +731,158 @@ pub enum TypeKind { /// The same trade-off [`Unsupported::error`](super::Unsupported) makes. extent: Box, }, - /// A borrow — `&T`, `&mut T`, or `&mut MaybeUninit`. The lifetime is - /// spelling, so it lives in [`TypeRef::syntax`] rather than here. + /// A borrow — `&T` or `&mut T`, with the lifetime the source wrote. /// - /// This is the ownership layer for every concept underneath it: `&str` is - /// `Ref(Str)`, `&[T]` is `Ref(Sequence)`. A shared-ownership handle - /// (`Arc`, `Rc`) belongs here too when the language accepts one. + /// An out-parameter is `&mut` over [`Uninit`](TypeKind::Uninit), which is + /// what the source spells. What that *means* at a boundary — the caller + /// supplies the slot, the callee fills it — is the adapter's reading of the + /// form, not a third value of a mode enum. + Ref { + lifetime: Option, + mutable: bool, + inner: Box, + }, + /// `Box`. + /// + /// A form of its own. It was erased once, on the grounds that no + /// destination language can tell `Box` from `T` — true, and still the + /// adapter's call to make: [`TypeRef::unwrapped`] makes it, on demand. + Boxed(Box), + /// `Cow<'a, T>`. /// - /// `inner` is always the borrowed *value's* type, so an out-parameter's - /// `MaybeUninit` is absorbed into [`RefMode::Out`] rather than wrapping it: - /// uninitialized-ness is a property of the **borrow**, not of the type, and - /// it is meaningless anywhere else. - Ref { mode: RefMode, inner: Box }, + /// The lifetime is **not** optional: `Cow` has one in its own signature, so + /// a `Cow` is not Rust and no source crate can compile it. Lowering + /// refuses the shape ([`WrongGenericArguments`](UnsupportedTypeReason::WrongGenericArguments)) + /// rather than modelling an absence that would then have to be spelled back + /// as something the source did not write. + Cow { + lifetime: syn::Lifetime, + inner: Box, + }, + /// `MaybeUninit`. + /// + /// Accepted **only** directly under a `&mut` — see + /// [`UnsupportedTypeReason::OwnedUninit`]. It has a variant because the + /// source writes it; that it is refused elsewhere is an acceptance rule, + /// which is a separate question from how the form is represented. + Uninit(Box), /// `impl Fn(A, B, …) + Send + Sync + 'static` — the callback form. Callback { args: Vec }, /// `()`. Unit, } -/// What a borrow permits, and what the callee owes. +/// One generic argument of a [`Named`](TypeKind::Named) type, as written. /// -/// One axis with three values rather than a `mutable` flag plus a wrapper, so the -/// combinations that mean nothing at a boundary — a shared borrow of -/// uninitialized storage, an owned `MaybeUninit` — cannot be written down. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RefMode { - /// `&T` — the callee may read it and must not write it. - Shared, - /// `&mut T` — the callee may read and write an already-valid `T`. - Exclusive, - /// `&mut MaybeUninit` — an **out-parameter**: the caller supplies storage - /// only, and the callee's job is to make it a valid `T`. The callee may not - /// read it first. +/// A lifetime is kept rather than dropped: no destination language acts on it, +/// but `Foo<'a>` is not `Foo`, and a model that cannot say which one the source +/// wrote cannot claim to have lost nothing. +#[derive(Clone, Debug)] +pub enum GenericArg { + Lifetime(syn::Lifetime), + /// Boxed so a lifetime argument — the common one, and a fraction of the + /// size — does not pay for a type it is not. The same trade-off + /// [`Array`](TypeKind::Array)'s extent makes. + Type(Box), +} + +impl TypeKind { + /// This kind spelled back as Rust — the inverse of the lowering. + /// + /// # What it is for + /// + /// **Not** for generating code: generated Rust spells + /// [`TypeRef::syntax`], the source's own tokens, and always will. This + /// exists so that claim can be *checked* — a kind that cannot reproduce the + /// syntax it was lowered from has dropped something, and the round-trip test + /// is what says so before a consumer has to discover it. + /// + /// Two forms reconstruct up to their own freedom rather than token for + /// token, because the model keeps what was written and not how it was + /// written: /// - /// This is a boundary concept every destination language has (C's `T *out`), - /// which is why it is modelled here rather than left as a nominal - /// `MaybeUninit` for each adapter to recognise. - Out, + /// * a `Group` or `Paren` around a type, which the lowering sees through; + /// * a [`Callback`](TypeKind::Callback)'s bound *order* — `Send + Sync` and + /// `Sync + Send` are one accepted form, and nothing reads the order. + pub fn to_syn(&self) -> syn::Type { + let opt_lifetime = + |l: &Option| l.as_ref().map(|l| quote::quote!(#l)).unwrap_or_default(); + match self { + Self::Scalar(k) => { + let ident = syn::Ident::new(k.as_str(), proc_macro2::Span::call_site()); + syn::parse_quote!(#ident) + } + Self::Str => syn::parse_quote!(str), + Self::String => syn::parse_quote!(String), + Self::Optional(t) => { + let inner = t.kind.to_syn(); + syn::parse_quote!(Option<#inner>) + } + Self::Vec(t) => { + let inner = t.kind.to_syn(); + syn::parse_quote!(Vec<#inner>) + } + Self::Slice(t) => { + let inner = t.kind.to_syn(); + syn::parse_quote!([#inner]) + } + Self::Boxed(t) => { + let inner = t.kind.to_syn(); + syn::parse_quote!(Box<#inner>) + } + Self::Uninit(t) => { + let inner = t.kind.to_syn(); + syn::parse_quote!(MaybeUninit<#inner>) + } + Self::Cow { lifetime, inner } => { + let inner = inner.kind.to_syn(); + syn::parse_quote!(Cow<#lifetime, #inner>) + } + Self::Fallible { ok, err } => { + let (ok, err) = (ok.kind.to_syn(), err.kind.to_syn()); + syn::parse_quote!(Result<#ok, #err>) + } + Self::Ref { + lifetime, + mutable, + inner, + } => { + let lt = opt_lifetime(lifetime); + let mutability = mutable.then(|| quote::quote!(mut)).unwrap_or_default(); + let inner = inner.kind.to_syn(); + syn::parse_quote!(& #lt #mutability #inner) + } + Self::Array { elem, extent } => { + let elem = elem.kind.to_syn(); + let len = &extent.origin.syntax; + syn::parse_quote!([#elem; #len]) + } + Self::Named { id, args } => { + // The name is a spelling, so it parses back as one — including + // the leading `::` and any path segments before the last. + let mut path: syn::Path = + syn::parse_str(&id.name).expect("a name this model built from a path"); + if !args.is_empty() { + let args = args.iter().map(|a| match a { + GenericArg::Lifetime(l) => quote::quote!(#l), + GenericArg::Type(t) => { + let t = t.kind.to_syn(); + quote::quote!(#t) + } + }); + let last = path.segments.last_mut().expect("a non-empty path"); + last.arguments = + syn::PathArguments::AngleBracketed(syn::parse_quote!(<#(#args),*>)); + } + syn::parse_quote!(#path) + } + Self::Callback { args } => { + let args = args.iter().map(|a| a.kind.to_syn()); + syn::parse_quote!(impl Fn(#(#args),*) + Send + Sync + 'static) + } + Self::Unit => syn::parse_quote!(()), + } + } } /// A nominal type's identity: a name, and nothing else. @@ -788,22 +1011,38 @@ pub enum UnsupportedTypeReason { DisallowedImplTrait, /// A generic that takes a fixed arity and did not get it — `Option` with no /// argument, `Result` with one. + /// + /// Counts **type** arguments, which is the whole question for every builtin + /// but one: a lifetime on `Option`, `Vec`, `Box` or `Result` is not a shape + /// this language has, and such a spelling is a nominal type nobody declared + /// rather than a builtin with a bad argument. `Cow` is the exception and has + /// its own reason, [`WrongGenericArguments`](Self::WrongGenericArguments). WrongGenericArity { expected: usize }, + /// A builtin whose whole argument list is not the shape it takes — + /// `Cow` (no lifetime), `Cow` (wrong order), `Cow<'a, 'b, u8>` + /// (two lifetimes). + /// + /// Separate from [`WrongGenericArity`](Self::WrongGenericArity) because it + /// is about the list and not its type-argument count: each of those three + /// has exactly one type argument, and refusing them is what keeps + /// [`TypeKind::to_syn`] able to spell every accepted form back. + WrongGenericArguments { expected: &'static str }, /// A non-empty tuple. Only `()` is in the language: no adapter has ever /// lowered a tuple, so accepting one would defer the failure to a late /// "unresolved type" instead of naming it here. UnsupportedTuple, - /// `MaybeUninit` somewhere other than behind a `&mut`. + /// `MaybeUninit` somewhere other than directly under a `&mut`. /// - /// Uninitialized storage is a property of a *borrow*, not of a type — see - /// [`RefMode::Out`]. Owned, returned or stored in a field it promises nothing - /// a destination language can use, and reading it would be undefined. + /// The one acceptance rule about a **position** rather than a form: + /// [`Uninit`](TypeKind::Uninit) exists, and only an out-parameter can hold + /// one. Owned, returned or stored in a field it promises nothing a + /// destination language can use, and reading it would be undefined. OwnedUninit, /// `&MaybeUninit` — a shared borrow of uninitialized storage. /// /// A shared borrow promises a readable `T`, and this supplies storage that may - /// not be one. Only `&mut MaybeUninit` means anything: see - /// [`RefMode::Out`]. + /// not be one. Only `&mut MaybeUninit` means anything — see + /// [`Uninit`](TypeKind::Uninit). SharedUninit, /// A path with a qualified self — `::Assoc`. /// @@ -838,6 +1077,12 @@ impl fmt::Display for UnsupportedType { "type `{}` needs exactly {expected} type argument(s)", self.offending ), + UnsupportedTypeReason::WrongGenericArguments { expected } => write!( + f, + "type `{}` is not the shape `{expected}` \u{2014} its arguments are the ones \ + that type takes, in the order it takes them", + self.offending + ), UnsupportedTypeReason::UnsupportedTuple => write!( f, "type `{}` is a tuple; only the unit `()` is supported — return the \ @@ -885,25 +1130,30 @@ impl std::error::Error for UnsupportedType {} /// `at` is the origin of the item this type was written in — the location every /// node lowered from that item shares, and the crate an array extent's const /// must come from. -/// The wrappers this language **erases**: `W` classifies as whatever `T` -/// classifies as, because no destination language can tell them apart. +/// The wrappers a destination language **cannot see**: `W` crosses as +/// whatever `T` crosses as, because nothing outside Rust can tell them apart. /// -/// The single source of truth for that set. [`lower_type`] erases exactly these, -/// and an adapter that has to *undo* one in generated Rust reads the same list — -/// so the question "which wrappers are transparent?" has one answer instead of a -/// copy per consumer that can drift out of step. +/// The single source of truth for that set. [`TypeRef::unwrapped`] peels exactly +/// these, and an adapter that has to *put one back* in generated Rust reads the +/// same list — so the question "which wrappers are transparent?" has one answer +/// instead of a copy per consumer that can drift out of step. /// -/// Adding one is adding a row here. What it means for a given destination is -/// that adapter's business: erasing a wrapper says nothing about whether Rust -/// can move a value out of it, which is why `Cow` is on this list and is still +/// It is a **reading**, not a classification: [`TypeKind`] keeps every wrapper +/// the source wrote, and a consumer says here, at its own call site, that it +/// does not care. What that means for a given destination is still that +/// adapter's business — erasing a wrapper says nothing about whether Rust can +/// move a value out of it, which is why `Cow` is on this list and is still /// refused where a converter would have to move its payload. pub const TRANSPARENT_WRAPPERS: &[&str] = &["Box", "Cow"]; /// Strip one [transparent wrapper](TRANSPARENT_WRAPPERS) from a **spelling**, /// naming the one removed — `Box>` → `("Box", Option)`. /// -/// Spelling in, spelling out: this is the inverse of the erasure, for a consumer -/// that must reconstruct in Rust what the classification dropped. +/// Spelling in, spelling out — the syntax-side peer of +/// [`TypeRef::unwrapped`], for an adapter comparing a spelling it composed +/// against one it has a converter for. Here rather than in the adapter because +/// taking a `syn::Type` apart is this module's job, and doing it next door would +/// put a classifier back outside the model. pub fn peel_transparent(ty: &syn::Type) -> Option<(&'static str, syn::Type)> { let syn::Type::Path(tp) = ty else { return None }; let seg = tp.path.segments.last()?; @@ -933,25 +1183,27 @@ pub(crate) fn lower_type( // spelling, which is the one a consumer wants to emit. syn::Type::Group(g) => return lower_type(&g.elem, consts, at), syn::Type::Paren(p) => return lower_type(&p.elem, consts, at), - // The mode is read off the borrow AND its target together, because - // `&mut MaybeUninit` is one concept — an out-parameter — rather than a - // mutable borrow of a distinct `MaybeUninit` type. + // The borrow and its target are read together for one reason only: a + // `MaybeUninit` is accepted **here** and refused everywhere else, so the + // position is what decides, and only this arm knows it. syn::Type::Reference(r) => { - let (mode, target) = match maybe_uninit_inner(&r.elem) { - Some(inner) if r.mutability.is_some() => (RefMode::Out, inner), + let inner = match maybe_uninit_inner(&r.elem) { + Some(uninit) if r.mutability.is_some() => TypeRef { + kind: TypeKind::Uninit(Box::new(lower_type(&uninit, consts, at)?)), + origin: Origin::new((*r.elem).clone(), Rc::clone(at)), + }, // `&MaybeUninit` promises a readable `T` and supplies storage // that may not be one. Nothing at a boundary can use it. Some(_) => return Err(fail(UnsupportedTypeReason::SharedUninit)), - None if r.mutability.is_some() => (RefMode::Exclusive, (*r.elem).clone()), - None => (RefMode::Shared, (*r.elem).clone()), + None => lower_type(&r.elem, consts, at)?, }; TypeKind::Ref { - mode, - inner: Box::new(lower_type(&target, consts, at)?), + lifetime: r.lifetime.clone(), + mutable: r.mutability.is_some(), + inner: Box::new(inner), } } - // `[T]` is the borrowed spelling of the same concept `Vec` owns. - syn::Type::Slice(s) => TypeKind::Sequence(Box::new(lower_type(&s.elem, consts, at)?)), + syn::Type::Slice(s) => TypeKind::Slice(Box::new(lower_type(&s.elem, consts, at)?)), _ if is_unit_type(ty) => TypeKind::Unit, // Only the unit is in the language. Refusing here names the type; // accepting would defer the failure to an "unresolved type" much later. @@ -1007,20 +1259,23 @@ fn lower_path( }; let name = last.ident.to_string(); - // Type arguments only. A lifetime argument is accepted and dropped: it is - // part of the spelling (`Foo<'a>` is not `Foo`), and the spelling is in - // `TypeRef::origin`, so modelling it would be a second copy of one fact. + // Every argument is kept, in the order it was written — a lifetime among + // them. `Foo<'a>` is not `Foo`, and a model that drops the difference cannot + // spell the type back. let mut has_lifetime_arg = false; - let args: Vec = match &last.arguments { + let args: Vec = match &last.arguments { syn::PathArguments::None => Vec::new(), syn::PathArguments::AngleBracketed(ab) => { let mut out = Vec::new(); for a in &ab.args { match a { syn::GenericArgument::Type(t) => { - out.push(lower_type(t, consts, at)?); + out.push(GenericArg::Type(Box::new(lower_type(t, consts, at)?))); + } + syn::GenericArgument::Lifetime(l) => { + has_lifetime_arg = true; + out.push(GenericArg::Lifetime(l.clone())); } - syn::GenericArgument::Lifetime(_) => has_lifetime_arg = true, _ => return Err(fail(UnsupportedTypeReason::UnsupportedGenericArgument)), } } @@ -1030,6 +1285,19 @@ fn lower_path( return Err(fail(UnsupportedTypeReason::UnsupportedForm)) } }; + // `Named` holds the last segment's arguments, so a generic anywhere else is + // a spelling this model cannot give back. Refused rather than dropped: no + // flat API writes `a::B::C`. + if tp + .path + .segments + .iter() + .rev() + .skip(1) + .any(|s| !matches!(s.arguments, syn::PathArguments::None)) + { + return Err(fail(UnsupportedTypeReason::UnsupportedForm)); + } // A builtin must be spelled BARE. `normalize_type` has already reduced the // real std paths (`std::option::Option` → `Option`) at ingest and @@ -1038,16 +1306,14 @@ fn lower_path( // is not `Option`, and collapsing it would silently retype the field. let is_bare = tp.path.leading_colon.is_none() && tp.path.segments.len() == 1; if is_bare { - if args.is_empty() && !has_lifetime_arg { + if args.is_empty() { if let Some(kind) = ScalarKind::from_name(&name) { return Ok(TypeKind::Scalar(kind)); } - // `String` and `str` are one concept. `str` is unsized and so only - // ever appears behind a `&`, which the `Ref` layer already records - // — classifying it as a nominal type instead would send every - // adapter looking for an item named `str` to resolve. - if name == "String" || name == "str" { - return Ok(TypeKind::Str); + match name.as_str() { + "String" => return Ok(TypeKind::String), + "str" => return Ok(TypeKind::Str), + _ => {} } } // A builtin generic takes TYPE arguments only — a lifetime on one is not a @@ -1055,9 +1321,15 @@ fn lower_path( // lifetime, so it is the one builtin where a lifetime argument is expected // rather than refused. if !has_lifetime_arg || name == "Cow" { - let mut args = args; + let mut types: Vec = args + .iter() + .filter_map(|a| match a { + GenericArg::Type(t) => Some((**t).clone()), + GenericArg::Lifetime(_) => None, + }) + .collect(); let arity = |n: usize| { - if args.len() == n { + if types.len() == n { Ok(()) } else { Err(fail(UnsupportedTypeReason::WrongGenericArity { @@ -1068,40 +1340,48 @@ fn lower_path( match name.as_str() { "Option" => { arity(1)?; - return Ok(TypeKind::Optional(Box::new(args.remove(0)))); + return Ok(TypeKind::Optional(Box::new(types.remove(0)))); } "Vec" => { arity(1)?; - return Ok(TypeKind::Sequence(Box::new(args.remove(0)))); + return Ok(TypeKind::Vec(Box::new(types.remove(0)))); } - // Reached here, it is not behind a `&mut`, so it is not an - // out-parameter — see `RefMode::Out`, which is the only place - // uninitialized storage means anything. - "MaybeUninit" => return Err(fail(UnsupportedTypeReason::OwnedUninit)), - // `Box` **is** `T`: an owned value either way, and no - // destination language can tell the two apart. So it carries no - // kind of its own and classifies as whatever it wraps — the - // `Box` survives in `TypeRef::origin`, which is what generated - // Rust spells. (A shared-ownership handle would classify as a - // `Ref` for the same reason, when the language accepts one.) - // `Box` and `Cow` are erased — see `TRANSPARENT_WRAPPERS`, - // which is the list this arm consults so the set cannot drift - // from the one adapters undo. - w if TRANSPARENT_WRAPPERS.contains(&w) => { + "Box" => { arity(1)?; - return Ok(args.remove(0).kind); + return Ok(TypeKind::Boxed(Box::new(types.remove(0)))); } + // The one builtin whose signature has a lifetime, so it is the + // one whose WHOLE argument list has to be checked: counting type + // arguments alone accepts `Cow` and `Cow<'a, 'b, u8>`, + // which are not `Cow`s at all, and a model that then kept only + // the first lifetime could not spell either one back. + "Cow" => { + let [GenericArg::Lifetime(lifetime), GenericArg::Type(inner)] = &args[..] + else { + return Err(fail(UnsupportedTypeReason::WrongGenericArguments { + expected: "Cow<'a, T>", + })); + }; + return Ok(TypeKind::Cow { + lifetime: lifetime.clone(), + inner: inner.clone(), + }); + } + // Reached here it is not directly under a `&mut`, and that is the + // one position where uninitialized storage means anything — + // `TypeKind::Uninit` is built by the reference arm alone. + "MaybeUninit" => return Err(fail(UnsupportedTypeReason::OwnedUninit)), "Result" => { arity(2)?; - let err = Box::new(args.remove(1)); - let ok = Box::new(args.remove(0)); + let err = Box::new(types.remove(1)); + let ok = Box::new(types.remove(0)); return Ok(TypeKind::Fallible { ok, err }); } - _ => return Ok(named(tp)), + _ => return Ok(named(tp, args)), } } } - Ok(named(tp)) + Ok(named(tp, args)) } /// If `ty` is a bare `MaybeUninit`, the `T` it holds storage for. @@ -1144,24 +1424,25 @@ pub(crate) fn is_unit_type(ty: &syn::Type) -> bool { /// `Named` with the identity read off the path: every segment joined, minus the /// generic arguments, which are already in `args`. -/// `Named` with the identity read off the path: every segment joined, minus the -/// generic arguments. /// -/// The arguments are **lowered but not retained** — a bad type inside one is still -/// diagnosed, it just leaves no trace. Nothing could read them: a surviving -/// reference resolves to a declared type, and no declaration takes type parameters, -/// so `Foo` against a declared `Foo` would not compile in the source crate. -/// Accepting *instantiated* generics — `Wrapper` as its own declared type — is -/// what would bring the field back. -fn named(tp: &syn::TypePath) -> TypeKind { - let name = tp - .path - .segments - .iter() - .map(|s| s.ident.to_string()) - .collect::>() - .join("::"); +/// The leading `::` rides along in the name when the source wrote one. It is +/// nothing a destination language acts on — but the name is what +/// [`TypeKind::to_syn`] spells the path back from, and `::a::B` is not `a::B`. +fn named(tp: &syn::TypePath, args: Vec) -> TypeKind { + let mut name = String::new(); + if tp.path.leading_colon.is_some() { + name.push_str("::"); + } + name.push_str( + &tp.path + .segments + .iter() + .map(|s| s.ident.to_string()) + .collect::>() + .join("::"), + ); TypeKind::Named { id: TypeId { name }, + args, } } diff --git a/prebindgen/src/api/core/registry/scan.rs b/prebindgen/src/api/core/registry/scan.rs index e076edf5..b88cf404 100644 --- a/prebindgen/src/api/core/registry/scan.rs +++ b/prebindgen/src/api/core/registry/scan.rs @@ -393,11 +393,10 @@ impl Registry { /// /// The children come from [`TypeKind`], not from taking the syntax apart, and /// the difference is load-bearing rather than cosmetic. `&mut MaybeUninit` - /// is `Ref { mode: Out, inner: T }` — the model absorbed the `MaybeUninit`, so - /// the edge lands on `T` directly instead of on an intermediate - /// `MaybeUninit` that no source ever wrote and no adapter can convert. - /// Each edge is still *spelled* from the child's own `origin.syntax`, which is - /// what the caller keys the table by. + /// yields `T` — [`borrow_target`](crate::api::core::flat::TypeRef::borrow_target) + /// sees past the slot — instead of an intermediate `MaybeUninit` that no + /// adapter can convert and no table holds. Each edge is still *spelled* from + /// the child's own `origin.syntax`, which is what the caller keys the table by. /// /// The reading comes from **this registry's own table**, where `ensure_entry` /// put it before the walk reached this type — so a spelling the binding composed @@ -414,21 +413,38 @@ impl Registry { let mut out: Vec<(Direction, crate::api::core::flat::TypeRef)> = Vec::new(); if let Some(reading) = self.type_table(dir).get(key).map(|c| &c.subject) { let (children, child_dir): (Vec<&crate::api::core::flat::TypeRef>, Direction) = - match reading.kind() { + match reading.unwrapped().kind() { + // Through the accessor, not the field: it sees past an + // out-parameter's `MaybeUninit` slot, which is storage rather + // than a type any converter is keyed by. + // `expect`, not a fallible collect: a `Ref` kind always has a + // target, so an empty child list here would mean the accessor + // and the kind disagree — and it would silently truncate the + // graph walk instead of saying so. + TypeKind::Ref { .. } => ( + vec![reading + .borrow_target() + .expect("a `Ref` kind has a borrow target")], + dir, + ), TypeKind::Optional(t) - | TypeKind::Sequence(t) - | TypeKind::Ref { inner: t, .. } => (vec![t], dir), + | TypeKind::Vec(t) + | TypeKind::Slice(t) + | TypeKind::Uninit(t) => (vec![t], dir), TypeKind::Array { elem, .. } => (vec![elem], dir), TypeKind::Fallible { ok, err } => (vec![ok, err], dir), TypeKind::Callback { args } => (args.iter().collect(), dir.flip()), - // A name is a leaf in the type graph: its generic arguments are - // lowered but not retained, because no declaration takes type - // parameters. Its *fields* are the edges, and they come off the - // element below. + // A name is a leaf in the type graph: its generic arguments + // belong to the reference, not to a declaration, because no + // declaration takes type parameters. Its *fields* are the + // edges, and they come off the element below. TypeKind::Named { .. } | TypeKind::Scalar(_) | TypeKind::Str + | TypeKind::String | TypeKind::Unit => (Vec::new(), dir), + // `unwrapped` peeled these off. + TypeKind::Boxed(_) | TypeKind::Cow { .. } => (Vec::new(), dir), }; // The child reading itself, not its spelling: it has already been // classified — by the model, or by whoever composed the parent — so @@ -472,13 +488,13 @@ impl Registry { // `Named { id: Node }` — `Box` **is** `T` in this language — so it // reaches `Node`'s fields, where asking the syntax for a bare ident would // have answered `None` and dead-ended the walk. - if let Some(name) = self - .type_table(dir) - .get(key) - .and_then(|c| match c.subject.kind() { - TypeKind::Named { id } => Some(id.name.clone()), - _ => None, - }) + if let Some(name) = + self.type_table(dir) + .get(key) + .and_then(|c| match c.subject.unwrapped().kind() { + TypeKind::Named { id, .. } => Some(id.name.clone()), + _ => None, + }) { use crate::api::core::flat::{Field, Type}; let fields: Vec<&Field> = match self.flat.declared_type(name.as_str()) { diff --git a/prebindgen/src/api/core/registry/tests.rs b/prebindgen/src/api/core/registry/tests.rs index c5f9e52d..25794d98 100644 --- a/prebindgen/src/api/core/registry/tests.rs +++ b/prebindgen/src/api/core/registry/tests.rs @@ -971,7 +971,7 @@ fn an_adapter_authored_type_cell_is_classified_but_placeless() { let cell = ®.input_types[&TypeKey::parse("Foreign").expect("test type")]; assert!(cell.root, "the binding asked for it directly"); assert!( - matches!(cell.subject.kind(), TypeKind::Named { id } if id.name == "Foreign"), + matches!(cell.subject.kind(), TypeKind::Named { id, .. } if id.name == "Foreign"), "a declared name is a name, and the grammar can say so" ); assert!( diff --git a/prebindgen/src/api/core/unfold.rs b/prebindgen/src/api/core/unfold.rs index f343098b..110b2fb3 100644 --- a/prebindgen/src/api/core/unfold.rs +++ b/prebindgen/src/api/core/unfold.rs @@ -418,10 +418,10 @@ pub fn apply( let (by_ref, core_ty) = peel_borrow(arg_ty); // Only a NAMED core can match a deconstructor target: an // `Option` / `Vec` / tuple arg is delivered whole. The model - // says which, so a wrapper the language sees through — `Box` — - // no longer reads as un-nameable. + // says which, and `unwrapped` is where a wrapper the destination + // cannot see — `Box` — stops reading as un-nameable. if !matches!( - core_ty.kind(), + core_ty.unwrapped().kind(), crate::api::core::flat::TypeKind::Named { .. } ) { continue; @@ -1743,16 +1743,16 @@ fn accessor_signature( .function(&func) .ok_or_else(|| UnfoldError::UnknownAccessor(func.clone()))?; - // First parameter is the receiver `&T`; peel the borrow to get `T`. The - // borrow is `TypeKind::Ref`, so the peel reads the classification instead of - // re-deciding it from `syn::Type::Reference`. + // First parameter is the receiver `&T`; peel the borrow to get `T`. + // `borrow_target` is the model's own answer, so the peel reads a + // classification instead of re-deciding it from `syn::Type::Reference`. let first = f .params .first() .ok_or_else(|| UnfoldError::UnknownAccessor(func.clone()))?; - let takes = match first.ty.kind() { - crate::api::core::flat::TypeKind::Ref { inner, .. } => inner.syntax().clone(), - _ => first.ty.syntax().clone(), + let takes = match first.ty.borrow_target() { + Some(inner) => inner.syntax().clone(), + None => first.ty.syntax().clone(), }; Ok((takes, f.ret.clone())) } @@ -1788,7 +1788,7 @@ fn accessor_consumes(registry: &Registry, func: &syn::Ident) -> bool { .flat() .function(&func) .and_then(|f| f.params.first()) - .is_some_and(|p| !matches!(p.ty.kind(), crate::api::core::flat::TypeKind::Ref { .. })) + .is_some_and(|p| p.ty.borrow_target().is_none()) } fn check_takes( diff --git a/prebindgen/src/api/lang/cbindgen/emit.rs b/prebindgen/src/api/lang/cbindgen/emit.rs index 7f29ccc8..5df0b483 100644 --- a/prebindgen/src/api/lang/cbindgen/emit.rs +++ b/prebindgen/src/api/lang/cbindgen/emit.rs @@ -351,9 +351,11 @@ impl CbindgenBuilder { /// Whether any declared function returns a `Vec<_>` (possibly nested under /// `Result`/`Option`), so the array builder/freer prelude must be emitted. /// - /// `TypeKind::Sequence` is the whole question: it is what `Vec` lowers to, - /// and — since `Cow<'_, T>` **is** `T` — what `Cow<'_, [T]>` lowers to as well, - /// so the two spellings this used to test separately are one classification. + /// A run of values is the whole question — `Vec` and `[T]` alike, and + /// through a transparent wrapper, so `Cow<'_, [T]>` counts as the `Vec` + /// it crosses as. [`sequence_elem`](crate::api::core::flat::TypeRef::sequence_elem) + /// answers all three, which is why the two spellings this used to test + /// separately need no arms of their own. pub(super) fn produces_array(&self, registry: &Registry<()>) -> bool { self.functions.keys().any(|orig| { registry @@ -361,12 +363,7 @@ impl CbindgenBuilder { .function(&orig) // The model already decided that an elided return and `-> ()` // are one thing, so there is no second arm to write here. - .map(|f| { - f.ret - .walk() - .iter() - .any(|t| matches!(t.kind(), crate::api::core::flat::TypeKind::Sequence(_))) - }) + .map(|f| f.ret.walk().iter().any(|t| t.sequence_elem().is_some())) .unwrap_or(false) }) } diff --git a/prebindgen/src/api/lang/jnigen/jni/builder.rs b/prebindgen/src/api/lang/jnigen/jni/builder.rs index 063ebcb0..c775dfd3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/builder.rs +++ b/prebindgen/src/api/lang/jnigen/jni/builder.rs @@ -127,7 +127,7 @@ impl Declarations { /// `Box>` reaches the same declaration `Priority` does, /// where taking the spelling apart finds `Box` and answers about it. pub(crate) fn is_kotlin_enum_reading(&self, reading: &TypeRef) -> bool { - let flat::TypeKind::Named { id } = enum_probe(reading).kind() else { + let flat::TypeKind::Named { id, .. } = enum_probe(reading).unwrapped().kind() else { return false; }; id.ident() @@ -899,7 +899,7 @@ impl Declarations { // model already normalized them. let ret = accessor.ret.borrow_target().unwrap_or(&accessor.ret); assert!( - !matches!(ret.kind(), flat::TypeKind::Unit), + !matches!(ret.unwrapped().kind(), flat::TypeKind::Unit), "expand_return!({}).fields(fields!({func})): `{func}` returns nothing — a \ value form returns the struct holding this type's fields", key.as_str(), @@ -1100,7 +1100,7 @@ impl Declarations { ); // The name is the reading's, not a path taken apart to // re-derive one. - let flat::TypeKind::Named { id } = probe.kind() else { + let flat::TypeKind::Named { id, .. } = probe.unwrapped().kind() else { panic!("a sum type is a named type") }; let flat::Type::Variant(sum) = registry diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs index 7bbcbfd4..e3d1c7b8 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/flat_input.rs @@ -173,8 +173,8 @@ pub(crate) fn struct_input_body( // The NAME off the classification, not off the last path segment: // `Box` IS `T` here, and taking the spelling apart would answer // about the wrapper. - if let Some(fqn) = match inner.kind() { - flat::TypeKind::Named { id } => id.ident(), + if let Some(fqn) = match inner.unwrapped().kind() { + flat::TypeKind::Named { id, .. } => id.ident(), _ => None, } .and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n))) @@ -253,8 +253,8 @@ pub(crate) fn struct_input_body( .or_else(|| { // The NAME off the classification, not off the last // path segment. - match inner.kind() { - flat::TypeKind::Named { id } => id.ident(), + match inner.unwrapped().kind() { + flat::TypeKind::Named { id, .. } => id.ident(), _ => None, } .and_then(|name| { @@ -494,8 +494,8 @@ fn read_kotlin_property( // distinction for data-class fields; this is that logic for a property. if ext.is_kotlin_enum_reading(inner) { // The NAME off the classification, not off the last path segment. - let fqn = match inner.kind() { - flat::TypeKind::Named { id } => id.ident(), + let fqn = match inner.unwrapped().kind() { + flat::TypeKind::Named { id, .. } => id.ident(), _ => None, } .and_then(|n| ext.kotlin_fqn(&TypeKey::from_ident(&n))) @@ -562,10 +562,10 @@ fn read_kotlin_property( // class, another sum, a `List`): the slot's descriptor is the // registered Kotlin class and the value decodes through its own // converter — the same delegation the data-class path uses. - let sig = match inner.kind() { + let sig = match inner.unwrapped().kind() { // The NAME off the classification, not off the last path // segment: `Box` IS `T` here. - flat::TypeKind::Named { id } => id.ident(), + flat::TypeKind::Named { id, .. } => id.ident(), _ => None, } .and_then(|name| ext.kotlin_fqn(&TypeKey::from_ident(&name))) @@ -1063,8 +1063,8 @@ fn build_flat_sum_field( // The NAME off the classification, and then the ELEMENT — `enum_item` // hands back only the `syn::ItemEnum`, deliberately, so a consumer that // acts on the Variant/Enum distinction asks `declared_type` (#289). - let ident = match sum_reading.kind() { - flat::TypeKind::Named { id } => id.ident(), + let ident = match sum_reading.unwrapped().kind() { + flat::TypeKind::Named { id, .. } => id.ident(), _ => None, }?; let flat::Type::Variant(sum) = registry.flat().declared_type(&ident)? else { @@ -1322,7 +1322,7 @@ pub(crate) fn build_flat_input_plan( // every caller — see the sibling helper's doc. // The name off the classification, not off the last path segment: `Box` // IS `S` here, and taking the spelling apart would answer about the wrapper. - let flat::TypeKind::Named { id } = inner.kind() else { + let flat::TypeKind::Named { id, .. } = inner.unwrapped().kind() else { return Ok(None); }; let Some(name) = id.ident() else { diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs index 5dbae936..f9c75b50 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/sum_out.rs @@ -180,7 +180,8 @@ pub(crate) fn encode_sum_group( .expect("a sum segment carries its selector leaf"); // The name off the reading — `TypeId` IS the name, so nothing takes a path // apart to re-derive one. - let crate::api::core::flat::TypeKind::Named { id } = tag_leaf.out_ty.kind() else { + let crate::api::core::flat::TypeKind::Named { id, .. } = tag_leaf.out_ty.unwrapped().kind() + else { panic!( "jnigen sum unfold: selector type `{}` is not a named type", tag_leaf.out_ty.key() diff --git a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs index 9c26a7be..93a419a3 100644 --- a/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs +++ b/prebindgen/src/api/lang/jnigen/jni/emit/vec_build.rs @@ -6,7 +6,7 @@ use super::*; // classifier (reached through `use super::*`), and an explicit import would win // over the glob and silently retarget it. use crate::api::{ - core::flat::{self, RefMode, TypeRef}, + core::flat::{self, TypeRef}, lang::jnigen::jni::trait_impl::build_through_erased_wrappers, }; @@ -16,7 +16,7 @@ use crate::api::{ // where a `Box>` answers as `Vec` does instead of failing a // last-path-segment test. Its one non-structural rule — `&mut [T]` is refused, // because mutate-back semantics keep the `input_vec` path — survives as the -// `RefMode::Shared` guard on that match. +// `mutable: false` guard on that match. /// `Some((element_type, by_ref))` when `arg_ty` is a slice/`Vec` input whose /// element is a **flattenable `data_class`** — i.e. it decomposes into the @@ -35,7 +35,8 @@ pub(crate) fn vec_build_elem( ) -> Option<(TypeRef, bool)> { // The run and its element off the MODEL. `&mut [T]` is still refused — // mutate-back semantics keep the `input_vec` path — and that is the one - // fact the layer accessors do not carry, so `RefMode` is read directly. + // fact the layer accessors do not carry, so the borrow's mutability is read + // off the kind directly. // // The conversion follows the SYNTAX, and must: this path builds a Rust-side // `Vec` and hands the source fn a borrow of it (or `mem::take`s it), so @@ -51,8 +52,12 @@ pub(crate) fn vec_build_elem( // sequence — whose spelling is a clean `Vec` — and let the outer `Box` // through unseen. Every layer is checked on the way down, the way // `rebuildable_target` does it. - let (run, by_ref) = match arg.kind() { - flat::TypeKind::Ref { mode, inner } if *mode == RefMode::Shared => (&**inner, true), + let (run, by_ref) = match arg.unwrapped().kind() { + flat::TypeKind::Ref { + mutable: false, + inner, + .. + } => (&**inner, true), _ => (arg, false), }; // A wrapper over the RUN is buildable only on the by-value path, and the diff --git a/prebindgen/src/api/lang/jnigen/jni/selector.rs b/prebindgen/src/api/lang/jnigen/jni/selector.rs index 710f5793..5111ea87 100644 --- a/prebindgen/src/api/lang/jnigen/jni/selector.rs +++ b/prebindgen/src/api/lang/jnigen/jni/selector.rs @@ -44,8 +44,6 @@ impl Declarations { ty: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { - use crate::api::core::flat::RefMode; - // What the type IS comes from `kind`; what generated Rust must SPELL it // comes from here. The converter yields this spelling, so a // `Box>` crossing produces a `Box>` — the shape it @@ -65,13 +63,7 @@ impl Declarations { // `Option>`) before the shallow `Optional`; the shape // that resolves correctly wins. if let Some(target) = inner.borrow_target() { - let mutable = matches!( - inner.kind(), - crate::api::core::flat::TypeKind::Ref { - mode: RefMode::Exclusive, - .. - } - ); + let mutable = inner.is_exclusive_borrow(); if let Some(mut c) = self.input_wrapper_shape( WrapperShape::OptionRef { mutable }, syntax, @@ -114,7 +106,10 @@ impl Declarations { } return None; } - if let crate::api::core::flat::TypeKind::Ref { mode, inner } = ty.kind() { + if let crate::api::core::flat::TypeKind::Ref { mutable, .. } = ty.unwrapped().kind() { + // The target through the accessor: an out-parameter's `MaybeUninit` + // is the slot a `T` goes in, and it is the `T` that converts. + let inner = ty.borrow_target().expect("a borrow"); // `&[T]` shared slice borrow: there is no owned `[T]` to decode, so // reuse the `Vec<_>` shape — decode the Java `List` into an owned // `Vec`; the call site borrows it (`&Vec` deref-coerces to @@ -135,7 +130,7 @@ impl Declarations { // NOT: passing `&Vec` there does not compile. Those fall through to // the plain borrow arm below, which hands the whole spelling on as the // sub, exactly as the old syntactic slice check did. - if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(inner.syntax()) { + if !*mutable && decoded_vec_satisfies(inner.syntax()) { if let Some(elem) = inner.sequence_elem() { let elem_ty = elem.syntax().clone(); // The one place `produced` is NOT the crossing's spelling: @@ -160,7 +155,7 @@ impl Declarations { return None; } } - let mutable = matches!(mode, RefMode::Exclusive); + let mutable = ty.is_exclusive_borrow(); if let Some(mut c) = self.input_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, inner, registry) { @@ -181,8 +176,6 @@ impl Declarations { ty: &crate::api::core::flat::TypeRef, registry: &impl Conversions, ) -> Option> { - use crate::api::core::flat::RefMode; - // What the type IS comes from `kind`; the spelling is what generated // Rust must say. This direction used to be handed only the spelling — // `convert_crossing` fetched the reading and threw it away — so it @@ -226,18 +219,20 @@ impl Declarations { } return None; } - if let crate::api::core::flat::TypeKind::Ref { mode, inner } = ty.kind() { + if let crate::api::core::flat::TypeKind::Ref { mutable, .. } = ty.unwrapped().kind() { + // The target through the accessor, as on the input side. + let inner = ty.borrow_target().expect("a borrow"); // `&[T]` shared slice (a callback argument crossing native→JVM): // build a `List` from the borrowed slice. Dual of the `&[T]` // input branch, and the same split: `kind` says it is a borrow of a // run of values; whether the generated Rust can iterate the borrow // directly is a question about the SPELLING. - if matches!(mode, RefMode::Shared) && decoded_vec_satisfies(inner.syntax()) { + if !*mutable && decoded_vec_satisfies(inner.syntax()) { if let Some(elem) = inner.sequence_elem() { return self.output_slice(elem.syntax(), registry); } } - let mutable = matches!(mode, RefMode::Exclusive); + let mutable = ty.is_exclusive_borrow(); if let Some(mut c) = self.output_wrapper_shape(WrapperShape::Borrow { mutable }, syntax, inner, registry) { @@ -269,8 +264,11 @@ fn fallible_parts( ty: &syn::Type, registry: &impl Conversions, ) -> Option<(syn::Type, syn::Type)> { - use crate::api::core::flat::TypeKind; - if let Some(TypeKind::Fallible { ok, err }) = registry.flat().type_ref(ty).map(|t| t.kind()) { + if let Some((ok, err)) = registry + .flat() + .type_ref(ty) + .and_then(|t| t.fallible_parts()) + { return Some((ok.syntax().clone(), err.syntax().clone())); } crate::api::core::types_util::result_parts(ty) diff --git a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs index 27005bfb..f50769f9 100644 --- a/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs +++ b/prebindgen/src/api/lang/jnigen/jni/struct_plan.rs @@ -355,8 +355,8 @@ pub(crate) fn classify_field( // The NAME off the classification, not off the last // path segment: `Box` IS `T` here, and taking the // spelling apart would answer about the wrapper. - match slot.kind() { - crate::api::core::flat::TypeKind::Named { id } => id.ident(), + match slot.unwrapped().kind() { + crate::api::core::flat::TypeKind::Named { id, .. } => id.ident(), _ => None, } .and_then(|name| { diff --git a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs index cdc3698c..ac473103 100644 --- a/prebindgen/src/api/lang/jnigen/jni/tests/values.rs +++ b/prebindgen/src/api/lang/jnigen/jni/tests/values.rs @@ -1638,11 +1638,12 @@ fn check_array_length_qualification(loc: SourceLocation, module: &str) { /// A borrowed **transparent wrapper** around a sequence is refused, not decoded as /// a bare `Vec`. /// -/// `Box>` and `Cow<'_, [T]>` classify as `TypeKind::Sequence` — correctly: -/// no destination language can tell them from `Vec`, which is exactly why the -/// model folds them together. But the generated glue **is** a destination -/// artifact, and it is the one consumer that can tell: `&Vec` is not -/// `&Box>`, and `TypeRef::origin` exists to carry precisely that. +/// `Box>` and `Cow<'_, [T]>` read as a run of values — correctly: no +/// destination language can tell them from `Vec`, which is why +/// `sequence_elem` answers through the wrapper. But the generated glue **is** a +/// destination artifact, and it is the one consumer that can tell: `&Vec` +/// is not `&Box>`, which is why the wrapper is still standing in +/// `kind` and in `TypeRef::origin` alike. /// /// So the selector asks two questions, and only the first is `kind`'s: that this /// is a run of values makes the `Vec` shortcut a *candidate*, and the **spelling** @@ -1907,9 +1908,10 @@ fn a_transparently_wrapped_option_takes_the_present_value_pair_and_is_rebuilt() /// The transparent-wrapper guard runs **before** the model's layers are /// interpreted, not after. /// -/// An erasure sits *outside* the layer it wraps, so `Box<&Vec>` classifies -/// as `TypeKind::Ref` — the `Box` is gone from `kind` and survives only in the -/// spelling. A guard that reads `kind` first replaces the argument with the +/// A wrapper sits *outside* the layer it wraps, so `Box<&Vec>` **reads** as +/// a borrow — `unwrapped` peels the `Box` to answer, and it is the reading, not +/// the kind, that the layers come off. A guard that takes that reading first +/// and forgets the wrapper replaces the argument with the /// inner sequence reading, whose own spelling is a clean `Vec`, and the /// outer wrapper is never seen: the Vec-build plan is selected, its emitter /// hands the source fn a `&[Foo]` built from the transient Rust-side `Vec`, and diff --git a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs index a6c0dbd8..e68d28be 100644 --- a/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs +++ b/prebindgen/src/api/lang/jnigen/jni/trait_impl.rs @@ -1310,7 +1310,9 @@ impl Declarations { // rides `immediate_edges` rather than this converter's `subs`. // The arguments are `TypeRef`s on the classification, so nothing // is re-extracted from the signature's syntax. - let crate::api::core::flat::TypeKind::Callback { args } = reading.kind() else { + let crate::api::core::flat::TypeKind::Callback { args } = + reading.unwrapped().kind() + else { return None; }; self.dispatch_fn_input(args, built) @@ -2013,7 +2015,10 @@ impl Declarations { // `str` is handled above, separately and deliberately: it is unsized, // so its converter yields an owned `String` the call site borrows — // a different contract, not a different spelling. - if matches!(reading.kind(), crate::api::core::flat::TypeKind::Str) { + if matches!( + reading.unwrapped().kind(), + crate::api::core::flat::TypeKind::Str | crate::api::core::flat::TypeKind::String + ) { let wire: syn::Type = syn::parse_quote!(jni::objects::JString); let body: syn::Expr = syn::parse_quote!({ let s = env.get_string(v).map_err(|e| { @@ -2154,7 +2159,7 @@ impl Declarations { // // Asked of the MODEL: an erasure is transparent, so `Box<&T>` already // classifies as `Ref` and nothing here matches a `syn` variant. - if matches!(reading.kind(), crate::api::core::flat::TypeKind::Ref { .. }) { + if reading.borrow_target().is_some() { return None; } // It has to be a type this binding already crosses; if it is not, the @@ -2229,7 +2234,7 @@ impl Declarations { // Asked of the MODEL, not of `stripped`: an erasure is transparent, so // `Box<&T>` already classifies as `Ref` and `kind` answers this without // anything here matching a `syn` variant. - if matches!(reading.kind(), crate::api::core::flat::TypeKind::Ref { .. }) { + if reading.borrow_target().is_some() { return None; } // It has to be a type this binding already crosses; if it is not, the @@ -2371,7 +2376,10 @@ impl Declarations { // Plain `String` keeps its own earlier arm in `primitive_output`, whose // body this matches exactly; this one is reached for the wrapped // spellings that arm's key cannot name. - if matches!(reading.kind(), crate::api::core::flat::TypeKind::Str) { + if matches!( + reading.unwrapped().kind(), + crate::api::core::flat::TypeKind::Str | crate::api::core::flat::TypeKind::String + ) { let wire: syn::Type = syn::parse_quote!(jni::objects::JString); let body: syn::Expr = syn::parse_quote!({ env.new_string(v.as_str()).map_err(|e| { @@ -2403,7 +2411,10 @@ impl Declarations { // Wire is `()`. Body just returns `v`. No Kotlin name — Unit // returns are dropped from emitted signatures, so metadata stays // empty. - if matches!(reading.kind(), crate::api::core::flat::TypeKind::Unit) { + if matches!( + reading.unwrapped().kind(), + crate::api::core::flat::TypeKind::Unit + ) { let wire: syn::Type = syn::parse_quote!(()); let body: syn::Expr = syn::parse_quote!(v); return Some(ConverterImpl {