prebindgen-jni 0.5.0

JNI / Kotlin binding generator for prebindgen
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
//! Projection / `FoldStrategy` folding helpers and Kotlin type-shape
//! probes for the JNI back-end's Kotlin emitter.
//!
//! Carved from the former `jni_kotlin_ext.rs`; shares the `jni` namespace
//! via `use super::*`.

use kotlin_codegen::KtType;
use prebindgen_registry::flat::TypeRef;

use super::*;

/// Peel the layers that never change whether a value's core is a Kotlin enum,
/// **off the model**: a borrow and an optional, in any nesting. So `&Priority`,
/// `Priority`, `Option<Priority>` and `Option<&Priority>` all probe as
/// `Priority` — letting nullable enum params (`Option<enum>`) wire as `Int?` +
/// `?.value` just like a non-null enum wires as `Int` + `.value`, instead of
/// leaking the enum object to the (boxed-int-expecting) Rust converter.
///
/// A **run is not peeled**. `Vec<Priority>` is a `List<Priority>`, not an enum,
/// so this is deliberately not [`TypeRef::layer_stack`], which strips the
/// sequence layer too.
///
/// Borrowing rather than composing is not a shortcut: every layer of a reading
/// already holds the next as a `TypeRef` of its own, so there is nothing to
/// mint — which is also why this needs no registry. What it returns spells
/// itself (`spell()`) and classifies itself (`kind`), and the two cannot
/// disagree.
pub(crate) fn enum_probe(reading: &TypeRef) -> &TypeRef {
    let mut cur = reading;
    while let Some(inner) = cur.borrow_target().or_else(|| cur.optional_inner()) {
        cur = inner;
    }
    cur
}

// The bottom-up layer fold is the shared `prebindgen_registry::shape::fold_shape`
// (its `on_optional` receives the layer's `&NullableKind` + the wrapped
// `&FoldStrategy`, so callers can special-case e.g. a `Niche` layer sitting
// directly over the `Base` leaf). Used by the **type-name** folds
// (`handle_kt_type` / `projection_wire_return`). The **expression** folds
// (`render_handle_close` / `fold_projection_wrap`) are deliberately *not*
// expressed through it: they fold the other direction (threading a `receiver` /
// fresh lambda variable top-down rather than combining a bottom-up result), so
// a shared combinator would obscure rather than simplify them.
use prebindgen_registry::shape::fold_shape;

/// The Kotlin type for a closeable handle reached through the folded
/// [`FoldStrategy`] layers, given the leaf typed-handle type (e.g.
/// `ZKeyExpr`): `Direct → ZKeyExpr`, `Nullable(inner) → <inner>?`,
/// `Iterable(inner) → List<<inner>>`.
pub(crate) fn handle_kt_type(strategy: &FoldStrategy, leaf: &KtType) -> KtType {
    fold_shape(
        strategy,
        &|| leaf.clone(),
        // The declared Kotlin projection type is `T?` regardless of how null
        // is represented over the wire — the wrap fold and the wire-return
        // helper read the kind to handle the wire shape separately.
        &|inner, _kind, _inner_strategy| inner.nullable(),
        &|inner| KtType::generic("List", [inner]),
    )
}

/// Typed Kotlin leaf of a projection. Declared handle projections
/// take their configured class FQN; the built-in `u64` projection is Kotlin's
/// stable unsigned scalar type.
pub(crate) fn projection_leaf_kt(ext: &Declarations, proj: &Projection) -> Option<KtType> {
    match proj.kind {
        ProjectionKind::Handle => ext.kotlin_fqn(&proj.leaf_key).map(KtType::cls),
        ProjectionKind::Unsigned64 => Some(KtType::cls("ULong")),
    }
}

