Skip to main content

prebindgen_registry/
unfold.rs

1//! Output (data) expansion — the dual of constructor expansion
2//! (`api/core/expand.rs`). A function returning a rich type is *decomposed* by a
3//! **deconstructor** into a set of leaf values.
4//!
5//! A **deconstructor** (a type-level `expand_return!` `.field*` list,
6//! or the per-fn `.expand_return` override) is a
7//! **deterministic product**: every record always runs and contributes its leaf
8//! — there is no selector (unlike a *constructor*, whose selector picks one
9//! variant). A record's accessor is a `#[prebindgen]` function `f(&T) -> &F` (a
10//! reference return where possible, for zero-copy); an accessor whose return
11//! type has its own deconstructor splices the child's records with prefixed
12//! leaf names.
13//!
14//! Two **deliveries** (see [`Delivery`]), derived from the resolved leaf count:
15//! * `Callback` — replaces the return with a foreign **callback** receiving
16//!   all the leaves (any leaf count).
17//! * `Return` — **returns** the single leaf value directly (no callback);
18//!   requires a single-leaf decomposition.
19//!
20//! Resolution is language-agnostic: it turns the declarations into
21//! [`UnfoldPlan`]s (stored on the registry, keyed by function ident) and
22//! registers every leaf's `out_ty` as a required **output** so the resolver
23//! produces its converter (and projection). The jnigen adapter reads the
24//! plan at the return-emission site.
25//!
26//! [`Iterable`]: UnfoldShape::Iterable
27
28use std::collections::HashSet;
29
30use crate::{
31    declared_target::check_declared_target,
32    registry::{Registry, TypeKey},
33};
34
35mod error;
36mod plan;
37
38pub use self::{
39    error::{UnfoldDeclError, UnfoldError},
40    plan::{
41        steps_are_movable, DeconId, DeconSpec, Hoist, LeafSource, PathStep, UnfoldLeaf, UnfoldPlan,
42        UnfoldShape,
43    },
44};
45
46// ──────────────────────────────────────────────────────────────────────
47// Declarations (populated by the language builder)
48// ──────────────────────────────────────────────────────────────────────
49
50/// One record (field) of a deconstructor. A deconstructor is a product: every
51/// record contributes a leaf.
52// large_enum_variant: a handful of records exist per binding — boxing the
53// syn payloads would only complicate the arms (same trade-off as
54// `ConvertSourceKind`).
55#[allow(clippy::large_enum_variant)]
56#[derive(Clone)]
57pub enum DeconRecord {
58    /// Read this field by calling the accessor function `f(&T) -> &F`. `name`
59    /// is the author-supplied leaf name, used **literally** (no casing /
60    /// stripping); it may not contain the reserved `"__"` chain separator.
61    /// An accessor whose return type has its own deconstructor splices that
62    /// child's records with the leaf names prefixed `name__<child>`.
63    Acc { func: syn::Ident, name: String },
64    /// Read this field by calling a **custom, locally-defined** accessor: any
65    /// callable in the binding crate (`path`) with a STATED return type
66    /// (`ty`) — there is no `#[prebindgen]` item behind it, so the signature
67    /// cannot be looked up. The adapter's `local_functions()` pre-pass
68    /// synthesizes a registry entry from the stated signature (so call
69    /// qualification and `Option`-nesting checks work unchanged); splicing
70    /// follows [`Self::Acc`] rules except that a self-referential field (its
71    /// type already being decomposed) degrades to a plain converter leaf
72    /// instead of a cycle error — that is what lets such a field re-deliver
73    /// (part of) the value itself, e.g. under a binding-defined condition.
74    LocalAcc { path: syn::Path, name: String },
75    /// The value itself — the handle/identity leaf (cloned for a `&T` return,
76    /// moved for an owned `T`). At most one per
77    /// deconstructor.
78    Identity,
79    /// Read the fields of the type's **value form**: call `func` once
80    /// (`f(&T) -> TStruct`) and contribute one record per [`FieldRecord`],
81    /// reached by field access on the returned struct. The language adapter
82    /// builds the field list (it knows which structs are declared classes and
83    /// therefore inline); this record only says how to get there.
84    ///
85    /// Each field then decomposes exactly like an [`Acc`](Self::Acc) record's
86    /// return does — its own `records` if the
87    /// declaration overrode it, else its type's own deconstructor if it has
88    /// one, else one leaf — so a value form and a hand-written field list
89    /// produce the same leaves.
90    Fields {
91        func: syn::Ident,
92        /// The accessor **consumes** its receiver (`f(T) -> TStruct`): the
93        /// value is moved in and each field moved *out* into its leaf, instead
94        /// of being cloned out of a borrow. Declared by the adapter rather than
95        /// read off the signature — giving the value away is a boundary
96        /// decision — and cross-checked against the signature when the records
97        /// are flattened, so the two cannot drift.
98        consuming: bool,
99        fields: Vec<FieldRecord>,
100    },
101}
102
103/// One field of a value form (see [`DeconRecord::Fields`]).
104#[derive(Clone)]
105pub struct FieldRecord {
106    /// Field-access chain from the value form's returned struct. More than one
107    /// element when the adapter inlined a nested declared class.
108    pub members: Vec<syn::Ident>,
109    /// The leaf name (already `__`-joined across inlined nesting).
110    pub name: String,
111    /// The field's **reading**, `Option` / `Vec` layers included — and its
112    /// syntax with it, which is what a leaf's `out_ty` spells.
113    ///
114    /// The declaration carries this rather than naming a `syn::Type` for the
115    /// walk to look up, because there was nothing to look up: a field record is
116    /// built from an element whose every field already has a reading, and the
117    /// types it names are the ones the caller registers *after* the walk
118    /// returns. Asking the registry here was asking before registration
119    /// (#266) — a lookup that could only miss, answered by a second source of
120    /// readings that hid the ordering.
121    pub ty: prebindgen_flat::flat::TypeRef,
122    /// How this field decomposes.
123    pub decon: FieldDecon,
124}
125
126/// How one [`FieldRecord`] decomposes.
127#[derive(Clone)]
128pub enum FieldDecon {
129    /// By the field type's own deconstructor if it has one, else one leaf —
130    /// the same default a [`DeconRecord::Acc`] record's return follows.
131    Default,
132    /// Explicit records, replacing the type default wholesale (the declaration
133    /// stated this field's complete leaf set).
134    Records(Vec<DeconRecord>),
135    /// Leaves the **adapter** built, appended with this field's path and name
136    /// prefixed onto each. For shapes whose leaf structure only the adapter
137    /// knows — a decomposed sum, which is a selector plus one group per
138    /// alternative rather than a product of records.
139    Leaves(Vec<UnfoldLeaf>),
140}
141
142impl DeconRecord {
143    /// The fn ident of a [`Self::LocalAcc`] — its path's last segment (the
144    /// name the emitted call resolves to under the path-prefix origin).
145    fn local_ident(path: &syn::Path) -> syn::Ident {
146        path.segments
147            .last()
148            .expect("field!(...).with(...): empty accessor path")
149            .ident
150            .clone()
151    }
152}
153
154/// A type-level deconstructor declaration (`expand_return!(T).field*`): the
155/// complete, ordered record list decomposing `target`. An immutable record —
156/// the leaf order is the declaration order of the `records` vector.
157#[derive(Clone)]
158pub struct DeconstructorDecl {
159    /// The type being decomposed, as an **identity** — see
160    /// [`ConstructorDecl::target`](crate::expand::ConstructorDecl::target).
161    pub target: TypeKey,
162    pub records: Vec<DeconRecord>,
163    /// Auto-apply this deconstructor to every matching declared fn (`Some`
164    /// carries the inferred `(target-position, delivery)` to use). Always
165    /// `Some` for type-level default (`expand_return!`) declarations.
166    pub default: Option<(DeconTarget, Delivery)>,
167}
168
169/// How an output expansion chooses the deconstructor for a function's return
170/// type: the type's default (`expand_return!`-declared) or a per-fn
171/// inline record list (`.expand_return`).
172#[derive(Clone)]
173pub enum DeconSel {
174    /// Use the return type's unique deconstructor (error if ambiguous).
175    TopLevel,
176    /// Per-fn override (`.expand_return`): use exactly these
177    /// accessor-fn records.
178    Inline(Vec<DeconRecord>),
179}
180
181/// Which value of a function the deconstructor decomposes: its success return
182/// (`Output`) or its `Result<_, E>` domain error (`Error`).
183#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
184pub enum DeconTarget {
185    Output,
186    Error,
187}
188
189/// How the decomposed value(s) are delivered to the foreign side. Derived
190/// from the resolved leaf count (1 ⇒ `Return`, N ⇒ `Callback`); errors are
191/// always `Callback`-shaped.
192#[derive(Clone, Copy, PartialEq, Eq, Debug)]
193pub enum Delivery {
194    /// Deliver the leaves to a foreign **callback** (builder / fold). Any
195    /// leaf count.
196    Callback,
197    /// **Return**/deliver the single decomposed value (no builder). Requires
198    /// exactly one leaf and a non-`Iterable` shape.
199    Return,
200}
201
202/// A per-fn output expansion (`.expand_return(expand_return!(T)…)`) —
203/// decompose `func`'s return (or error position) via the record list.
204/// Recorded as an explicit decl so the auto-`default` skips it; an
205/// identity-only record list lowers to the raw whole-value return at
206/// resolution.
207#[derive(Clone)]
208pub struct OutputDecl {
209    pub func: syn::Ident,
210    pub sel: DeconSel,
211    pub target: DeconTarget,
212    pub delivery: Delivery,
213    /// The type the per-fn decl was declared for (`expand_return!(T)`) —
214    /// cross-checked against the fn's peeled return type at resolution.
215    /// `None` for internally-synthesized decls (the type comes from the
216    /// return itself).
217    pub declared_source: Option<TypeKey>,
218}
219
220/// Deconstructor / output-expansion declarations gathered from a language
221/// builder — an immutable record set: complete values, no build protocol.
222/// Declaration order is the vector order; leaf order is each record
223/// vector's order. Handed to the registry as
224/// [`Decompositions::deconstructors`](crate::Decompositions::deconstructors);
225/// empty or duplicate declarations are diagnosed at resolution (collected),
226/// not at construction.
227#[derive(Clone, Default)]
228pub struct Deconstructors {
229    pub deconstructors: Vec<DeconstructorDecl>,
230    pub outputs: Vec<OutputDecl>,
231    /// Identity-only per-fn field-set opt-outs: fns excluded from the
232    /// default auto-apply (the raw whole-value return).
233    pub skip_output: std::collections::HashSet<syn::Ident>,
234}
235
236// ──────────────────────────────────────────────────────────────────────
237// apply
238// ──────────────────────────────────────────────────────────────────────
239
240/// Structural validation of the declaration records — duplicate targets,
241/// collected (EVERY offender before failing) so a build surfaces all
242/// declaration problems at once. Empty record lists are NOT diagnosed:
243/// an empty inline list is the valid whole-element delivery form.
244fn validate_declarations(acc: &Deconstructors) -> Result<(), UnfoldError> {
245    let mut entries: Vec<UnfoldDeclError> = Vec::new();
246    let mut decon_targets: std::collections::HashSet<String> = std::collections::HashSet::new();
247    for d in &acc.deconstructors {
248        let target = d.target.as_str().to_string();
249        if !decon_targets.insert(target.clone()) {
250            entries.push(UnfoldDeclError::DuplicateDeconstructor { target });
251        }
252    }
253    let mut output_keys: std::collections::HashSet<(String, DeconTarget)> =
254        std::collections::HashSet::new();
255    for od in &acc.outputs {
256        if !output_keys.insert((od.func.to_string(), od.target)) {
257            entries.push(UnfoldDeclError::DuplicateOutput {
258                func: od.func.clone(),
259                target: od.target,
260            });
261        }
262    }
263    if entries.is_empty() {
264        Ok(())
265    } else {
266        Err(UnfoldError::InvalidDeclarations { entries })
267    }
268}
269
270/// Resolve every output-expansion declaration (explicit + `.default()`
271/// auto-applied) into an [`UnfoldPlan`], register each leaf's `out_ty` as a
272/// required output, and store the plans on the registry (`unfold_plans` for
273/// `Output`, `error_plans` for `Error`).
274///
275/// `declared_fns` is the adapter's claimed `#[prebindgen]` fn set — the domain
276/// over which `.default()` deconstructors are auto-applied. `accessor_fns` is
277/// the `.fun_accessor` subset — the only functions a decomposer record may
278/// reference.
279///
280/// Runs inside `write_rust` after `expand::apply` and before `resolve`, so leaf
281/// converters resolve through the normal rank machinery.
282pub(crate) fn apply<M>(
283    registry: &mut Registry<M>,
284    acc: &Deconstructors,
285    declared_fns: &std::collections::HashSet<syn::Ident>,
286    accessor_fns: &std::collections::HashSet<syn::Ident>,
287) -> Result<(), UnfoldError> {
288    validate_declarations(acc)?;
289    // Binding-local accessors (`LocalAcc` records) resolve through registry
290    // entries synthesized by the adapter's `local_functions()` pre-pass in
291    // the builder's scan — by this point they read exactly like
292    // `#[prebindgen]` accessors.
293
294    // Gate: every accessor-function record of every declared deconstructor must
295    // be a `.fun_accessor` (the single source of truth for "accessor").
296    // Binding-local records skip the gate — there is no `#[prebindgen]` item
297    // behind them — but keep the reserved-separator name check.
298    for d in &acc.deconstructors {
299        check_records(&d.records, accessor_fns)?;
300    }
301
302    // Explicit decls first; they take precedence over (and suppress) a default
303    // for the same `(fn, target)`.
304    let mut done: std::collections::HashSet<(syn::Ident, DeconTarget)> = Default::default();
305    for ed in &acc.outputs {
306        // Per-fn decl cross-check: the decl's declared type must match the
307        // fn's peeled (`Option`/`Vec`/`&`) return type — the typo guard for
308        // `.expand_return(expand_return!(T)…)`.
309        if let Some(declared) = &ed.declared_source {
310            let ret = registry
311                .flat()
312                .function(&ed.func)
313                .map(|f| f.ret.clone())
314                .ok_or_else(|| UnfoldError::UnknownFunction(ed.func.clone()))?;
315            if !returns_type(&ret, declared) {
316                return Err(UnfoldError::ReturnTypeMismatch {
317                    func: ed.func.clone(),
318                    declared: declared.as_str().to_string(),
319                    actual: {
320                        let s = ret.spell();
321                        quote::quote!(#s).to_string()
322                    },
323                });
324            }
325        }
326        // Identity-only field set = the raw whole-value return (the
327        // complete-set rule: "the set is {self}"): no plan — mark done so the
328        // type default doesn't re-apply, and let the return cross through the
329        // type's ordinary output converter (borrowed-`&T`-capable).
330        if let DeconSel::Inline(records) = &ed.sel {
331            if matches!(records.as_slice(), [DeconRecord::Identity]) {
332                done.insert((ed.func.clone(), ed.target));
333                continue;
334            }
335        }
336        process_decl(registry, acc, ed)?;
337        done.insert((ed.func.clone(), ed.target));
338    }
339
340    // Default auto-apply: a type's deconstructor (`expand_return!`) is
341    // applied to every declared fn that returns it (Output) or has it as a
342    // `Result<_, E>` error (Error), unless the fn is `fun_accessor` or has a
343    // per-fn override. `Delivery` is recomputed from leaf count inside
344    // `process_decl` for Output (1 ⇒ Return, N ⇒ Callback).
345    for d in &acc.deconstructors {
346        if d.default.is_none() {
347            continue;
348        }
349        let dkey = d.target.clone();
350        let sel = DeconSel::TopLevel;
351        for func in declared_fns {
352            // Read accessors are never output-decomposed (they ARE the records).
353            if accessor_fns.contains(func) {
354                continue;
355            }
356            let Some(ret) = registry.flat().function(&func).map(|f| f.ret.clone()) else {
357                continue;
358            };
359            // Error position: fn returns `Result<_, E>` and `E == d.target`.
360            if let Some(err_ty) = ret.fallible_parts().map(|(_, e)| e) {
361                if err_ty.key() == dkey && done.insert((func.clone(), DeconTarget::Error)) {
362                    process_decl(
363                        registry,
364                        acc,
365                        &OutputDecl {
366                            func: func.clone(),
367                            sel: sel.clone(),
368                            target: DeconTarget::Error,
369                            delivery: Delivery::Callback,
370                            declared_source: None,
371                        },
372                    )?;
373                }
374            }
375            // Output position: fn returns `T` / `&T` / `Option<T|&T>` / `Vec<T>`
376            // with `T == d.target` (Result returns keep a handle — factories).
377            if returns_type(&ret, &dkey)
378                && !acc.skip_output.contains(func)
379                && done.insert((func.clone(), DeconTarget::Output))
380            {
381                process_decl(
382                    registry,
383                    acc,
384                    &OutputDecl {
385                        func: func.clone(),
386                        sel: sel.clone(),
387                        target: DeconTarget::Output,
388                        delivery: Delivery::Callback,
389                        declared_source: None,
390                    },
391                )?;
392            }
393        }
394    }
395
396    // Callback-argument decomposition: each `T` of a declared fn's
397    // `impl Fn(T, …)` parameter is delivered per `T`'s default deconstructor —
398    // the same default output a *return* of `T` would use — so the foreign
399    // callback receives the flattened leaves in one crossing instead of a
400    // whole value. Plans are type-level (keyed by `T`, fn-independent) with
401    // `by_ref = false` (the trampoline owns the value, so a root identity
402    // record moves it). Delivery is always `Callback` regardless of leaf count
403    // (there is no return-value lane in a callback invocation). A type without
404    // a default deconstructor gets no plan and is delivered whole.
405    for func in declared_fns {
406        let Some(params) = registry.flat().function(&func).map(|f| f.params.clone()) else {
407            continue;
408        };
409        for param in &params {
410            // The callback's argument types, read off the parameter's
411            // classification. `TypeKind::Callback` carries them as `TypeRef`s, so
412            // there is nothing to re-extract from the signature's syntax.
413            let prebindgen_flat::flat::TypeKind::Callback { args } = param.ty.kind() else {
414                continue;
415            };
416            for arg_ty in args {
417                // A borrowed arg (`impl Fn(&T)`) decomposes through the same
418                // machinery as a `&T` return: strip the leading `&` to reach the
419                // deconstructor target and set `by_ref` so the leaves are read
420                // (cloned) through the reference instead of by move. The plan is
421                // keyed under the ACTUAL arg type (`&T`) — that is what
422                // `callback_input`/`callback_iface_spec` look up.
423                let (by_ref, core_ty) = peel_borrow(arg_ty);
424                // Only a NAMED core can match a deconstructor target: an
425                // `Option<T>` / `Vec<T>` / tuple arg is delivered whole. The model
426                // says which, and `unwrapped` is where a wrapper the destination
427                // cannot see — `Box<T>` — stops reading as un-nameable.
428                if !matches!(
429                    core_ty.unwrapped().kind(),
430                    prebindgen_flat::flat::TypeKind::Named { .. }
431                ) {
432                    continue;
433                }
434                let key = arg_ty.key();
435                if registry.callback_arg_plans.contains_key(&key) {
436                    continue;
437                }
438                let core_key = core_ty.key();
439                let Some(d) = acc
440                    .deconstructors
441                    .iter()
442                    .find(|d| d.default.is_some() && d.target == core_key)
443                else {
444                    continue;
445                };
446                let ed = OutputDecl {
447                    func: func.clone(),
448                    sel: DeconSel::TopLevel,
449                    target: DeconTarget::Output,
450                    delivery: Delivery::Callback,
451                    declared_source: None,
452                };
453                let decon = decl_id(&core_key, d);
454                let records = d.records.clone();
455                register_decon_spec(registry, acc, &decon, &records, core_ty)?;
456                let plan = build_plan(
457                    acc,
458                    registry,
459                    &ed,
460                    by_ref,
461                    core_ty,
462                    UnfoldShape::Base,
463                    &records,
464                    decon,
465                )?;
466                if plan.leaves.is_empty() {
467                    continue;
468                }
469                for leaf in &plan.leaves {
470                    registry.require_output(&leaf.out_ty);
471                }
472                registry.callback_arg_plans.insert(key, plan);
473            }
474        }
475    }
476    Ok(())
477}
478
479/// A synthesized by-value `data_class` decomposition, produced by the language
480/// adapter (which knows the per-field encoding — projections, enums, nested
481/// classes) and handed over as
482/// [`Decompositions::value_structs`](crate::Decompositions::value_structs).
483/// Its [`leaves`](Self::leaves)
484/// are [`LeafSource::Field`] leaves: each crosses the boundary as its own field
485/// value and the foreign side reassembles the object (no Java object is built
486/// on the Rust side).
487pub struct ValueDecon {
488    /// Canonical key of the value struct (the `DeconId::Default` key).
489    pub key: TypeKey,
490    /// The struct type (owned) the leaves decompose.
491    pub source: prebindgen_flat::flat::TypeRef,
492    /// Field-access leaves in foreign-signature / `fromParts` order.
493    pub leaves: Vec<UnfoldLeaf>,
494}
495
496/// Wire the synthesized by-value `data_class` decompositions into the registry:
497/// register each as a `DeconId::Default` [`DeconSpec`], then build a
498/// **fixed-builder** [`UnfoldPlan`] for every declared function that returns the
499/// struct (`T` / `&T` / `Option<T>` / `Vec<T>`) and a callback-arg plan for
500/// every `impl Fn(&T)` / `impl Fn(T)` parameter. Each leaf's `out_ty` is
501/// registered as a required output. Mirrors the per-function matching of
502/// [`apply`], but the builder/folder is a fixed foreign singleton
503/// (`fixed_builder = true`) reconstructing the concrete class, so delivery is
504/// always `Callback` (never the single-leaf `Return` shortcut) and the wrapper
505/// stays non-generic.
506///
507/// Runs in `write_rust` right after [`apply`] and before `resolve`.
508pub(crate) fn apply_value_structs<M>(
509    registry: &mut Registry<M>,
510    decons: Vec<ValueDecon>,
511    declared_fns: &std::collections::HashSet<syn::Ident>,
512) -> Result<(), UnfoldError> {
513    for vd in &decons {
514        let decon = wire_fixed_decon(registry, &vd.key, &vd.source, &vd.leaves)?;
515
516        // Output position: a declared fn returning the struct
517        // (`T` / `&T` / `Option<T|&T>` / `Vec<T|&T>`) decomposes into a
518        // fixed-builder plan. (`Result<T, E>` is left to the whole-value
519        // converter — the synthesizer covers the infallible returns.)
520        wire_fixed_returns(registry, vd, &decon, declared_fns, false);
521
522        // Callback-argument position: an `impl Fn(&T)` / `impl Fn(T)` parameter
523        // of a declared fn delivers the flattened leaves to the foreign
524        // callback, which reassembles the whole value via the data class's
525        // `fromParts` before invoking the user's typed callback (the group
526        // reassembly lives in the JNI adapter's `asRaw` proxy).
527        wire_fixed_callbacks(registry, vd, &decon, declared_fns)?;
528    }
529    Ok(())
530}
531
532/// A synthesized **sum** decomposition, produced by the language adapter (which
533/// knows how each payload encodes) and handed over as
534/// [`Decompositions::sums`](crate::Decompositions::sums) — the
535/// selector-carrying sibling of [`ValueDecon`].
536///
537/// Its [`leaves`](Self::leaves) are a [`LeafSource::SumTag`] selector followed
538/// by one **group** per alternative ([`LeafSource::VariantField`] leaves
539/// carrying [`UnfoldLeaf::group`]). Exactly one group is live per value; the
540/// emitter reads the whole list as ONE `match` over the value, filling every
541/// inert slot with its wire default. The foreign side picks the live group by
542/// the tag and rebuilds the alternative, so no object is built on the Rust
543/// side.
544pub struct SumDecon {
545    /// Canonical key of the sum type (the `DeconId::Default` key).
546    pub key: TypeKey,
547    /// The enum type (owned) the leaves decompose.
548    pub source: prebindgen_flat::flat::TypeRef,
549    /// The tag leaf followed by every variant's group, in tag order.
550    pub leaves: Vec<UnfoldLeaf>,
551}
552
553/// Wire the synthesized **sum** decompositions into the registry — the
554/// [`apply_value_structs`] analog for a value whose alternatives are chosen at
555/// runtime instead of being a fixed product.
556///
557/// For every declared function returning the sum (`E` / `&E` / `Option<E>` /
558/// `Vec<E>`) and every `impl Fn(E)` / `impl Fn(&E)` callback parameter, builds a
559/// **fixed-builder** [`UnfoldPlan`] over the tag + group leaves, registering
560/// each leaf's `out_ty` as a required output.
561///
562/// A sum has no converter of its own (it is boundary-only: a tag plus groups is
563/// not a single wire), so the declared return's scan-time output requirement —
564/// including the `Option<E>` / `Vec<E>` layers, which the boundary-only pass
565/// does not reach — is dropped here as the plan takes over.
566///
567/// Put every leaf's `out_ty` in the table, and demand a converter for the ones
568/// that need one.
569///
570/// **Every leaf is registered; only a converter-bearing leaf is a root** (#282).
571/// The two are separate facts and this is the one place a sum plan states both:
572/// a cell says the type entered the pipeline, a root says the binding needs its
573/// conversion to resolve. The `SumTag` selector is registered and not required —
574/// it names *which* sum it chooses between, and a sum has no whole-value output
575/// converter, so requiring one would fail resolution over a type that never
576/// crosses whole.
577///
578/// This used to `filter` the selector out entirely, which left its `out_ty`
579/// with a cell only when the adapter happened to declare the sum separately —
580/// true for jnigen via `export_type`, and not true at all for a registry
581/// assembled without declarations. The invariant holds by construction now
582/// rather than by declaration order.
583fn register_leaves<M>(registry: &mut crate::registry::Registry<M>, leaves: &[UnfoldLeaf]) {
584    for leaf in leaves {
585        if leaf.has_converter() {
586            registry.require_output(&leaf.out_ty);
587        } else {
588            registry.reference_output(&leaf.out_ty);
589        }
590    }
591}
592
593/// Runs in `write_rust` right after [`apply_value_structs`] and before `resolve`.
594pub(crate) fn apply_sum_returns<M>(
595    registry: &mut Registry<M>,
596    decons: Vec<SumDecon>,
597    declared_fns: &std::collections::HashSet<syn::Ident>,
598) -> Result<(), UnfoldError> {
599    for sd in &decons {
600        let decon = wire_fixed_decon(registry, &sd.key, &sd.source, &sd.leaves)?;
601        let vd = ValueDecon {
602            key: sd.key.clone(),
603            source: sd.source.clone(),
604            leaves: sd.leaves.clone(),
605        };
606        wire_fixed_returns(registry, &vd, &decon, declared_fns, true);
607        wire_fixed_callbacks(registry, &vd, &decon, declared_fns)?;
608    }
609    Ok(())
610}
611
612/// Register the declaration-canonical [`DeconSpec`] of a synthesized
613/// decomposition (first writer wins) and return its identity.
614fn wire_fixed_decon<M>(
615    registry: &mut Registry<M>,
616    key: &TypeKey,
617    source: &prebindgen_flat::flat::TypeRef,
618    leaves: &[UnfoldLeaf],
619) -> Result<DeconId, UnfoldError> {
620    let decon = DeconId::Default(key.to_string());
621    require_unique_leaf_names(source, leaves)?;
622    registry
623        .decon_plans
624        .entry(decon.clone())
625        .or_insert_with(|| DeconSpec {
626            source: source.clone(),
627            leaves: leaves.to_vec(),
628        });
629    Ok(decon)
630}
631
632/// Build the fixed-builder output plan for every declared fn returning the
633/// decomposed type (`T` / `&T` / `Option<T|&T>` / `Vec<T|&T>`). `no_converter`
634/// marks a type that has no whole-value converter at all (a sum), so the
635/// declared return's scan-time output requirement is dropped as the plan
636/// replaces it.
637fn wire_fixed_returns<M>(
638    registry: &mut Registry<M>,
639    vd: &ValueDecon,
640    decon: &DeconId,
641    declared_fns: &std::collections::HashSet<syn::Ident>,
642    no_converter: bool,
643) {
644    for func in declared_fns {
645        let Some(ret) = registry.flat().function(&func).map(|f| f.ret.clone()) else {
646            continue;
647        };
648        if !returns_type(&ret, &vd.key) || registry.unfold_plans.contains_key(func) {
649            continue;
650        }
651        // Shape over the leaf decomposition: peel an outer `Option`, then a
652        // `Vec`, then a leading `&`. `Vec<T|&T>` ⇒ Iterable (a **fixed
653        // folder**: each element's leaves cross raw and the foreign folder
654        // rebuilds it + appends, so no Java object is built on the Rust
655        // side); `Option<…>` wraps the inner shape in Optional (`None` ⇒ a
656        // null result). `element: None` keeps the decomposed-leaf path. The
657        // element/inner borrow-ness sets `by_ref` (the reach clones either
658        // way).
659        let layers = peel(&ret);
660        let by_ref = layers.by_ref;
661        // The model's layer stack is the plan's shape — `UnfoldShape` is `Shape`
662        // — so there is nothing to rebuild here.
663        let shape = layers.shape.clone();
664        if no_converter {
665            // The plan delivers the return leaf-by-leaf, so no converter is
666            // needed for the declared return — and for a sum none can exist.
667            // Drop the scan-time registrations of every layer (the boundary-only
668            // pass only reaches the bare type), so the missing converters are not
669            // flagged as unresolved-required.
670            // EVERY layer, the `Vec` element included. The shape fold peels here,
671            // so the matching unrequire belongs here; leaving the element out made
672            // the invariant depend on the adapter's `boundary_only_types` covering
673            // it — true for JniGenBuilder today, and the only reason a
674            // `Vec<sum>`-only declaration resolves.
675            for layer in &layers.layer_types {
676                registry.unrequire_output(layer);
677            }
678        }
679        register_leaves(registry, &vd.leaves);
680        let plan = UnfoldPlan {
681            source: vd.source.clone(),
682            decon: Some(decon.clone()),
683            by_ref,
684            shape,
685            leaves: vd.leaves.clone(),
686            element: None,
687            delivery: Delivery::Callback,
688            convert_out_ty: None,
689            fixed_builder: true,
690            hoists: Vec::new(),
691        };
692        registry.unfold_plans.insert(func.clone(), plan);
693    }
694}
695
696/// Build a fixed-builder callback-arg plan for every `impl Fn(&T)` /
697/// `impl Fn(T)` parameter (of a declared fn) whose value is the decomposed type
698/// `vd`. The foreign callback receives the flattened leaves (reassembled there)
699/// instead of a whole value built on the Rust side. Separate from the
700/// output-position wiring so the callback path (which needs the foreign-side
701/// group-reassembly adapter) can be enabled on its own.
702fn wire_fixed_callbacks<M>(
703    registry: &mut Registry<M>,
704    vd: &ValueDecon,
705    decon: &DeconId,
706    declared_fns: &std::collections::HashSet<syn::Ident>,
707) -> Result<(), UnfoldError> {
708    for func in declared_fns {
709        let Some(params) = registry.flat().function(&func).map(|f| f.params.clone()) else {
710            continue;
711        };
712        for param in &params {
713            // The callback's argument types, read off the parameter's
714            // classification. `TypeKind::Callback` carries them as `TypeRef`s, so
715            // there is nothing to re-extract from the signature's syntax.
716            let prebindgen_flat::flat::TypeKind::Callback { args } = param.ty.kind() else {
717                continue;
718            };
719            for arg_ty in args {
720                // Peel a leading `&`, then detect a slice element. An
721                // `impl Fn(&T)` / `impl Fn(T)` arg of the value struct decomposes
722                // into a `Base` fixed builder (foreign side reassembles the whole
723                // value via `fromParts`); an `impl Fn(&[T])` / `impl Fn([T])` arg
724                // becomes an `Iterable` fixed FOLDER (the trampoline folds each
725                // element's leaves into a foreign list — see the callback emitter).
726                let (by_ref, after_ref) = peel_borrow(arg_ty);
727                // A run of `T` is an Iterable fold over the element; anything else
728                // is a Base fold over the value itself. `Sequence` is the one
729                // question, and it covers `[T]` and `Vec<T>` alike.
730                let (shape, matches_key) = match after_ref.sequence_elem() {
731                    Some(elem) => (
732                        UnfoldShape::Iterable(Box::new(UnfoldShape::Base)),
733                        elem.key() == vd.key,
734                    ),
735                    None => (UnfoldShape::Base, after_ref.key() == vd.key),
736                };
737                if !matches_key {
738                    continue;
739                }
740                let key = arg_ty.key();
741                if registry.callback_arg_plans.contains_key(&key) {
742                    continue;
743                }
744                register_leaves(registry, &vd.leaves);
745                let plan = UnfoldPlan {
746                    source: vd.source.clone(),
747                    decon: Some(decon.clone()),
748                    by_ref,
749                    shape,
750                    leaves: vd.leaves.clone(),
751                    element: None,
752                    delivery: Delivery::Callback,
753                    convert_out_ty: None,
754                    fixed_builder: true,
755                    hoists: Vec::new(),
756                };
757                registry.callback_arg_plans.insert(key, plan);
758            }
759        }
760    }
761    Ok(())
762}
763
764/// Wire **whole-element** `Iterable` fold plans for bare `Vec<T>` /
765/// `Option<Vec<T>>` returns and `impl Fn(&[T])` callback args whose element `T`
766/// is a single leaf (String, scalar, opaque handle) nominated by the adapter
767/// via `Prebindgen::leaf_vec_fold_elements`. Each
768/// such position crosses as decoupled raw leaves folded into a **foreign-built**
769/// list — the single-leaf dual of [`apply_value_structs`] (which handles
770/// multi-field `data_class` elements). The fold is a **fixed** foreign singleton
771/// (`fixed_builder = true`): the wrapper allocates the list, passes the hoisted
772/// appender, and returns the concrete `List<T>` (never a caller `fold` param), so
773/// no `java.util.ArrayList` is built on the Rust side.
774///
775/// Runs right after [`apply_value_structs`]; skips any function/arg that already
776/// carries a plan (an explicit `.deconstruct_output`, a `data_class` fold, …) so
777/// declared decompositions and value-struct folds win.
778pub(crate) fn apply_leaf_vec_folds<M>(
779    registry: &mut Registry<M>,
780    elements: Vec<TypeKey>,
781    declared_fns: &std::collections::HashSet<syn::Ident>,
782) -> Result<(), UnfoldError> {
783    if elements.is_empty() {
784        return Ok(());
785    }
786    let elem_keys = elements;
787    // Is the leading-`&`-peeled `bare` one of the nominated single-leaf elements?
788    let is_nominated = |bare: &prebindgen_flat::flat::TypeRef| elem_keys.contains(&bare.key());
789    for func in declared_fns {
790        let Some(params) = registry.flat().function(&func).map(|f| f.params.clone()) else {
791            continue;
792        };
793        // Output position: `Vec<T>` / `Option<Vec<T>>` return. Skip if a plan
794        // already exists (declared deconstructor / value-struct fold).
795        if !registry.unfold_plans.contains_key(func) {
796            let Some(ret) = registry.flat().function(&func).map(|f| f.ret.clone()) else {
797                continue;
798            };
799            let (optional, after_opt) = match ret.optional_inner() {
800                Some(inner) => (true, inner),
801                None => (false, &ret),
802            };
803            if let Some(vec_elem) = after_opt.sequence_elem() {
804                let bare = peel_borrow(vec_elem).1;
805                if is_nominated(bare) {
806                    let inner_shape = UnfoldShape::Iterable(Box::new(UnfoldShape::Base));
807                    let shape = if optional {
808                        UnfoldShape::Optional((), Box::new(inner_shape))
809                    } else {
810                        inner_shape
811                    };
812                    registry.require_output(vec_elem);
813                    // The fold delivers the return element-by-element, so the
814                    // whole `Vec<T>` / `Option<Vec<T>>` converter is not needed.
815                    // De-require it: for String / scalar elements it still
816                    // resolves (and is emitted as harmless dead code); for an
817                    // opaque-handle element it cannot resolve (`jlong` wire isn't
818                    // JObject-shaped), and de-requiring keeps that `None` from
819                    // being flagged as an unresolved-required error.
820                    registry.unrequire_output(&ret);
821                    registry
822                        .unfold_plans
823                        .insert(func.clone(), whole_leaf_fold_plan(vec_elem, shape));
824                }
825            }
826        }
827        // Callback-arg position: `impl Fn(&[T])` / `impl Fn([T])`.
828        for param in &params {
829            // The callback's argument types, read off the parameter's
830            // classification. `TypeKind::Callback` carries them as `TypeRef`s, so
831            // there is nothing to re-extract from the signature's syntax.
832            let prebindgen_flat::flat::TypeKind::Callback { args } = param.ty.kind() else {
833                continue;
834            };
835            for arg_ty in args {
836                let (_, after_ref) = peel_borrow(arg_ty);
837                let Some(elem) = after_ref.sequence_elem() else {
838                    continue;
839                };
840                if !is_nominated(peel_borrow(elem).1) {
841                    continue;
842                }
843                let key = arg_ty.key();
844                if registry.callback_arg_plans.contains_key(&key) {
845                    continue;
846                }
847                registry.require_output(elem);
848                let plan =
849                    whole_leaf_fold_plan(elem, UnfoldShape::Iterable(Box::new(UnfoldShape::Base)));
850                registry.callback_arg_plans.insert(key, plan);
851            }
852        }
853    }
854    Ok(())
855}
856
857/// Build a fixed-builder whole-element fold [`UnfoldPlan`] for a single-leaf
858/// element `vec_elem` (the `Vec`/slice element as written, keeping any leading
859/// `&` so `into_iter()`'s yield matches the element's own output converter).
860fn whole_leaf_fold_plan(
861    vec_elem: &prebindgen_flat::flat::TypeRef,
862    shape: UnfoldShape,
863) -> UnfoldPlan {
864    UnfoldPlan {
865        source: vec_elem.clone(),
866        decon: None,
867        by_ref: peel_borrow(vec_elem).0,
868        shape,
869        leaves: vec![],
870        element: Some(vec_elem.clone()),
871        delivery: Delivery::Callback,
872        convert_out_ty: None,
873        fixed_builder: true,
874        hoists: Vec::new(),
875    }
876}
877
878/// The deconstructor gate: every accessor-function record must be a declared
879/// `.fun_accessor` (the single source of truth for "accessor"), and no author
880/// leaf name may contain the reserved `"__"` chain separator. Binding-local
881/// records skip the accessor check — there is no `#[prebindgen]` item behind
882/// them — but keep the name check.
883///
884/// Recurses into a value form's per-field override records, so an override is
885/// held to the same rules as the declaration it replaces.
886fn check_records(
887    records: &[DeconRecord],
888    accessor_fns: &HashSet<syn::Ident>,
889) -> Result<(), UnfoldError> {
890    for rec in records {
891        let (func, name) = match rec {
892            DeconRecord::Acc { func, name } => (Some(func), name),
893            DeconRecord::LocalAcc { name, .. } => (None, name),
894            DeconRecord::Identity => continue,
895            // A value form's field names come from struct idents, not from the
896            // author, so the `"__"` in an inlined nested name is the separator
897            // doing its job. An author-supplied rename is checked where it is
898            // declared.
899            DeconRecord::Fields { func, fields, .. } => {
900                if !accessor_fns.contains(func) {
901                    return Err(UnfoldError::RecordNotAccessor { func: func.clone() });
902                }
903                for fr in fields {
904                    if let FieldDecon::Records(recs) = &fr.decon {
905                        check_records(recs, accessor_fns)?;
906                    }
907                }
908                continue;
909            }
910        };
911        if name.contains("__") {
912            return Err(UnfoldError::ReservedSeparator { name: name.clone() });
913        }
914        if let Some(func) = func {
915            if !accessor_fns.contains(func) {
916                return Err(UnfoldError::RecordNotAccessor { func: func.clone() });
917            }
918        }
919    }
920    Ok(())
921}
922
923/// The arity layers over `ty`, the types they wrap, and the value underneath.
924///
925/// A thin owned view over
926/// [`TypeRef::layer_stack`](prebindgen_flat::flat::TypeRef::layer_stack) and
927/// [`layer_types`](prebindgen_flat::flat::TypeRef::layer_types): the borrows are
928/// resolved to clones because these feed plan fields and registry calls that own
929/// their types. The classification is the model's; only the copying is local, and
930/// it happens **once**, where a value is stored — not on every question asked.
931///
932/// The stack **is** the plan's shape — `UnfoldShape` is `Shape` — so a caller
933/// stores it rather than rebuilding one from flags.
934struct Layered {
935    /// The arity layers, outermost first.
936    shape: UnfoldShape,
937    /// Every type on the way down, outermost first — what a registration walks.
938    layer_types: Vec<prebindgen_flat::flat::TypeRef>,
939    /// Past the borrow too: what actually crosses — as an **identity**, which
940    /// is all its one consumer ever asked of it.
941    core: TypeKey,
942    /// Whether the core is reached through a borrow.
943    by_ref: bool,
944}
945
946/// The layers of a type the model has **already read**.
947///
948/// Takes a `&TypeRef`, not a `&syn::Type`, and that is the whole point: a caller
949/// must hold a reading, and the ways to hold one are to take it off an element or
950/// to be the scan admitting a type with no element. Re-deriving a reading from
951/// `spell()` — the round trip this signature makes impossible — is reasoning
952/// from the spelling, which is what `origin` is not for.
953fn peel(ty: &prebindgen_flat::flat::TypeRef) -> Layered {
954    let (shape, layered) = ty.layer_stack();
955    let borrowed = layered.borrow_target();
956    Layered {
957        shape,
958        layer_types: ty.layer_types().into_iter().cloned().collect(),
959        core: borrowed.unwrap_or(layered).key(),
960        by_ref: borrowed.is_some(),
961    }
962}
963
964/// Just the borrow: whether `ty` is one, and what it borrows.
965///
966/// Deliberately **not** [`peel`]: a site that peels only the borrow means it,
967/// because the layer underneath is the thing it is about to classify. `Vec<T>`
968/// answers `(false, Vec<T>)` here and `(iterable, T)` there, and confusing the two
969/// turns "this arg is a collection" into "this arg is a T".
970fn peel_borrow(ty: &prebindgen_flat::flat::TypeRef) -> (bool, &prebindgen_flat::flat::TypeRef) {
971    match ty.borrow_target() {
972        Some(inner) => (true, inner),
973        None => (false, ty),
974    }
975}
976
977/// True when `ret` is `T` / `&T` / `Option<T|&T>` / `Vec<T|&T>` with
978/// `T == key` — the default-output match. `Result<_, _>` is NOT peeled, so a
979/// fallible factory (`-> Result<T, E>`) keeps its handle return; the error
980/// position is matched separately on `E`.
981fn returns_type(ret: &prebindgen_flat::flat::TypeRef, key: &TypeKey) -> bool {
982    peel(ret).core == *key
983}
984
985/// Build one output/error plan for `ed` and store it in the right registry map.
986fn process_decl<M>(
987    registry: &mut Registry<M>,
988    acc: &Deconstructors,
989    ed: &OutputDecl,
990) -> Result<(), UnfoldError> {
991    {
992        // The value to decompose: the success return (`Output`) or the
993        // `Result<_, E>` domain error `E` (`Error`).
994        let fn_ret = registry
995            .flat()
996            .function(&ed.func)
997            .map(|f| f.ret.clone())
998            .ok_or_else(|| UnfoldError::UnknownFunction(ed.func.clone()))?;
999        let ret_ty = match ed.target {
1000            DeconTarget::Output => fn_ret,
1001            DeconTarget::Error => {
1002                fn_ret
1003                    .fallible_parts()
1004                    .map(|(_, e)| e.clone())
1005                    .ok_or_else(|| UnfoldError::Unsupported {
1006                        func: ed.func.clone(),
1007                        reason: "convert_error/deconstruct_error on a non-Result return",
1008                    })?
1009            }
1010        };
1011
1012        // Peel an outer `Option` off the success return BEFORE probing for a
1013        // `Vec`, so `Option<Vec<T>>` composes as `Optional(Iterable)` — the
1014        // fold is skipped and a null result delivered for `None` (issue
1015        // #105). The scalar arm below reuses this peel. Error targets keep
1016        // the historical probe order (the `Vec` probe runs on `E` itself), so
1017        // an `Option<Vec<E>>` error stays whole.
1018        let (optional, after_opt) = match ed.target {
1019            DeconTarget::Output => match ret_ty.optional_inner() {
1020                Some(inner) => (true, inner),
1021                None => (false, &ret_ty),
1022            },
1023            DeconTarget::Error => (false, &ret_ty),
1024        };
1025        // `Vec<T>` / `Option<Vec<T>>` return → `Iterable` (± an `Optional`
1026        // layer). Two element-delivery modes:
1027        //   * **decomposed** (M5): the element type has an accessor →
1028        //     flatten it into leaves, fold `(acc, leaf0, …) -> acc`.
1029        //   * **whole** (M4): no accessor → deliver each element whole
1030        //     via its own output converter + projection, fold `(acc, T) -> acc`.
1031        // The other shapes (`Option`/scalar) decompose via an accessor
1032        // (M1–M3). `Vec<Option<…>>` is not supported.
1033        let plan = if let Some(inner) = after_opt.sequence_elem() {
1034            if inner.optional_inner().is_some() {
1035                return Err(UnfoldError::Unsupported {
1036                    func: ed.func.clone(),
1037                    reason: "Vec<Option<…>> returns",
1038                });
1039            }
1040            let iterable = UnfoldShape::Iterable(Box::new(UnfoldShape::Base));
1041            let shape = if optional {
1042                UnfoldShape::Optional((), Box::new(iterable))
1043            } else {
1044                iterable
1045            };
1046            // The fold delivers the return element-by-element, so the
1047            // whole-collection converter is not needed — and for an
1048            // opaque-handle element it cannot resolve at all (a `jlong` wire
1049            // isn't JObject-shaped). De-require the scan-time registrations
1050            // (the declared return and, under `Option`, the inner `Vec` its
1051            // recursive registration also required) — same reasoning as
1052            // [`apply_leaf_vec_folds`] for the fixed folds.
1053            if ed.target == DeconTarget::Output {
1054                registry.unrequire_output(&ret_ty);
1055                if optional {
1056                    registry.unrequire_output(after_opt);
1057                }
1058            }
1059            // Element type peeled of a leading `&` (accessors take `&Element`).
1060            let (by_ref, element) = peel_borrow(inner);
1061            let ekey = element.key();
1062            if let Some(d) = find_deconstructor_by_type(acc, &ekey) {
1063                // Decomposed: reuse the shared flatten (M3 nesting composes).
1064                let records = d.records.clone();
1065                let decon = decl_id(&ekey, d);
1066                register_decon_spec(registry, acc, &decon, &records, element)?;
1067                let plan = build_plan(acc, registry, ed, by_ref, element, shape, &records, decon)?;
1068                for leaf in &plan.leaves {
1069                    registry.require_output(&leaf.out_ty);
1070                }
1071                plan
1072            } else {
1073                // Whole element: keep the type exactly as written so the
1074                // element's own output converter matches `into_iter()`'s yield.
1075                // No declaration is involved (`decon: None`) — the element
1076                // crosses whole through its own converter.
1077                let by_ref = peel_borrow(inner).0;
1078                registry.require_output(inner);
1079                UnfoldPlan {
1080                    source: inner.clone(),
1081                    decon: None,
1082                    by_ref,
1083                    shape,
1084                    leaves: vec![],
1085                    element: Some(inner.clone()),
1086                    delivery: ed.delivery,
1087                    convert_out_ty: None,
1088                    fixed_builder: false,
1089                    hoists: Vec::new(),
1090                }
1091            }
1092        } else {
1093            // Scalar/decomposed arm. The `Option` peel already happened above
1094            // for `Output` (exactly one layer — `Option<Option<…>>` is NOT
1095            // re-peeled and fails as "no deconstructor" for the inner
1096            // `Option`); for `Error` it happens here, unchanged.
1097            let (optional, core_ty) = match ed.target {
1098                DeconTarget::Output => (optional, after_opt),
1099                DeconTarget::Error => match after_opt.optional_inner() {
1100                    Some(inner) => (true, inner),
1101                    None => (false, after_opt),
1102                },
1103            };
1104            let (by_ref, source) = peel_borrow(core_ty);
1105            let source_key = source.key();
1106            let shape = if optional {
1107                UnfoldShape::Optional((), Box::new(UnfoldShape::Base))
1108            } else {
1109                UnfoldShape::Base
1110            };
1111            let (records, decon) = resolve_deconstructor(acc, &source_key, ed)?;
1112            register_decon_spec(registry, acc, &decon, &records, source)?;
1113            let plan = build_plan(acc, registry, ed, by_ref, source, shape, &records, decon)?;
1114            for leaf in &plan.leaves {
1115                registry.require_output(&leaf.out_ty);
1116            }
1117            plan
1118        };
1119        // Delivery is by **leaf count**, not a per-decl flag:
1120        //   * Output, single non-nullable leaf, non-Iterable ⇒ Return (wrapper
1121        //     returns the value via its ordinary output converter —
1122        //     `convert_out_ty`).
1123        //   * Output, multiple leaves or Iterable (at any layer — an
1124        //     `Optional(Iterable)` fold has no single value to return) ⇒
1125        //     Callback (builder / fold).
1126        //   * Error ⇒ always Callback-shaped: every leaf is a `ze` arg after the
1127        //     fixed `je` (no return-value path; `convert_out_ty` stays None).
1128        //
1129        // A NULLABLE leaf is one whose path passes through an `Option` that
1130        // something is decomposed below (`Option<Handle>` reached by
1131        // `.field_self()`, a nested value form behind an `Option`). Returning it
1132        // has nowhere to put the absent case: a return value is one expression,
1133        // so there is no `None` arm, and `convert_out_ty` names the leaf's own
1134        // type rather than an optional of it. Callback delivery has that arm
1135        // already — the leaf crosses as a boxed `Long` / JVM null — so the
1136        // shape goes there instead of being composed into Rust that hands
1137        // `&Option<T>` to a converter typed for `T`.
1138        let single_return = ed.target == DeconTarget::Output
1139            && !plan.shape.has_iterable_layer()
1140            && plan.leaves.len() == 1
1141            && !plan.leaves[0].nullable;
1142        let plan = if single_return {
1143            // Composed with the model's own layering rather than by spelling
1144            // `Option<#leaf_ty>` and handing the tokens over: `optional()` pairs
1145            // the `kind` with its spelling in one place, so the reading that
1146            // reaches the table is the one this plan carries (#281).
1147            let cv = if matches!(plan.shape, UnfoldShape::Optional((), _)) {
1148                plan.leaves[0].out_ty.optional()
1149            } else {
1150                plan.leaves[0].out_ty.clone()
1151            };
1152            registry.require_output(&cv);
1153            UnfoldPlan {
1154                delivery: Delivery::Return,
1155                convert_out_ty: Some(cv.clone()),
1156                ..plan
1157            }
1158        } else {
1159            UnfoldPlan {
1160                delivery: Delivery::Callback,
1161                ..plan
1162            }
1163        };
1164        match ed.target {
1165            DeconTarget::Output => registry.unfold_plans.insert(ed.func.clone(), plan),
1166            DeconTarget::Error => registry.error_plans.insert(ed.func.clone(), plan),
1167        };
1168    }
1169    Ok(())
1170}
1171
1172/// The identity of a found declaration — the type's default deconstructor.
1173fn decl_id(type_key: &TypeKey, _decl: &DeconstructorDecl) -> DeconId {
1174    DeconId::Default(type_key.to_string())
1175}
1176
1177/// Register the declaration-default [`DeconSpec`] for `decon` (no-op when
1178/// already present): re-flatten the records with normalized inputs —
1179/// borrowed identity, no outer shape — so the stored spec is independent of
1180/// the using function's return shape and of processing order.
1181fn register_decon_spec<M>(
1182    registry: &mut Registry<M>,
1183    acc: &Deconstructors,
1184    decon: &DeconId,
1185    records: &[DeconRecord],
1186    source: &prebindgen_flat::flat::TypeRef,
1187) -> Result<(), UnfoldError> {
1188    if registry.decon_plans.contains_key(decon) {
1189        return Ok(());
1190    }
1191    let mut leaves: Vec<UnfoldLeaf> = Vec::new();
1192    let mut visited: HashSet<TypeKey> = HashSet::new();
1193    visited.insert(source.key());
1194    flatten(
1195        acc,
1196        registry,
1197        records,
1198        source,
1199        &[],
1200        &[],
1201        true,
1202        false,
1203        &mut visited,
1204        &mut leaves,
1205        // A `DeconSpec` describes the leaf list only — signature artifacts are
1206        // derived from it, never emitted code — so its hoists are discarded.
1207        &mut Vec::new(),
1208    )?;
1209    require_unique_leaf_names(source, &leaves)?;
1210    registry.decon_plans.insert(
1211        decon.clone(),
1212        DeconSpec {
1213            source: source.clone(),
1214            leaves,
1215        },
1216    );
1217    Ok(())
1218}
1219
1220/// Pick the deconstructor (its records + declaration identity) for one
1221/// output expansion.
1222fn resolve_deconstructor(
1223    acc: &Deconstructors,
1224    source_key: &TypeKey,
1225    ed: &OutputDecl,
1226) -> Result<(Vec<DeconRecord>, DeconId), UnfoldError> {
1227    match &ed.sel {
1228        DeconSel::Inline(records) => Ok((
1229            records.clone(),
1230            DeconId::PerFn(source_key.to_string(), ed.func.to_string()),
1231        )),
1232        DeconSel::TopLevel => find_deconstructor_by_type(acc, source_key)
1233            .map(|d| (d.records.clone(), DeconId::Default(source_key.to_string())))
1234            .ok_or_else(|| UnfoldError::NoDeconstructor {
1235                func: ed.func.clone(),
1236                target: source_key.to_string(),
1237            }),
1238    }
1239}
1240
1241/// Find the deconstructor whose target is `type_key` (unique per type:
1242/// `ensure_default_deconstructor` dedups by type key). Used for both the
1243/// top-level output expansion and nested-record splicing.
1244fn find_deconstructor_by_type<'a>(
1245    acc: &'a Deconstructors,
1246    type_key: &TypeKey,
1247) -> Option<&'a DeconstructorDecl> {
1248    acc.deconstructors.iter().find(|c| c.target == *type_key)
1249}
1250
1251/// Build the [`UnfoldPlan`] for a chosen accessor. `shape` is the outer
1252/// shape over the core decomposition (`Decompose` for `T`/`&T`,
1253/// `Optional(Decompose)` for `Option<T>`/`Option<&T>`). The records are
1254/// recursively flattened ([`flatten`]) — nested accessors contribute
1255/// their leaves with the access path prefixed.
1256#[allow(clippy::too_many_arguments)]
1257fn build_plan<M>(
1258    acc: &Deconstructors,
1259    registry: &Registry<M>,
1260    ed: &OutputDecl,
1261    by_ref: bool,
1262    source: &prebindgen_flat::flat::TypeRef,
1263    shape: UnfoldShape,
1264    records: &[DeconRecord],
1265    decon: DeconId,
1266) -> Result<UnfoldPlan, UnfoldError> {
1267    let mut leaves: Vec<UnfoldLeaf> = Vec::new();
1268    let mut visited: HashSet<TypeKey> = HashSet::new();
1269    visited.insert(source.key());
1270    let mut hoists: Vec<Hoist> = Vec::new();
1271    flatten(
1272        acc,
1273        registry,
1274        records,
1275        source,
1276        &[],
1277        &[],
1278        by_ref,
1279        false,
1280        &mut visited,
1281        &mut leaves,
1282        &mut hoists,
1283    )?;
1284    require_unique_leaf_names(source, &leaves)?;
1285    require_root_identity_last(by_ref, source, &leaves)?;
1286
1287    Ok(UnfoldPlan {
1288        source: source.clone(),
1289        decon: Some(decon),
1290        by_ref,
1291        shape,
1292        leaves,
1293        element: None,
1294        delivery: ed.delivery,
1295        convert_out_ty: None,
1296        fixed_builder: false,
1297        hoists,
1298    })
1299}
1300
1301/// Error when an **owned** decomposition emits the root identity leaf before a
1302/// nested identity leaf. Leaves are emitted in declaration order, and the root
1303/// identity MOVES the owned value while a nested identity clones from a borrow
1304/// of it — the wrong order generates non-compiling Rust ("use of moved value")
1305/// with a cryptic rustc message. Caught here instead, with the fix in the
1306/// error: declare `.field_self()` after the nested-identity fields. (Borrowed
1307/// decompositions clone the root identity, so any order is fine.)
1308fn require_root_identity_last(
1309    by_ref: bool,
1310    source: &prebindgen_flat::flat::TypeRef,
1311    leaves: &[UnfoldLeaf],
1312) -> Result<(), UnfoldError> {
1313    if by_ref {
1314        return Ok(());
1315    }
1316    let root_at = leaves.iter().position(|l| l.identity && l.path.is_empty());
1317    let last_nested_at = leaves
1318        .iter()
1319        .rposition(|l| l.identity && !l.path.is_empty());
1320    if let (Some(root), Some(nested)) = (root_at, last_nested_at) {
1321        if root < nested {
1322            return Err(UnfoldError::RootIdentityBeforeNested {
1323                target: source.key().to_string(),
1324            });
1325        }
1326    }
1327    Ok(())
1328}
1329
1330/// Recursively flatten an accessor's records into [`UnfoldLeaf`]s.
1331///
1332/// * `source` — the type whose accessor `records` belong to (the root
1333///   on the first call, a nested child type on recursion).
1334/// * `path_prefix` — accessor chain from the root value to `source` (empty at
1335///   the root; `[…, nesting_accessor]` when recursing into a nested child).
1336/// * `by_ref` — the top-level return/element borrow-ness. The identity leaf is
1337///   **owned** (`source`) only at the root of an owned value (`path_prefix`
1338///   empty && `!by_ref`) — a `Copy` value delivers itself by copy and an
1339///   opaque handle moves; everywhere else it is **borrowed** (`&source`,
1340///   cloned).
1341/// * `nullable` — `true` once any nesting accessor on the path returned
1342///   `Option` (the reached value may be absent ⇒ the leaf is `null`).
1343/// * `visited` — type keys on the current nesting chain (cycle guard; entries
1344///   are removed after each nested recursion so sibling records may reuse a type).
1345#[allow(clippy::too_many_arguments)]
1346fn flatten<M>(
1347    acc: &Deconstructors,
1348    registry: &Registry<M>,
1349    records: &[DeconRecord],
1350    source: &prebindgen_flat::flat::TypeRef,
1351    path_prefix: &[PathStep],
1352    name_prefix: &[String],
1353    by_ref: bool,
1354    nullable: bool,
1355    visited: &mut HashSet<TypeKey>,
1356    leaves: &mut Vec<UnfoldLeaf>,
1357    hoists: &mut Vec<Hoist>,
1358) -> Result<(), UnfoldError> {
1359    let source_key = source.key();
1360    // The author-supplied (literal) leaf-name segment at this level, appended
1361    // to the inherited chain prefix. Segments are joined with `"__"`.
1362    let seg_name = |name: &str| -> Vec<String> {
1363        let mut v = name_prefix.to_vec();
1364        v.push(name.to_string());
1365        v
1366    };
1367    // Identity uniqueness is per accessor (one move/clone of the value
1368    // at this level); nested levels each get their own identity budget.
1369    let mut seen_identity = false;
1370
1371    for rec in records {
1372        match rec {
1373            DeconRecord::Identity => {
1374                if seen_identity {
1375                    return Err(UnfoldError::MultipleIdentity {
1376                        target: source_key.to_string(),
1377                    });
1378                }
1379                seen_identity = true;
1380                // Owned where the value is OURS to give: the root of an owned
1381                // plan (a `Copy` blob copies / an opaque handle moves), or a
1382                // field of a value form that CONSUMED its value — that form was
1383                // handed the value, so its fields move out like every other
1384                // field of it. Borrowed (clone) otherwise. The adapter-side type
1385                // + projection come from this `out_ty`'s output converter, so
1386                // this is what decides whether the leaf is boxed by move or
1387                // cloned through the borrowed-opaque one.
1388                // A plan field: the drop to spelling happens here, where the value
1389                // is stored for emission, and the borrowed form is composed rather
1390                // than looked up because no source wrote it.
1391                // The borrowed form is COMPOSED — no source wrote it — and the
1392                // composition pairs the kind with its own spelling, so nothing
1393                // downstream has to look either up.
1394                let out_ty = if place_is_owned(hoists, path_prefix, by_ref) {
1395                    source.clone()
1396                } else {
1397                    source.borrowed()
1398                };
1399                leaves.push(UnfoldLeaf {
1400                    name: if path_prefix.is_empty() {
1401                        "handle".to_string()
1402                    } else {
1403                        name_prefix.join("__")
1404                    },
1405                    path: path_prefix.to_vec(),
1406                    out_ty,
1407                    identity: true,
1408                    nullable,
1409                    source: LeafSource::Accessor,
1410                    group: None,
1411                });
1412            }
1413            DeconRecord::Fields {
1414                func,
1415                consuming,
1416                fields,
1417            } => {
1418                let consuming = *consuming;
1419                // The value form is called once; every field hangs off that one
1420                // call, so the whole record shares a single `Call` step and the
1421                // emitter can hoist it.
1422                accessor_signature(registry, func, &source.key())?;
1423                // The declarator states whether the value is given away; the
1424                // signature has to agree, or the emitted call would not compile
1425                // in the consumer's crate. Checked rather than inferred so that
1426                // declaring `.fields_self_into(..)` on a borrowing accessor is a
1427                // named error instead of a silently downgraded boundary.
1428                if consuming != accessor_consumes(registry, func) {
1429                    return Err(UnfoldError::Unsupported {
1430                        func: func.clone(),
1431                        reason: if consuming {
1432                            "declared as a CONSUMING value form (`.fields_self_into(..)`) but the \
1433                             accessor borrows its receiver — declare it with `.fields(..)`, or \
1434                             name the by-value accessor"
1435                        } else {
1436                            "declared as a BORROWING value form (`.fields(..)`) but the accessor \
1437                             takes its receiver by value — declare it with `.fields_self_into(..)`, or \
1438                             name the `&Self` accessor"
1439                        },
1440                    });
1441                }
1442                let mut root_path = path_prefix.to_vec();
1443                root_path.push(PathStep::call(func.clone(), false, false));
1444                // A hoist below an optional step is CONDITIONAL: it binds an
1445                // `Option<TStruct>` local (built only in the `Some` arm) and
1446                // every leaf under it is null when the value is absent — which
1447                // is the nullability `flatten` already propagates down here.
1448                // What it cannot do is nest: composing a second hoist off a
1449                // conditional one would have to reach through the outer
1450                // `Option`, and the binder has no arm to put that in. One level
1451                // is the shape real bindings need (`Option<&Sample>` delivering
1452                // a sample's value form), so implement that and name the rest.
1453                //
1454                // A top-level `Option<T>` is represented by
1455                // `UnfoldShape::Optional`, not by a path step, and is unaffected.
1456                if root_path.iter().any(PathStep::is_optional)
1457                    && hoists.iter().any(|h| {
1458                        h.prefix.len() < root_path.len() && root_path.starts_with(&h.prefix)
1459                    })
1460                {
1461                    return Err(UnfoldError::Unsupported {
1462                        func: func.clone(),
1463                        reason: "a value form nested under another one that is reached through \
1464                                 `Option` — conditional hoists do not nest",
1465                    });
1466                }
1467                // A consuming value form DESTROYS the value into its parts, so
1468                // a sibling record — `.field_self()` or another `.field()` —
1469                // would read what it just gave away. jnigen refuses this in the
1470                // declarator, where the author can see it; this is the backstop
1471                // for records built directly against core.
1472                //
1473                // Being reached through ANOTHER value form is fine: a hoisted
1474                // value form is an owned struct and its fields are disjoint, so
1475                // the parent's field is handed over by move.
1476                if consuming && records.len() > 1 {
1477                    return Err(UnfoldError::Unsupported {
1478                        func: func.clone(),
1479                        reason: "a consuming value form must be the only record of its \
1480                                 declaration — it moves the value, so `.field_self()` or \
1481                                 a sibling `.field()` would read a moved value",
1482                    });
1483                }
1484                // Evaluate this value form ONCE. Recorded at the prefix it sits
1485                // at rather than as a lone accessor, so a nested value form
1486                // (this record reached through another one's field) gets its own
1487                // hoist instead of being rebuilt per child leaf. `path_prefix`
1488                // grows as `flatten` descends, so the list comes out
1489                // outermost-first.
1490                hoists.push(Hoist {
1491                    prefix: root_path.clone(),
1492                    consuming,
1493                });
1494
1495                for fr in fields {
1496                    // The declaration carries the field's reading, so nothing is
1497                    // looked up and nothing is re-classified.
1498                    //
1499                    // A field's own `Option` makes everything under it nullable,
1500                    // exactly as an `Option`-returning accessor step does.
1501                    let (opt, core) = match fr.ty.optional_inner() {
1502                        Some(inner) => (true, inner),
1503                        None => (false, &fr.ty),
1504                    };
1505                    let child_ty = core.borrow_target().unwrap_or(core);
1506                    let child_key = child_ty.key();
1507
1508                    // Same three-way choice a `.field()` record makes: declared
1509                    // override, else the field type's own deconstructor, else
1510                    // one leaf — with the adapter able to pre-build the leaves
1511                    // for a shape only it can describe.
1512                    let child_records = match &fr.decon {
1513                        FieldDecon::Records(recs) => Some(recs.clone()),
1514                        FieldDecon::Leaves(_) => None,
1515                        FieldDecon::Default => match find_deconstructor_by_type(acc, &child_key) {
1516                            Some(child_decl) if !visited.contains(&child_key) => {
1517                                Some(child_decl.records.clone())
1518                            }
1519                            Some(_) => {
1520                                return Err(UnfoldError::Cycle {
1521                                    target: child_key.to_string(),
1522                                });
1523                            }
1524                            None => None,
1525                        },
1526                    };
1527                    let decomposed =
1528                        child_records.is_some() || matches!(fr.decon, FieldDecon::Leaves(_));
1529
1530                    // The field's own `Option` is a nullable NESTING step only
1531                    // when something is decomposed below it. For a plain leaf
1532                    // the whole `Option<F>` is what the converter takes — the
1533                    // same rule that makes a terminal accessor's `Option` ride
1534                    // its converter instead of being unwrapped.
1535                    let mut field_path = root_path.clone();
1536                    let (last, lead) = fr
1537                        .members
1538                        .split_last()
1539                        .expect("a field record addresses at least one member");
1540                    // Only the LAST member can be optional — an inlined nested
1541                    // class is reached directly, never through an `Option`.
1542                    field_path.extend(lead.iter().map(|m| PathStep::field(m.clone(), false)));
1543                    field_path.push(PathStep::field(last.clone(), opt && decomposed));
1544
1545                    // Adapter-built leaves: rebase each onto this field's path
1546                    // and name. Their internal structure (a selector plus its
1547                    // groups) is opaque here and passes through untouched.
1548                    if let FieldDecon::Leaves(built) = &fr.decon {
1549                        for l in built {
1550                            let mut path = field_path.clone();
1551                            path.extend(l.path.iter().cloned());
1552                            let mut name = seg_name(&fr.name);
1553                            name.push(l.name.clone());
1554                            leaves.push(UnfoldLeaf {
1555                                name: name.join("__"),
1556                                path,
1557                                nullable: l.nullable || nullable || opt,
1558                                ..l.clone()
1559                            });
1560                        }
1561                        continue;
1562                    }
1563
1564                    if let Some(child_records) = child_records {
1565                        visited.insert(child_key.clone());
1566                        flatten(
1567                            acc,
1568                            registry,
1569                            &child_records,
1570                            // The declaration's reading, peeled — not a new one.
1571                            child_ty,
1572                            &field_path,
1573                            &seg_name(&fr.name),
1574                            by_ref,
1575                            nullable || opt,
1576                            visited,
1577                            leaves,
1578                            hoists,
1579                        )?;
1580                        visited.remove(&child_key);
1581                    } else {
1582                        // A plain field leaf: the value is CLONED out of the
1583                        // struct, so its converter takes the owned field type as
1584                        // written — `Option` and all, which is why a terminal
1585                        // `Option` step is not a nesting step for it.
1586                        leaves.push(UnfoldLeaf {
1587                            name: seg_name(&fr.name).join("__"),
1588                            path: field_path,
1589                            out_ty: fr.ty.clone(),
1590                            identity: false,
1591                            nullable,
1592                            source: LeafSource::Field,
1593                            group: None,
1594                        });
1595                    }
1596                }
1597            }
1598            DeconRecord::Acc { name, .. } | DeconRecord::LocalAcc { name, .. } => {
1599                // A binding-local record resolves through its synthesized
1600                // registry entry (see `synthesize_local_accessors`), so both
1601                // kinds read one signature source; only the cycle rule below
1602                // differs.
1603                let (func, local) = match rec {
1604                    DeconRecord::Acc { func, .. } => (func.clone(), false),
1605                    DeconRecord::LocalAcc { path, .. } => (DeconRecord::local_ident(path), true),
1606                    DeconRecord::Identity | DeconRecord::Fields { .. } => unreachable!(),
1607                };
1608                let ret = accessor_signature(registry, &func, &source.key())?;
1609                // Default unwrap: if the return type has its own deconstructor,
1610                // splice it (recurse); otherwise the return is one leaf. Peel an
1611                // `Option` (value may be absent) + leading `&` to reach the child.
1612                // This site peels an `Option` only — an accessor returning a run
1613                // of values is not spliced — so it asks the model for that one
1614                // layer rather than the whole stack.
1615                let after_opt = ret.optional_inner();
1616                let opt = after_opt.is_some();
1617                let core = after_opt.unwrap_or(&ret);
1618                let (core_by_ref, child_ty) = peel_borrow(core);
1619                let child_key = child_ty.key();
1620                // A child already on the nesting chain: for a `#[prebindgen]`
1621                // accessor that is an authoring cycle (hard error); a
1622                // binding-local field re-delivering (part of) its own type
1623                // under a binding-defined condition is the POINT — degrade to
1624                // a plain converter leaf instead of splicing.
1625                let splice = match find_deconstructor_by_type(acc, &child_key) {
1626                    Some(child_decl) if !visited.contains(&child_key) => Some(child_decl),
1627                    Some(_) if local => None,
1628                    Some(_) => {
1629                        return Err(UnfoldError::Cycle {
1630                            target: child_key.to_string(),
1631                        });
1632                    }
1633                    None => None,
1634                };
1635                if let Some(child_decl) = splice {
1636                    visited.insert(child_key.clone());
1637                    let child_records = child_decl.records.clone();
1638                    let mut child_path = path_prefix.to_vec();
1639                    child_path.push(PathStep::call(func.clone(), opt, !core_by_ref));
1640                    flatten(
1641                        acc,
1642                        registry,
1643                        &child_records,
1644                        child_ty,
1645                        &child_path,
1646                        &seg_name(name),
1647                        by_ref,
1648                        nullable || opt,
1649                        visited,
1650                        leaves,
1651                        hoists,
1652                    )?;
1653                    visited.remove(&child_key);
1654                } else {
1655                    // Leaf: the return value as written (`&str`, enum, `i64`, …).
1656                    // One exception: a binding-local field returning an
1657                    // OPTIONAL BORROW (`Option<&T>`) is the conditional
1658                    // HANDLE-delivery idiom — structurally a spliced identity
1659                    // behind an `Option`-returning step (cf. an
1660                    // `Option`-returning nesting accessor + the child's
1661                    // `field_self`), so it contributes a nullable IDENTITY
1662                    // leaf of the borrowed type: the reach path unwraps the
1663                    // final `Option` (the synthesized signature keeps the
1664                    // full return) and the value clones through its handle
1665                    // projection, `None` delivering null. It shares the
1666                    // one-identity-per-deconstructor budget with
1667                    // `.field_self()` — two handle deliveries of one value
1668                    // make no sense.
1669                    let cond_handle = local && opt && core_by_ref;
1670                    if cond_handle {
1671                        if seen_identity {
1672                            return Err(UnfoldError::MultipleIdentity {
1673                                target: source_key.to_string(),
1674                            });
1675                        }
1676                        seen_identity = true;
1677                    }
1678                    // A plan field: the spelling is taken here, once, where the
1679                    // leaf is stored for emission.
1680                    let (out_ty, nullable, identity) = if cond_handle {
1681                        (core.clone(), true, true)
1682                    } else {
1683                        (ret.clone(), nullable, false)
1684                    };
1685                    let mut path = path_prefix.to_vec();
1686                    path.push(PathStep::call(func.clone(), opt, !core_by_ref));
1687                    leaves.push(UnfoldLeaf {
1688                        name: seg_name(name).join("__"),
1689                        path,
1690                        out_ty,
1691                        identity,
1692                        nullable,
1693                        source: LeafSource::Accessor,
1694                        group: None,
1695                    });
1696                }
1697            }
1698        }
1699    }
1700
1701    Ok(())
1702}
1703
1704/// Error if two leaves of one flattened deconstructor share a name. Author leaf
1705/// names are explicit and emitted literally, so a collision is a declaration
1706/// bug — never auto-resolved.
1707fn require_unique_leaf_names(
1708    source: &prebindgen_flat::flat::TypeRef,
1709    leaves: &[UnfoldLeaf],
1710) -> Result<(), UnfoldError> {
1711    let mut seen: HashSet<&str> = HashSet::new();
1712    for l in leaves {
1713        if !seen.insert(l.name.as_str()) {
1714            return Err(UnfoldError::DuplicateLeafName {
1715                target: source.key().to_string(),
1716                name: l.name.clone(),
1717            });
1718        }
1719    }
1720    Ok(())
1721}
1722
1723/// Make a signature's name list unique: a duplicate gets a numeric suffix
1724/// (`name2`, `name3`, …). Adapters run this over the final per-signature
1725/// list (after their own casing), since one signature may concatenate the
1726/// leaves of several plans.
1727pub fn dedup_names(names: &mut [String]) {
1728    let mut seen: HashSet<String> = HashSet::new();
1729    for n in names.iter_mut() {
1730        if !seen.insert(n.clone()) {
1731            let mut k = 2;
1732            while !seen.insert(format!("{n}{k}")) {
1733                k += 1;
1734            }
1735            *n = format!("{n}{k}");
1736        }
1737    }
1738}
1739
1740/// An accessor `f(&T) -> R`: returns its return type `R` as written (a
1741/// reference where possible).
1742///
1743/// `expected` is the type the deconstructor decomposes, and what comes back is
1744/// an accessor already proven to take it. Taking it as a parameter rather than
1745/// leaving the caller to check afterwards is the point: a declarator cannot
1746/// reach an accessor's signature without saying what that accessor is supposed
1747/// to be about, so the check cannot be the thing a new declarator forgets
1748/// (#223). The comparison is [`check_declared_target`], shared with the input
1749/// side's constructor lookup.
1750fn accessor_signature<M>(
1751    registry: &Registry<M>,
1752    func: &syn::Ident,
1753    expected: &TypeKey,
1754) -> Result<prebindgen_flat::flat::TypeRef, UnfoldError> {
1755    let f = registry
1756        .flat()
1757        .function(&func)
1758        .ok_or_else(|| UnfoldError::UnknownAccessor(func.clone()))?;
1759
1760    // First parameter is the receiver `&T`; peel the borrow to get `T`.
1761    // `borrow_target` is the model's own answer, so the peel reads a
1762    // classification instead of re-deciding it from `syn::Type::Reference`.
1763    let first = f
1764        .params
1765        .first()
1766        .ok_or_else(|| UnfoldError::UnknownAccessor(func.clone()))?;
1767    // The receiver's identity, keyed so the comparison below cannot fail on a
1768    // spelling difference that does not change which type this is about.
1769    let takes = match first.ty.borrow_target() {
1770        Some(inner) => inner.key(),
1771        None => first.ty.key(),
1772    };
1773    check_declared_target(func, &takes, expected)?;
1774    Ok(f.ret.clone())
1775}
1776
1777/// Whether the value sitting at `path_prefix` is the plan's **to give away**:
1778/// the root of an owned plan, or a field of a value form that consumed its
1779/// value and is reached by a movable run of field steps.
1780///
1781/// Consulted where a leaf's `out_ty` is chosen, so the ownership decision is
1782/// made ONCE, in the plan, rather than re-derived by each emitter — a leaf
1783/// whose `out_ty` is the owned type is boxed by move, one whose `out_ty` is a
1784/// borrow is cloned through the borrowed-opaque converter.
1785fn place_is_owned(hoists: &[Hoist], path_prefix: &[PathStep], by_ref: bool) -> bool {
1786    if path_prefix.is_empty() {
1787        return !by_ref;
1788    }
1789    hoists
1790        .iter()
1791        .filter(|h| h.prefix.len() <= path_prefix.len() && path_prefix.starts_with(&h.prefix))
1792        .max_by_key(|h| h.prefix.len())
1793        .is_some_and(|h| h.consuming && steps_are_movable(&path_prefix[h.prefix.len()..]))
1794}
1795
1796/// Whether an accessor takes its receiver **by value** — a *consuming* value
1797/// form, which destroys the object into its parts instead of cloning them out
1798/// of a borrow.
1799///
1800/// Asked separately because [`accessor_signature`] peels the `&` in order to
1801/// compare target types, so `f(v: T)` and `f(v: &T)` are indistinguishable
1802/// there by design.
1803fn accessor_consumes<M>(registry: &Registry<M>, func: &syn::Ident) -> bool {
1804    registry
1805        .flat()
1806        .function(&func)
1807        .and_then(|f| f.params.first())
1808        .is_some_and(|p| p.ty.borrow_target().is_none())
1809}
1810
1811/// The shared mismatch, in this direction's vocabulary: an output accessor is
1812/// declared to **take** the type the deconstructor decomposes.
1813impl From<crate::declared_target::TargetMismatch> for UnfoldError {
1814    fn from(m: crate::declared_target::TargetMismatch) -> Self {
1815        UnfoldError::AccessorTargetMismatch {
1816            accessor: m.func,
1817            takes: m.actual,
1818            expected: m.expected,
1819        }
1820    }
1821}
1822
1823#[cfg(test)]
1824mod tests;