prebindgen_registry/unfold/plan.rs
1//! Resolved output-deconstruction plans.
2
3/// Outer shape wrapping the [core decomposition](`UnfoldShape::Base`).
4/// The output-side analog of [`crate::expand::FoldShape`], on the
5/// unified [`Shape`](prebindgen_flat::shape::Shape) layer stack:
6/// * `Base` — run the accessor's records on the value, producing all
7/// [leaves](`UnfoldPlan::leaves`) and invoking the builder once;
8/// * `Optional((), inner)` — `Option<T>`/`Option<&T>` return: `None` ⇒ a null
9/// result (builder skipped), `Some` ⇒ decompose the inner;
10/// * `Iterable(inner)` — `Vec<T>` return: fold the elements through an
11/// accumulator `(acc, …) -> acc`. Each element is delivered either WHOLE (via
12/// its own output converter + projection — see [`UnfoldPlan::element`]) or
13/// DECOMPOSED into per-element leaves (explicit accessors, or a synthesized
14/// `data_class` — see [`UnfoldPlan::fixed_builder`]); inner is `Base`.
15///
16/// The `()` payload is unused here — only the JNI adapter's
17/// `Shape<NullableKind>` carries per-layer data.
18pub use prebindgen_flat::shape::Shape as UnfoldShape;
19
20use super::Delivery;
21
22/// Identity of the deconstructor **declaration** a plan's records came from.
23/// A `run`-signature artifact (e.g. a generated callback interface) is fully
24/// determined by the declaration, so adapters key such artifacts on this —
25/// functions selecting the same declaration share one artifact; differently
26/// declared decompositions of the same type get distinct ones. The first
27/// field is always the target type's canonical [`TypeKey`](crate::registry::TypeKey)
28/// string.
29#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub enum DeconId {
31 /// The type's default (`expand_return!`-declared) deconstructor.
32 Default(String),
33 /// Per-fn inline records (`.expand_return`) — unique to the
34 /// function (second field = the fn ident).
35 PerFn(String, String),
36}
37
38/// The declaration-default decomposition of one deconstructor: its leaf
39/// list resolved ONCE from the declaration's records with **normalized**
40/// inputs (borrowed identity form, no outer shape), so the content is
41/// independent of which functions use the declaration and in what order.
42/// Stored in `Registry::decon_plans`; the single source language adapters
43/// derive declaration-keyed signature artifacts (e.g. generated callback
44/// interfaces) from. Per-function aspects (`by_ref`, shape, delivery) live on
45/// each function's [`UnfoldPlan`], which points here via [`UnfoldPlan::decon`].
46///
47/// Normalization detail: the identity leaf's `out_ty` is always the borrowed
48/// `&Source` form (an owned-return function's own plan carries owned `Source`
49/// instead) — both resolve to the same projection/class, and adapters reading
50/// the spec must tolerate whichever form their type tables resolved.
51#[derive(Clone)]
52pub struct DeconSpec {
53 /// The decomposed type as first encountered. A **reading**, so "compare via
54 /// [`TypeKey`](crate::registry::TypeKey), not syntactically" is
55 /// the type rather than an instruction: `source.key()` is the identity and
56 /// `source.spell()` is what generated Rust says.
57 pub source: prebindgen_flat::flat::TypeRef,
58 /// Flattened leaves in declared record order — names, types, paths,
59 /// nullability all declaration-fixed.
60 pub leaves: Vec<UnfoldLeaf>,
61}
62
63/// One step of a leaf's [`UnfoldLeaf::path`] — how to get from the value
64/// reached so far to the next one.
65///
66/// A step is typed rather than a bare ident because a single path may **mix**
67/// the two: an `expand_return!(T).fields(fields!(t_to_struct))` leaf calls the
68/// value-form accessor, reads a struct field, and may then call that field
69/// type's own accessor — `Call(t_to_struct)`, `Field(key_expr)`,
70/// `Call(keyexpr_as_str)`. [`LeafSource`] still says what *kind* of leaf sits
71/// at the end of the path; the steps say how it is reached.
72///
73/// Each step also records whether it is **optional** — its accessor returns
74/// `Option<…>`, or its field is typed `Option<…>`. A `true` on a step *before*
75/// the last makes it a nullable nesting step: the emitter matches on it and the
76/// `None` arm short-circuits the whole leaf to null. The flag is carried rather
77/// than re-derived so both kinds answer the question the same way and the
78/// emitter needs no type walk (an accessor's `Option` was already peeled where
79/// the step was built).
80#[derive(Clone, PartialEq, Eq, Debug)]
81pub enum PathStep {
82 /// Call a `#[prebindgen]` accessor on the value reached so far:
83 /// `source_module::f(&value)`.
84 Call {
85 ident: syn::Ident,
86 optional: bool,
87 /// Whether the value this call yields (its return with any `Option`
88 /// peeled) is **owned** rather than a borrow — `f(..) -> T` / `-> Option<T>`
89 /// against `-> &T` / `-> Option<&T>`.
90 ///
91 /// Only an OPTIONAL step's payload can reach an emitter as a bare
92 /// binding, so this is only ever consulted there: what a `Some` arm
93 /// binds is the accessor's own value, and everything downstream of it —
94 /// the next step's receiver, the value form's argument — needs to know
95 /// whether it may be moved or has to be borrowed. Recorded here, at the
96 /// one place the signature is in hand, because deriving it later from
97 /// the path alone is not possible.
98 owned: bool,
99 },
100 /// Read a struct field of the value reached so far: `value.f`.
101 Field { ident: syn::Ident, optional: bool },
102}
103
104impl PathStep {
105 /// An accessor call step. `owned` says whether its (`Option`-peeled) return
106 /// is an owned value rather than a borrow — see [`Self::Call::owned`].
107 pub fn call(ident: syn::Ident, optional: bool, owned: bool) -> Self {
108 Self::Call {
109 ident,
110 optional,
111 owned,
112 }
113 }
114
115 /// Whether the value this step yields is owned. A field read composes as
116 /// `&(e).f`, so it is a borrow by construction.
117 pub fn yields_owned(&self) -> bool {
118 matches!(self, Self::Call { owned: true, .. })
119 }
120
121 /// A struct-field read step.
122 pub fn field(ident: syn::Ident, optional: bool) -> Self {
123 Self::Field { ident, optional }
124 }
125
126 /// The step's ident, whichever kind it is.
127 pub fn ident(&self) -> &syn::Ident {
128 match self {
129 Self::Call { ident, .. } | Self::Field { ident, .. } => ident,
130 }
131 }
132
133 /// Whether the step yields an `Option` — a nullable nesting step when it is
134 /// not the last on the path.
135 pub fn is_optional(&self) -> bool {
136 match self {
137 Self::Call { optional, .. } | Self::Field { optional, .. } => *optional,
138 }
139 }
140
141 /// Whether the step is a plain (non-optional) field read — a path made only
142 /// of these renders as `value.a.b`, needing no nesting `match`.
143 pub fn is_plain_field(&self) -> bool {
144 matches!(
145 self,
146 Self::Field {
147 optional: false,
148 ..
149 }
150 )
151 }
152
153 /// Whether the step is a field read, `Option` or not.
154 pub fn is_field(&self) -> bool {
155 matches!(self, Self::Field { .. })
156 }
157}
158
159/// Whether a run of steps can be **moved** out of the value it hangs off:
160/// field reads only, with an `Option` allowed on the last one — a `None` arm
161/// still hands over the whole `Option` by value, while an `Option` in the
162/// middle would have to be unwrapped and so can only be borrowed through.
163///
164/// This is the one place the rule is written: the resolver uses it to decide
165/// whether a leaf OWNS what it reaches (its `out_ty` then being the owned type
166/// rather than a borrow), and the emitters use it to project that place. Two
167/// readings of it would drift, and the disagreement would be a borrow handed to
168/// an owning converter.
169pub fn steps_are_movable(steps: &[PathStep]) -> bool {
170 steps
171 .iter()
172 .enumerate()
173 .all(|(i, s)| s.is_field() && (!s.is_optional() || i + 1 == steps.len()))
174}
175
176/// How a leaf's [`UnfoldLeaf::path`] is reached from the decomposed value.
177#[derive(Clone, PartialEq, Eq, Debug, Default)]
178pub enum LeafSource {
179 /// The path is a chain of `#[prebindgen]` **accessor functions**:
180 /// `source_module::f(&value)`, composing nested accessors. Nesting steps
181 /// that return `Option` make the leaf nullable. This is the form produced
182 /// by `.deconstructor_record*` / `.fun_accessor` declarations.
183 #[default]
184 Accessor,
185 /// The path is a chain of **struct field idents** reached by field access
186 /// and cloned: `value.a.b.clone()`. Produced by the synthesized
187 /// decomposition of a by-value `data_class` (see
188 /// [`ValueDecon`](crate::unfold::ValueDecon)); the value's own
189 /// fields cross as decoupled leaves and the foreign side reassembles the
190 /// object (so no Java object is built on the Rust side).
191 Field,
192 /// The **synthesized selector** of a decomposed sum: an `i32` naming which
193 /// alternative is live. It is not read off the value at all — the emitter
194 /// assigns it per `match` arm — so it has no path. Emitted once, ahead of
195 /// the groups it selects between (see
196 /// [`SumDecon`](crate::unfold::SumDecon)).
197 ///
198 /// Its [`out_ty`](UnfoldLeaf::out_ty) is **the sum**, not the `i32` — it
199 /// carries *which* sum it chooses between, which is how the emitter finds
200 /// the enum to `match`. That type is **registered and not required** (#282):
201 /// it gets a table cell like every other leaf's, but no root, because a sum
202 /// has no whole-value output converter and demanding one would fail
203 /// resolution over a type that never crosses whole. The reading comes from
204 /// the declaration — [`Variant::type_ref`](prebindgen_flat::flat::Variant::type_ref)
205 /// — never from an adapter composing one out of a name.
206 SumTag,
207 /// A payload field of ONE alternative of a decomposed sum, reached through
208 /// a **variant pattern** rather than an accessor chain or a field chain:
209 /// the emitter binds `member` inside `variant`'s `match` arm. The leaf is
210 /// live only when [`UnfoldLeaf::group`] equals the value's tag; in every
211 /// other arm its slot carries the wire default.
212 ///
213 /// This is the selector [`Accessor`](Self::Accessor) and
214 /// [`Field`](Self::Field) deliberately lack — both are deterministic
215 /// products, every record contributing unconditionally.
216 VariantField {
217 /// The variant's ident as declared in the source enum.
218 variant: syn::Ident,
219 /// How the payload field is addressed in the arm's pattern.
220 member: syn::Member,
221 },
222}
223
224/// A resolved output expansion for one function.
225#[derive(Clone)]
226pub struct UnfoldPlan {
227 /// Owned core type the records decompose — the function's return after
228 /// peeling `&` / `Option` / `Vec`.
229 pub source: prebindgen_flat::flat::TypeRef,
230 /// Which deconstructor declaration produced [`Self::leaves`] — the
231 /// identity adapters key signature artifacts on. `None` only for the
232 /// whole-element `Iterable` arm (no declaration involved).
233 pub decon: Option<DeconId>,
234 /// True when the return was `&T` / `Option<&T>`: the identity leaf clones
235 /// the borrow; otherwise it moves the owned value.
236 pub by_ref: bool,
237 /// Outer shape over the core decomposition (`Decompose` for a plain
238 /// `T`/`&T` return).
239 pub shape: UnfoldShape,
240 /// Flattened output leaves, in builder-argument order. Populated for
241 /// `Decompose`/`Optional` (accessor decomposition) and for a **decomposed**
242 /// `Iterable` fold (per-element leaves — explicit-accessor or a synthesized
243 /// `data_class` [`Self::fixed_builder`]); **empty** only for a
244 /// **whole-element** `Iterable`, which delivers each element via
245 /// [`Self::element`].
246 pub leaves: Vec<UnfoldLeaf>,
247 /// For a **whole-element** `Iterable` plan: the owned/ref element type,
248 /// delivered to the fold via its own output converter + projection (not
249 /// decomposed). `None` for `Decompose`/`Optional` and for a **decomposed**
250 /// `Iterable` fold (which uses [`Self::leaves`]).
251 pub element: Option<prebindgen_flat::flat::TypeRef>,
252 /// Callback (`deconstruct_output`) vs return-value (`convert_output`)
253 /// delivery.
254 pub delivery: Delivery,
255 /// For [`Delivery::Return`]: the single leaf's `out_ty` lifted through the
256 /// shape (`Decompose` ⇒ `out_ty`, `Optional` ⇒ `Option<out_ty>`). The
257 /// wrapper returns this value through its ordinary output converter (no
258 /// callback). `None` for [`Delivery::Callback`].
259 pub convert_out_ty: Option<prebindgen_flat::flat::TypeRef>,
260 /// `true` for a synthesized by-value `data_class` decomposition (see
261 /// [`ValueDecon`](crate::unfold::ValueDecon)): the builder/folder
262 /// is a **fixed, hoisted** foreign singleton that reconstructs the concrete
263 /// class (the wrapper takes no caller `build`/`fold` param and is not
264 /// generic over `R`/`A` — it returns the concrete type). `false` for the
265 /// accessor-declared deconstructors, whose builder is caller-supplied.
266 pub fixed_builder: bool,
267 /// Value forms that must be evaluated **once** and bound to a local. Every
268 /// leaf below one reaches off that local — otherwise each field would
269 /// rebuild the whole struct, cloning all of it once per leaf.
270 ///
271 /// A list rather than a single accessor because value forms **compose**: a
272 /// field may splice a child type whose own boundary is derived from *its*
273 /// value form, and that child call is a second hoist nested under the
274 /// first. Ordered outermost-first, so a hoist can be composed from the
275 /// longest already-bound prefix of itself.
276 pub hoists: Vec<Hoist>,
277}
278
279/// One hoisted value form: where it sits, and whether it **consumes** the value
280/// it decomposes.
281#[derive(Clone)]
282pub struct Hoist {
283 /// The path prefix to bind, ending in the value form's
284 /// [`PathStep::Call`] (`DeconRecord::Fields`).
285 pub prefix: Vec<PathStep>,
286 /// `true` when the accessor takes its receiver **by value**
287 /// (`f(v: T) -> TStruct`), so the value is moved in and each field can be
288 /// moved *out* into its leaf instead of cloned — the whole point of a
289 /// consuming value form.
290 ///
291 /// Carried on the hoist rather than on [`PathStep::Call`] because only a
292 /// value-form root can consume: the ordinary accessor-chain steps are
293 /// always borrows.
294 pub consuming: bool,
295}
296
297/// One flattened output leaf of a decomposed return value.
298#[derive(Clone)]
299pub struct UnfoldLeaf {
300 /// The author-supplied leaf name, used **literally** (no casing / stripping /
301 /// keyword escaping). Nested records prefix the child's name with their own
302 /// name, joined by the reserved `"__"` separator (`"sample"` splicing
303 /// `"keyExpr"` → `"sample__keyExpr"`); a root identity leaf is `"handle"`.
304 /// Names are unique within a deconstructor (a duplicate is a hard error).
305 pub name: String,
306 /// Reach chain from the root value (`[]` = the identity/root itself;
307 /// `[Call(f)]` = `f(&root)`; longer = nested records, M3). Steps of both
308 /// kinds may mix — see [`PathStep`].
309 pub path: Vec<PathStep>,
310 /// The **reading** of the type whose resolved output converter encodes this
311 /// leaf — a reference type for accessors (`&str`, `&F`), `&Source` for the
312 /// identity leaf (so the borrowed-opaque clone converter / projection is
313 /// reused). Spell it with `out_ty.spell()`.
314 ///
315 /// A reading rather than a spelling because a consumer asking what this
316 /// leaf's type *means* had to hand the spelling back to the registry and
317 /// hope for a cell — the round trip #263 removed from `api/core`, surviving
318 /// in the plans, and answering "no layer" for a type it had never seen
319 /// (#275). The composed ones (`&Source`) are built by
320 /// [`TypeRef::borrowed`](prebindgen_flat::flat::TypeRef::borrowed), which
321 /// pairs the kind with its own spelling.
322 pub out_ty: prebindgen_flat::flat::TypeRef,
323 /// `true` for the move/clone-the-value handle leaf, emitted **last** (after
324 /// every reference leaf's JVM conversion has ended its borrow).
325 pub identity: bool,
326 /// `true` when a nesting accessor on [`Self::path`] returns `Option` (M3):
327 /// the reached value may be absent, so the leaf is nullable on the
328 /// destination side (e.g. a Kotlin `?` type); emit wraps the encode in a
329 /// `match Some/None`.
330 pub nullable: bool,
331 /// How [`Self::path`] is reached from the value — an accessor-fn chain
332 /// (default), a struct-field chain (synthesized `data_class`), or a
333 /// variant pattern binding (decomposed sum).
334 pub source: LeafSource,
335 /// **Group membership**: `Some(tag)` marks the leaf as belonging to the
336 /// leaf group of the sum alternative with that tag — live only when the
337 /// value's [`LeafSource::SumTag`] leaf equals `tag`, wire-defaulted
338 /// otherwise. `None` for an unconditional (product) leaf, including the
339 /// tag leaf itself, which selects between groups rather than joining one.
340 ///
341 /// Grouping is what turns a leaf list into a `match`: leaves sharing a
342 /// group are emitted together in one arm instead of as independent
343 /// per-leaf expressions.
344 pub group: Option<i32>,
345}
346
347impl UnfoldLeaf {
348 /// Whether this leaf's [`out_ty`](Self::out_ty) needs a resolved **output
349 /// converter**. False only for the synthesized [`LeafSource::SumTag`]
350 /// selector: it is assigned per `match` arm, never converted, so requiring
351 /// a converter for it would make every sum depend on an unrelated `i32`
352 /// crossing existing in the binding.
353 ///
354 /// **This is the root question, not the registration question.** Every
355 /// leaf's `out_ty` gets a table cell; this decides which of them the
356 /// binding additionally *demands* a converter for. A cell says the type
357 /// entered the pipeline, a root says the binding asked for it directly, and
358 /// an entry says one resolved — three separate claims, and a `SumTag` leaf
359 /// makes only the first (#282).
360 pub fn has_converter(&self) -> bool {
361 self.source != LeafSource::SumTag
362 }
363}