/// Wrap one raw projection leaf into its typed Kotlin form.
pub(crate) fn projection_wrap_expr(kind: &ProjectionKind, short: &str, raw: &str) -> String {
    match kind {
        ProjectionKind::Handle => format!("{short}({raw})"),
        ProjectionKind::Unsigned64 => format!("{raw}.toULong()"),
    }
}

/// For a projection (handle / unsigned) **struct field**,
/// compute the `(wire_param_type, wrap_expr)` the data class's `fromParts`
/// factory uses: the wire param type matches the leaf wire
/// `struct_output_body` passes (handle → `Long` jlong sentinel), and the wrap
/// reconstructs the typed value in JVM
/// bytecode (`Short(arg)`, with null mapped from the `0L` sentinel for handles
/// or the declared invalid `Long` for a bounded unsigned representation; JVM
/// null remains the fallback for non-niche value projections). Only the
/// `Direct` and `Nullable{Direct}` shapes a scalar projection field can take
/// are supported — a collection
/// (`Vec<projection>`) field is rejected (matching the struct bridge's
/// scalar-only guard).
pub(crate) fn factory_projection_wire_wrap(
    proj: &crate::jni::Projection,
    short: &str,
    name: &str,
) -> (KtType, String) {
    use prebindgen_registry::shape::Shape::*;

    use crate::jni::{NullableKind, ProjectionKind::*};
    let direct = |kind: &crate::jni::ProjectionKind| match kind {
        Handle => (KtType::long(), format!("{short}({name})")),
        Unsigned64 => (KtType::long(), format!("{name}.toULong()")),
    };
    match &proj.strategy {
        Base => direct(&proj.kind),
        Optional(nullable, inner) => {
            if !matches!(**inner, Base) {
                panic!(
                    "factory_projection_wire_wrap: only `Nullable<Direct>` projection struct \
                     fields are supported (field `{name}`)"
                );
            }
            match proj.kind {
                // Handle null rides the `0L` jlong sentinel.
                Handle => (
                    KtType::long(),
                    format!("if ({name} == 0L) null else {short}({name})"),
                ),
                Unsigned64 => match nullable {
                    // A bounded unsigned representation reserves an invalid
                    // raw value for `None`, so the factory receives primitive
                    // `Long` and restores the nullable semantic property.
                    NullableKind::Niche => {
                        let sentinel = projection_leaf_sentinel(proj).unwrap_or_else(|| {
                            panic!(
                                "factory_projection_wire_wrap: niche unsigned field `{name}` \
                                 has no declared sentinel"
                            )
                        });
                        (
                            KtType::long(),
                            format!("if ({name} == {sentinel}) null else {name}.toULong()"),
                        )
                    }
                    // Plain `Option<u64>` has no spare bit pattern, so its
                    // primitive wire is boxed and JVM null represents `None`.
                    NullableKind::Boxed => {
                        (KtType::long().nullable(), format!("{name}?.toULong()"))
                    }
                },
            }
        }
        Iterable(_) => panic!(
            "factory_projection_wire_wrap: collection (`Vec<projection>`) struct fields are not \
             supported by the fromParts factory (field `{name}`)"
        ),
    }
}

/// True for the Kotlin types that map to JVM **primitives** (never null over
/// the JNI boundary). Used to decide which flattened `Option<nested>` leaf
/// params must be made nullable in the parent factory signature.
pub(crate) fn is_kotlin_primitive_ty(t: &KtType) -> bool {
    !t.is_nullable()
        && t.leaf_name().is_some_and(|n| {
            matches!(
                n,
                "Long" | "Int" | "Boolean" | "Double" | "Float" | "Byte" | "Short" | "Char"
            )
        })
}

