Skip to main content

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}