/// Recursively build the Kotlin `fromParts` factory for a data class — the
/// mirror of the native `flatten_struct_encode` (in the [`jni`](super)
/// module). Both walk the same [`build_struct_plan`], so the leaf order and
/// slot types agree by construction.
/// Returns `(params, reconstruct)`:
/// * `params` — the flattened `(name, kotlin_type)` list (one per transitive
///   leaf wire; nested data-class fields are inlined, `Option<nested>` prepends
///   a `…__present: Boolean` flag). Order/types match the native call's JVM
///   descriptor positionally.
/// * `reconstruct` — the Kotlin expression building this struct:
///   `Class(<part per constructor field>)`, where a nested field reconstructs
///   via `Child.fromParts(<child param names>)` (`if (present) … else null` when
///   optional) and a leaf reconstructs with its wrap.
#[allow(clippy::too_many_arguments)]
pub(crate) fn flatten_struct_factory(
    ext: &Declarations,
    registry: &Registry<KotlinMeta>,
    s: &prebindgen_registry::flat::Struct,
    prefix: &str,
    class_name: &str,
    imports: &mut BTreeSet<String>,
    depth: usize,
) -> Option<(Vec<(String, KtType)>, String)> {
    let plan = build_struct_plan(ext, registry, s, depth)?;
    factory_from_plan(&plan, prefix, class_name, imports)
}

/// Walk a [`StructPlan`] emitting the Kotlin `fromParts` side: the flattened
/// factory params and the reconstruct expression.
fn factory_from_plan(
    plan: &StructPlan,
    prefix: &str,
    class_name: &str,
    imports: &mut BTreeSet<String>,
) -> Option<(Vec<(String, KtType)>, String)> {
    let mut params: Vec<(String, KtType)> = Vec::new();
    let mut parts: Vec<String> = Vec::new();

    for f in &plan.fields {
        let camel = mangle_kotlin_ident(&kt_snake_to_camel(&f.fname.to_string()));
        let base = if prefix.is_empty() {
            camel.clone()
        } else {
            format!("{prefix}_{camel}")
        };
        let (p, part) = factory_field(&f.kind, &base, imports)?;
        params.extend(p);
        parts.push(part);
    }

    let reconstruct = format!("{class_name}({})", parts.join(", "));
    Some((params, reconstruct))
}

/// Declare the `fromParts` slots of ONE value position and the expression
/// that rebuilds it — the Kotlin mirror of
/// [`encode_field`](super::encode_field). A sum's payloads go through here
/// too, so a payload and a struct field of the same type declare the same
/// slot on both sides.
fn factory_field(
    kind: &PlanFieldKind,
    base: &str,
    imports: &mut BTreeSet<String>,
) -> Option<(Vec<(String, KtType)>, String)> {
    let mut params: Vec<(String, KtType)> = Vec::new();
    let mut parts: Vec<String> = Vec::new();
    let base = base.to_string();
    {
        let f_kind = kind;
        match f_kind {
            // Projection leaf (handle / `ULong`).
            PlanFieldKind::Projection { proj, fqn, .. } => {
                let short = register_fqn(fqn, imports);
                let (wire_ty, wrap) = factory_projection_wire_wrap(proj, &short, &base);
                params.push((base.clone(), wire_ty));
                parts.push(wrap);
            }
            // Enum leaf → `Int`, rebuilt via `Enum.fromInt(i)` (raw text: the
            // enum's short name, its FQN collected as a body import).
            PlanFieldKind::Enum { kotlin, .. } => {
                let short = register_fqn(kotlin.leaf_name()?, imports);
                params.push((base.clone(), KtType::int()));
                parts.push(format!("{short}.fromInt({base})"));
            }
            // `Option<enum>` leaf: the native encoder delivers the discriminant
            // `box_jint`-boxed (JVM `Ljava/lang/Integer;`, null for `None`), so
            // the factory takes `Int?` and rebuilds the nullable enum.
            PlanFieldKind::OptionEnum { kotlin, .. } => {
                let short = register_fqn(kotlin.leaf_name()?, imports);
                params.push((base.clone(), KtType::int().nullable()));
                parts.push(format!("{base}?.let {{ {short}.fromInt(it) }}"));
            }
            // Data-carrying enum — the tag slot plus every variant's group
            // side by side, rebuilt by an inlined `when` (no JNI crossing,
            // exactly like a nested data class).
            PlanFieldKind::Sum {
                kotlin_fqn,
                optional,
                variants,
                ..
            } => {
                let iface_short = register_fqn(kotlin_fqn, imports);
                let flag = format!("{base}__present");
                if *optional {
                    params.push((flag.clone(), KtType::boolean()));
                }
                params.push((format!("{base}__tag"), KtType::int()));

                let mut arms: Vec<String> = Vec::new();
                for (tag, v) in variants.iter().enumerate() {
                    // Each group's slots are declared once, in variant order,
                    // and are INERT whenever another variant is live — so the
                    // nullability rule below applies to every group, not just
                    // to an optional one.
                    let mut fwd: Vec<String> = Vec::new();
                    for pf in &v.fields {
                        let slot = format!("{base}_{}", pf.slot);
                        let (group_params, part) = factory_field(&pf.kind, &slot, imports)?;
                        fwd.push(nullable_group_part(&mut params, group_params, part));
                    }
                    let ctor = if v.fields.is_empty() {
                        format!("{iface_short}.{}", v.kotlin_name)
                    } else {
                        format!("{iface_short}.{}({})", v.kotlin_name, fwd.join(", "))
                    };
                    arms.push(format!("{tag} -> {ctor}"));
                }
                let when = format!(
                    "when ({base}__tag) {{ {}; else -> throw IllegalArgumentException(\"{}: \
                     invalid tag ${base}__tag\") }}",
                    arms.join("; "),
                    iface_short,
                );
                parts.push(if *optional {
                    format!("if ({flag}) {when} else null")
                } else {
                    when
                });
            }
            // Nested data-class field — inline its leaves and reconstruct via
            // the child's own `fromParts` (in bytecode, no JNI crossing).
            PlanFieldKind::Nested {
                optional,
                child_fqn,
                plan: child,
            } => {
                let child_fqn = child_fqn.as_ref()?;
                let child_short = register_fqn(child_fqn, imports);
                let (child_params, _child_reconstruct) =
                    factory_from_plan(child, &base, &child_short, imports)?;
                let child_names = child_params
                    .iter()
                    .map(|(n, _)| n.clone())
                    .collect::<Vec<_>>()
                    .join(", ");
                if !*optional {
                    params.extend(child_params);
                    parts.push(format!("{child_short}.fromParts({child_names})"));
                } else {
                    // `Option<nested>`: the parent receives default-null object wires
                    // for the child's leaves when absent (the native `None` arm), so
                    // every object-typed child param must be NULLABLE in the parent
                    // signature. Inside the `if (present)` guard the values are
                    // non-null again, so forward them to the child's (non-null)
                    // `fromParts` with `!!`. Primitive params (Long/Int/Boolean)
                    // can't be null and are forwarded as-is; already-nullable params
                    // stay nullable.
                    let flag = format!("{base}__present");
                    let mut fwd_names: Vec<String> = Vec::with_capacity(child_params.len());
                    params.push((flag.clone(), KtType::boolean()));
                    for (n, t) in &child_params {
                        if is_kotlin_primitive_ty(t) || t.is_nullable() {
                            params.push((n.clone(), t.clone()));
                            fwd_names.push(n.clone());
                        } else {
                            params.push((n.clone(), t.clone().nullable()));
                            fwd_names.push(format!("{n}!!"));
                        }
                    }
                    parts.push(format!(
                        "if ({flag}) {child_short}.fromParts({}) else null",
                        fwd_names.join(", ")
                    ));
                }
            }
            // Leaf primitive / object (string, byte array, Vec, …) — forwarded
            // unchanged to the constructor. Full-FQN param type; the
            // render-time `ImportSet` shortens it.
            PlanFieldKind::Leaf {
                kotlin, nullable, ..
            } => {
                let ty = if *nullable {
                    kotlin.clone().nullable()
                } else {
                    kotlin.clone()
                };
                params.push((base.clone(), ty));
                parts.push(base);
            }
        }
    }

    debug_assert_eq!(
        parts.len(),
        1,
        "factory_field must yield exactly one reconstruct expression"
    );
    Some((params, parts.remove(0)))
}

/// Declare one variant group's slots and return the expression that forwards
/// them into the variant constructor.
///
/// A group is **inert** whenever another variant is live, and the Rust
/// encoder fills an inert object slot with `JObject::null()`
/// ([`primitive_default_for_descriptor`](super::primitive_default_for_descriptor)).
/// A JVM `null` handed to a non-null Kotlin parameter throws inside the
/// intrinsic null-check, before any generated code runs — so every
/// object-shaped group slot must be declared nullable and forwarded `!!`
/// inside its own (live) arm, where it is non-null again. Primitives take
/// their `0`/`false` default and need no such treatment.
///
/// This is the N=1 `Option<nested>` rule (see the `Nested { optional }` arm)
/// generalized to N groups; both call it so the two paths cannot drift.
fn nullable_group_part(
    params: &mut Vec<(String, KtType)>,
    group_params: Vec<(String, KtType)>,
    part: String,
) -> String {
    let mut part = part;
    for (n, t) in group_params {
        if is_kotlin_primitive_ty(&t) || t.is_nullable() {
            params.push((n, t));
        } else {
            // The reconstruct expression references the slot by name; the
            // live arm re-asserts non-nullness there.
            part = replace_ident(&part, &n, &format!("{n}!!"));
            params.push((n, t.nullable()));
        }
    }
    part
}

/// Replace whole-identifier occurrences of `from` in a rendered Kotlin
/// expression. Whole-identifier: a match must not be flanked by identifier
/// characters, so rewriting `x` never touches `x_2` or `ax` — slot names of
/// one group share a prefix (`exact_v0`, `exact_v01`), and a substring
/// replace would corrupt them.
///
/// The left-hand character is taken from what has already been **written**,
/// never from the remaining input: `rest` advances past a rejected occurrence,
/// so by the next candidate the character preceding it can already have been
/// consumed. Re-slicing `rest` there loses it and turns two adjacent
/// occurrences (`"axx"`, rewriting `x`) into one accepted match.
fn replace_ident(haystack: &str, from: &str, to: &str) -> String {
    let is_ident_char = |c: char| c == '_' || c.is_alphanumeric();
    let mut out = String::with_capacity(haystack.len());
    let mut rest = haystack;
    while let Some(pos) = rest.find(from) {
        out.push_str(&rest[..pos]);
        let before_ok = out.chars().next_back().is_none_or(|c| !is_ident_char(c));
        let after = &rest[pos + from.len()..];
        let after_ok = after.chars().next().is_none_or(|c| !is_ident_char(c));
        if before_ok && after_ok {
            out.push_str(to);
        } else {
            out.push_str(from);
        }
        rest = after;
    }
    out.push_str(rest);
    out
}

/// Render the Kotlin `close()` expression for a handle `receiver` through
/// the folded [`FoldStrategy`] layers. Fresh lambda variable per nesting
/// level avoids `it` shadowing; the common single-layer cases are
/// special-cased for readable output (`x?.close()`, `x.forEach { it.close() }`).
pub(crate) fn render_handle_close(strategy: &crate::jni::FoldStrategy, receiver: &str) -> String {
    use prebindgen_registry::shape::Shape::*;
    fn go(strategy: &crate::jni::FoldStrategy, receiver: &str, depth: usize) -> String {
        match strategy {
            Base => format!("{receiver}.close()"),
            // The Kotlin-side receiver is already nullable (`handle_kt_type`
            // emits `T?` for both niche and boxed kinds), so `?.close()` covers
            // both wire representations.
            Optional(_, inner) => match &**inner {
                Base => format!("{receiver}?.close()"),
                _ => {
                    let v = format!("e{depth}");
                    format!("{receiver}?.let {{ {v} -> {} }}", go(inner, &v, depth + 1))
                }
            },
            Iterable(inner) => {
                let v = format!("e{depth}");
                format!(
                    "{receiver}.forEach {{ {v} -> {} }}",
                    go(inner, &v, depth + 1)
                )
            }
        }
    }
    go(strategy, receiver, 0)
}

/// Fold the projection wrap call `W(receiver)` through the
/// [`FoldStrategy`] layers:
/// * `Direct`         → `W(x)`
/// * `Nullable{Boxed}` → `x?.let { W(it) }` (JVM-null at the wire)
/// * `Nullable{Niche}` over a primitive wire (e.g. `jlong`) →
///   `x.let { if (it == <sentinel>) null else W(it) }`
/// * `Nullable{Niche}` over an object wire (e.g. `JByteArray`) →
///   `x?.let { W(it) }` (the wire is already a nullable reference)
/// * `Iterable`       → `x.map { W(it) }`
///
/// `niche_sentinel` is the Kotlin literal to compare against for the
/// `Niche+primitive` arm (e.g. `"0L"` for `jlong`-wired handles). When the
/// wire is object-shaped the sentinel is unused — `null` is the wire-level
/// representation and `?.let` is a no-cost null check.
pub(crate) fn fold_projection_wrap(
    strategy: &crate::jni::FoldStrategy,
    receiver: &str,
    kind: &crate::jni::ProjectionKind,
    wrap_class: &str,
    niche_sentinel: Option<&str>,
) -> String {
    use prebindgen_registry::shape::Shape::*;

    use crate::jni::NullableKind;
    fn go(
        s: &crate::jni::FoldStrategy,
        r: &str,
        kind: &crate::jni::ProjectionKind,
        w: &str,
        sentinel: Option<&str>,
        depth: usize,
    ) -> String {
        match s {
            Base => projection_wrap_expr(kind, w, r),
            Optional(nullable_kind, inner) => match (nullable_kind, &**inner) {
                // Primitive-wired niche → can't carry null on the wire, so
                // compare against the sentinel and synthesize null on the
                // Kotlin side.
                (NullableKind::Niche, Base) if sentinel.is_some() => {
                    let s = sentinel.unwrap();
                    let wrapped = projection_wrap_expr(kind, w, "it");
                    format!("{r}.let {{ if (it == {s}) null else {wrapped} }}")
                }
                // Object-wired niche or fully boxed Nullable → `?.let { W(it) }`.
                (_, Base) => {
                    let wrapped = projection_wrap_expr(kind, w, "it");
                    format!("{r}?.let {{ {wrapped} }}")
                }
                // Deeper nesting. The niche/boxed distinction is only
                // observable at the outermost layer covering a `Direct`
                // leaf; intermediate layers (nullable-of-iterable etc.)
                // can keep the simple form because Kotlin's `?.` chain
                // already represents the layered null.
                _ => {
                    let v = format!("e{depth}");
                    format!(
                        "{r}?.let {{ {v} -> {} }}",
                        go(inner, &v, kind, w, sentinel, depth + 1)
                    )
                }
            },
            Iterable(inner) => match &**inner {
                Base => {
                    let wrapped = projection_wrap_expr(kind, w, "it");
                    format!("{r}.map {{ {wrapped} }}")
                }
                _ => {
                    let v = format!("e{depth}");
                    format!(
                        "{r}.map {{ {v} -> {} }}",
                        go(inner, &v, kind, w, sentinel, depth + 1)
                    )
                }
            },
        }
    }
    go(strategy, receiver, kind, wrap_class, niche_sentinel, 0)
}

/// JNI extern's declared Kotlin wire-return for a projection. The leaf wire
/// is the inner converter's destination Kotlin name — `Long` for both
/// projection kinds (a handle's pointer, a `ULong`'s raw bit pattern). The
/// fold honours
/// [`NullableKind`] so the declared wire matches the runtime ABI:
/// `Niche+primitive` keeps the layer non-nullable on the wire (the sentinel
/// represents null); `Niche+object` and `Boxed` add `?`.
pub(crate) fn projection_wire_return(proj: &crate::jni::Projection) -> KtType {
    use crate::jni::{FoldStrategy, NullableKind, ProjectionKind};
    let (inner_wire, inner_is_primitive) = match proj.kind {
        ProjectionKind::Handle => (KtType::long(), true),
        ProjectionKind::Unsigned64 => (KtType::long(), true),
    };
    fold_shape(
        &proj.strategy,
        &|| inner_wire.clone(),
        &|inner, kind, inner_strategy| {
            // A niche layer over a primitive wire keeps the wire non-nullable —
            // the sentinel value is the null representation. Object-wired niches
            // and full-boxed Nullables both add `?` (JVM null on the reference).
            match (kind, inner_strategy) {
                (NullableKind::Niche, FoldStrategy::Base) if inner_is_primitive => inner,
                _ => inner.nullable(),
            }
        },
        &|inner| KtType::generic("List", [inner]),
    )
}

/// Kotlin null-sentinel literal for the *leaf wire* of a projection. Read
/// at the wrapper-body call site and forwarded to [`fold_projection_wrap`];
/// `None` when the leaf wire has no primitive null sentinel, where
/// `?.let { }` covers the JVM-null case directly.
pub(crate) fn projection_leaf_sentinel(proj: &crate::jni::Projection) -> Option<String> {
    if let Some(sentinel) = proj.niche_sentinels.first() {
        return Some(sentinel.clone());
    }
    use crate::jni::ProjectionKind;
    let leaf_wire: syn::Type = match proj.kind {
        ProjectionKind::Handle => syn::parse_quote!(jni::sys::jlong),
        // No niche exists for `u64`; `Option<u64>` uses the boxed path, so a
        // primitive sentinel must never be synthesized.
        ProjectionKind::Unsigned64 => return None,
    };
    kotlin_null_sentinel(&leaf_wire).map(|s| s.to_string())
}

/// The sentinel a **wrap** should test, given the projection and whether an
/// ancestor makes the leaf nullable — the one rule both derivations of that
/// wrap read (`unfold_leaf_kt` in `render.rs`, `leaf_iface_param` in
/// `iface.rs`), so it cannot drift between them again (#142).
///
/// A sentinel is the leaf's **own** `None` representation, so it belongs to the
/// leaf's own type and to nothing above it. Two independent facts meet here and
/// only the first one answers:
///
/// * the leaf's type carries a niche — `Option<Duration>` over a bounded
///   `convert!`, whose strategy is `Optional(Niche, _)`. Its `None` IS the
///   sentinel, and the test stays whatever the ancestor does.
/// * an **ancestor** can be absent (a conditional value form, an
///   `Option<sum>`/`Option<nested>` field). That widens the wire — the Rust
///   side boxes any nullable leaf, see
///   [`leaf_is_prim`](crate::jni::emit::leaf_is_prim) — and `?.` alone carries
///   it. It grants no sentinel.
///
/// [`projection_leaf_sentinel`] answers off `niche_sentinels`, which
/// `attach_domain_sentinels` puts on the **bare** type's converter as well as
/// the `Option` one. Asking it without the `Base` check therefore handed a
/// sentinel to a leaf that has no niche encoding at all, splicing
/// `?.let { if (it == -1L) … }` into a wrap whose own encoder can never emit
/// `-1`. Harmless at runtime — the value is outside the declared range — and
/// wrong on its face.
pub(crate) fn wrap_sentinel(proj: &crate::jni::Projection, nullable: bool) -> Option<String> {
    // `Base` means the leaf has no `Option` of its own: whatever absence it can
    // express is the ancestor's, and `?.` already expresses that.
    if nullable && matches!(proj.strategy, crate::jni::FoldStrategy::Base) {
        return None;
    }
    projection_leaf_sentinel(proj)
}

/// Kotlin literal for the null-sentinel of a primitive wire — used by
/// [`fold_projection_wrap`] when a `Niche` layer covers a primitive wire and
/// can't carry JVM null. Mirrors `jni_field_access`'s primitive descriptors.
/// Returns `None` for object-shaped wires (where JVM null *is* the null
/// representation and `?.let` is the right pattern).
pub(crate) fn kotlin_null_sentinel(wire: &syn::Type) -> Option<&'static str> {
    let (_, _, is_object) = crate::jni::wire_access::jni_field_access(wire)?;
    if is_object {
        return None;
    }
    let syn::Type::Path(tp) = wire else {
        return None;
    };
    let last = tp.path.segments.last()?;
    Some(match last.ident.to_string().as_str() {
        "jlong" => "0L",
        "jint" | "jshort" | "jbyte" | "jchar" => "0",
        "jfloat" => "0.0f",
        "jdouble" => "0.0",
        "jboolean" => "false",
        _ => return None,
    })
}

/// Shorten a class FQN to its simple name for use in **raw body text** (a
/// `fromParts` reconstruct fragment: `Child.fromParts(…)`, `Enum.fromInt(…)`,
/// `ZenohId(bytes)`), registering the FQN into `used` — the body `Code`'s own
/// import list. A non-dotted name (a Kotlin builtin like `String`) needs no
/// import and passes through. Signature types are NOT shortened this way —
/// those are full-FQN `KtType`s in the AST, shortened + imported by the
/// render-time `ImportSet`.
pub(crate) fn register_fqn(fqn: &str, used: &mut BTreeSet<String>) -> String {
    if fqn.contains('.') {
        used.insert(fqn.to_string());
        fqn.rsplit('.').next().unwrap_or(fqn).to_string()
    } else {
        fqn.to_string()
    }
}

#[cfg(test)]
mod replace_ident_tests {
    use super::replace_ident;

    /// The property the function exists for: a slot name is rewritten only as a
    /// whole identifier, so the `!!` never lands on a longer name that merely
    /// starts (or ends) with it.
    #[test]
    fn only_whole_identifiers_are_rewritten() {
        assert_eq!(replace_ident("x", "x", "x!!"), "x!!");
        assert_eq!(replace_ident("x_2", "x", "x!!"), "x_2");
        assert_eq!(replace_ident("ax", "x", "x!!"), "ax");
        assert_eq!(
            replace_ident("Reading.Exact(exact_v0)", "exact_v0", "exact_v0!!"),
            "Reading.Exact(exact_v0!!)"
        );
        // A prefix of a longer slot name of the same group is left alone.
        assert_eq!(
            replace_ident("f(exact_v0, exact_v01)", "exact_v0", "exact_v0!!"),
            "f(exact_v0!!, exact_v01)"
        );
    }

    /// A rejected occurrence must not consume the context of the next one: with
    /// the left character re-sliced out of the remaining input, `"axx"` rewrote
    /// its second `x` — the first having been dropped from `rest` along with the
    /// `a` that disqualified it.
    #[test]
    fn a_rejected_match_keeps_its_left_context() {
        assert_eq!(replace_ident("axx", "x", "x!!"), "axx");
        assert_eq!(replace_ident("xxx", "x", "x!!"), "xxx");
        assert_eq!(replace_ident("x ax", "x", "x!!"), "x!! ax");
        // Every occurrence of a genuinely standalone identifier is rewritten.
        assert_eq!(replace_ident("x + x", "x", "x!!"), "x!! + x!!");
    }
}