Skip to main content

zerodds_lang_rules/emit/
kotlin.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! The Kotlin backend of the shared codec walk.
4//!
5//! DDS-XTypes 1.3 §7.4 from the clause, the same as every other column,
6//! and none of them calls another. Kotlin shares the JVM's value model
7//! with [`crate::emit::java`] — no unsigned types, no pointers, a
8//! decoder answers with its value rather than writing through an
9//! out-parameter — so most of this file repeats that column's answer.
10//! What is Kotlin's own:
11//!
12//! # No exceptions to declare, and no unused-parameter statement either
13//!
14//! Kotlin has no checked exceptions (Kotlin language specification,
15//! "Exceptions"), so a fallible runtime call is written exactly as a
16//! plain one — [`Kotlin::call`] and [`Kotlin::runtime`] add nothing —
17//! and [`kotlin::FuncDecl`] carries no `throws`. [`Kotlin::mark_used`]
18//! answers with nothing for the same reason
19//! [`crate::emit::java::Java::mark_used`] does: `kotlinc` raises no
20//! error over an unread parameter, so there is nothing to suppress.
21//!
22//! # Every numeric conversion is written out
23//!
24//! Kotlin performs **no** implicit numeric widening at all — not even
25//! `Int` to `Long` (Kotlin language specification, "Integer types":
26//! "there are no implicit widening conversions for numbers"), which is
27//! narrower than Java's own rule and the reverse of C's. `readI8()`
28//! answers an `Int` and a `Byte` field needs `.toByte()` explicitly, so
29//! the shared walk's every `cast` has to reach the source here where
30//! `emit/java.rs` could let the compiler do it.
31//!
32//! The bookkeeping runs the other way all the same. `kotlinc -Werror`
33//! fails a build over "redundant call of conversion method" wherever a
34//! conversion is called on a value of the target type already —
35//! `a.toInt()` for an `Int` `a` is measured, exactly as `javac`'s
36//! `-Xlint:cast` fails a redundant cast — so [`Kotlin::cast`] does skip
37//! one, and `Kotlin::already` is the small type oracle that decides
38//! when. That oracle is the whole of this backend's type knowledge: the
39//! walk carries no symbol table, so what can be recognised is a literal,
40//! an array's `.size`, a runtime call whose return type
41//! `runtime/kotlin/ZdWire.kt` fixes, and the walk's own locals.
42//!
43//! # `Array(n) { T() }` needs no separate pre-fill loop
44//!
45//! `emit/java.rs`'s `make_room` allocates `new T[n]` — every reference
46//! slot `null` — and then loops constructing an instance at each slot
47//! only where the fill does not assign the slot outright (a `map`'s two
48//! halves, not a sequence's decoded elements). Kotlin's `Array(n) { T() }`
49//! constructor puts a real instance in every slot the moment the array
50//! is allocated (Kotlin language specification, "Array types"), so
51//! [`Kotlin::make_room`] needs no second pass and ignores
52//! `assigned_whole` outright — the one place this column's `make_room`
53//! is *shorter* than Java's rather than a plain translation of it.
54//!
55//! # No implicit bitwise operators
56//!
57//! Kotlin has no infix `&`, `|`, `^`, `<<`, `>>` or prefix `~` at all;
58//! `and`, `or`, `xor`, `shl`, `shr` and `.inv()` stand in (Kotlin
59//! language specification, "Bitwise operations"). The shared walk's own
60//! [`CodecTarget::binary`]/[`CodecTarget::unary`] never reach one of
61//! these — `crates/lang-rules/src/emit/{encode,decode}.rs` name only
62//! `&&`, `||`, `+`, `=`, `==`, `<`, `>`, `!` and `~`, all of which
63//! Kotlin already spells the same way except the last, which
64//! [`Kotlin::unary`] answers with `.inv()`.
65//!
66//! # A mutable property never smart-casts
67//!
68//! [`Kotlin::optional_value`] reaches an `@optional` member's value only
69//! inside a branch [`CodecTarget::present`] has already guarded, and the
70//! member is a `var` — the smart cast from `T?` to `T` that guard would
71//! otherwise earn does not apply to a mutable property (Kotlin language
72//! specification, "Smart casts": "a property... must be `val`... and
73//! must not have a custom getter"), so the value is asserted with `!!`
74//! rather than read bare.
75
76use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
77use crate::adapter::kotlin::{
78    kotlin_map_entry_name, kotlin_value_type, property_name, type_tag, unsigned_holder,
79};
80use crate::encode::EncodeError;
81use zerodds_ast::{Ast, MemberNode, TypeNode};
82use zerodds_ast_core::Id;
83use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
84use zerodds_lang_cst::kotlin;
85
86/// The Kotlin backend.
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct Kotlin;
89
90/// The type name [`CodecTarget::named`] is handed for a map key's signed
91/// reinterpretation; the member itself is `Int` and the ordering read is
92/// `(v as Byte)`... spelled here as `.toByte()`.
93const SIGNED_CHAR: &str = "signed char";
94
95impl Kotlin {
96    /// `place = Array(count) { start }` with `init` as each slot's start
97    /// value, or the element type's own where it is `None`.
98    fn allocate(
99        base: &str,
100        element: kotlin::Type,
101        count: kotlin::Expr,
102        init: Option<kotlin::Expr>,
103        fill: Vec<kotlin::Stmt>,
104    ) -> Vec<kotlin::Stmt> {
105        let length = Self::to_array_size(count);
106        let mut out = vec![kotlin::Stmt::Expr(kotlin::Expr::Binary {
107            op: "=",
108            lhs: Box::new(<Self as CodecTarget>::whole(base)),
109            rhs: Box::new(kotlin::Expr::NewArray {
110                element: Box::new(element),
111                length: Box::new(length),
112                init: init.map(Box::new),
113            }),
114        })];
115        out.extend(fill);
116        out
117    }
118
119    /// `write_u32` as `writeU32` — the runtime's own convention, and the
120    /// walk's spelling is the wire's.
121    fn camel(method: &str) -> String {
122        let mut out = String::new();
123        let mut rising = false;
124        for ch in method.chars() {
125            if ch == '_' {
126                rising = true;
127                continue;
128            }
129            if rising {
130                out.extend(ch.to_uppercase());
131                rising = false;
132            } else {
133                out.push(ch);
134            }
135        }
136        out
137    }
138
139    /// A value, parenthesised first when it is not already a single
140    /// token — `(a + b).toInt()` against `5L.inv()`. Kotlin's lexer
141    /// reads a bare numeric literal directly followed by `.name` without
142    /// ambiguity (measured: `5L.inv()` and `5.inv()` both parse), so a
143    /// literal or a name needs no bracket; anything built from an
144    /// operator does, the same rule [`CodecTarget::paren`] exists for.
145    fn atom(value: kotlin::Expr) -> kotlin::Expr {
146        match value {
147            // A negative number is not one token: Kotlin has no negative
148            // literal, only unary minus in front of one (Kotlin language
149            // specification, "Integer literals"), and a call binds
150            // tighter than the minus. `-3L.inv()` is `-(3L.inv())`,
151            // which compiles and is a different mask; every declared-bit
152            // check over a bitmask with bit 63 set was reading it.
153            kotlin::Expr::Literal(Literal::Int(ref text) | Literal::Float(ref text))
154                if text.starts_with('-') =>
155            {
156                kotlin::Expr::Paren(Box::new(value))
157            }
158            kotlin::Expr::Literal(_)
159            | kotlin::Expr::Path(_)
160            | kotlin::Expr::Call { .. }
161            | kotlin::Expr::Field { .. }
162            | kotlin::Expr::Index { .. }
163            | kotlin::Expr::New { .. }
164            | kotlin::Expr::NewArray { .. }
165            | kotlin::Expr::ArrayOf { .. }
166            | kotlin::Expr::Paren(_)
167            | kotlin::Expr::NotNull(_) => value,
168            other => kotlin::Expr::Paren(Box::new(other)),
169        }
170    }
171
172    /// A conversion method call: `value.toInt()`, `value.toByte()`, …
173    fn convert(value: kotlin::Expr, method: &str) -> kotlin::Expr {
174        kotlin::Expr::method_call(Self::atom(value), method, Vec::new())
175    }
176
177    /// `value` narrowed to `Int`, skipping the conversion where `value`
178    /// is already one — `kotlinc -Werror` fails a build over
179    /// "redundant call of conversion method" the way `-Xlint:cast`
180    /// fails Java's, and this is the one place that lint is reachable:
181    /// [`Kotlin::count_of`] and a decoded sequence length both flow into
182    /// [`Kotlin::count_up`], [`Kotlin::make_room`] and
183    /// [`Kotlin::keep_first`], and only the second of the two is ever
184    /// not already `Int`. The two shapes [`Kotlin::count_of`] can answer
185    /// with — a bare `Int` literal and a bound's own `.size` — are
186    /// recognised directly; anything else is a `Long` count read off the
187    /// wire and gets `.toInt()`.
188    fn to_array_size(value: kotlin::Expr) -> kotlin::Expr {
189        Self::cast(kotlin::Type::Primitive(kotlin::Primitive::Int), value)
190    }
191
192    /// The reverse of [`Kotlin::to_array_size`]: widen `value` to `Long`
193    /// where [`Kotlin::already`] recognises it as one of
194    /// [`Kotlin::count_of`]'s two `Int` shapes, and leave everything
195    /// else alone. Widening on the other rule — everything not
196    /// recognisably a `Long` — put `.toLong()` on a genuine
197    /// `unsigned long` member, which is already one, and
198    /// `kotlinc -Werror` refuses that as a redundant conversion.
199    fn widen_count(value: kotlin::Expr) -> kotlin::Expr {
200        if !Self::already(&kotlin::Type::Primitive(kotlin::Primitive::Int), &value) {
201            return value;
202        }
203        match value {
204            kotlin::Expr::Literal(_) => Self::suffix_long(value),
205            other => Self::cast(kotlin::Type::Primitive(kotlin::Primitive::Long), other),
206        }
207    }
208
209    /// A shift distance as the `Int` Kotlin's shift functions declare.
210    /// A literal loses its `L` rather than earning a `.toInt()`: the
211    /// distance is a small number the walk fixed, and a conversion
212    /// around a constant is noise in every generated bitset.
213    fn narrow_shift(value: kotlin::Expr) -> kotlin::Expr {
214        match value {
215            kotlin::Expr::Literal(Literal::Int(text)) if text.ends_with('L') => {
216                kotlin::Expr::Literal(Literal::Int(text.trim_end_matches('L').to_string()))
217            }
218            other => Self::cast(kotlin::Type::Primitive(kotlin::Primitive::Int), other),
219        }
220    }
221
222    /// Whether the bit bound this call names puts its value in a `Long`
223    /// — the boundary `adapter/kotlin.rs::unsigned_holder` draws.
224    fn wide_bit_bound(bound: Option<&kotlin::Expr>) -> bool {
225        match bound {
226            Some(kotlin::Expr::Literal(Literal::Int(text))) => text
227                .trim_end_matches('L')
228                .parse::<u16>()
229                .is_ok_and(|bits| unsigned_holder(bits) == kotlin::Primitive::Long),
230            _ => false,
231        }
232    }
233
234    /// `x.toLong()` back to `x`, and anything else unchanged.
235    fn drop_to_long(value: kotlin::Expr) -> kotlin::Expr {
236        match value {
237            kotlin::Expr::Call {
238                receiver: Some(inner),
239                ref name,
240                ref args,
241            } if name.as_str() == "toLong" && args.is_empty() => *inner,
242            other => other,
243        }
244    }
245
246    /// Whether `value` is one of the walk's own locals. The shared walk
247    /// spells every local it introduces with the `zd_` prefix an IDL
248    /// identifier cannot carry — `crates/lang-rules/src/emit/decode.rs`
249    /// names the headers `zd_p` and `zd_h` there — so this separates a
250    /// field of the runtime's own header from a field of the sample.
251    fn walk_local(value: &kotlin::Expr) -> bool {
252        match value {
253            kotlin::Expr::Path(path) => match path.segments() {
254                [single] => single.as_str().starts_with("zd_"),
255                _ => false,
256            },
257            _ => false,
258        }
259    }
260
261    /// The same integer literal spelled as a `Long`, and anything else
262    /// unchanged. A suffix costs nothing where the value already fits an
263    /// `Int`; a conversion method would be the redundant call
264    /// `kotlinc -Werror` refuses.
265    fn suffix_long(value: kotlin::Expr) -> kotlin::Expr {
266        match value {
267            kotlin::Expr::Literal(Literal::Int(text))
268                if text.parse::<i128>().is_ok() && !text.ends_with('L') =>
269            {
270                kotlin::Expr::Literal(Literal::Int(format!("{text}L")))
271            }
272            other => other,
273        }
274    }
275
276    /// Whether `value`'s own static type is already `type_`, for the
277    /// closed set of shapes this backend can tell without a general type
278    /// checker. [`Kotlin::cast`]'s documentation says why this exists at
279    /// all; extending it to a shape not listed here is safe in the
280    /// direction of *not* recognising it — the conversion is then
281    /// written out where Kotlin would have accepted it bare, which
282    /// compiles, and never in the direction of recognising a shape whose
283    /// type is not actually `type_`, which would drop a needed
284    /// conversion silently.
285    fn already(type_: &kotlin::Type, value: &kotlin::Expr) -> bool {
286        use kotlin::{Primitive as P, Type};
287        match value {
288            kotlin::Expr::Paren(inner) => Self::already(type_, inner),
289            kotlin::Expr::Literal(Literal::Int(text)) => {
290                // `Long.MIN_VALUE` carries no suffix and is a `Long` all
291                // the same; `zerodds_lang_render::kotlin::long_text` says
292                // why it is spelled that way and not as a literal.
293                let is_long = text.ends_with('L') || text == "Long.MIN_VALUE";
294                matches!(type_, Type::Primitive(P::Long)) == is_long
295                    && matches!(type_, Type::Primitive(P::Long) | Type::Primitive(P::Int))
296            }
297            // `count_of`'s unbounded answer: a Kotlin array's `.size` is
298            // always `Int`.
299            kotlin::Expr::Field { name, .. } if name.as_str() == "size" => {
300                *type_ == Type::Primitive(P::Int)
301            }
302            // A field of a header the reader handed back, whose type
303            // `runtime/kotlin/ZdWire.kt` fixes the same way it fixes a
304            // read method's. `PidRead.length` and `EmHeaderRead.extent`
305            // are octet counts a 32-bit NEXTINT can fill to the top —
306            // DDS-XTypes 1.3 §7.4.3.4.2 — so the runtime holds both in a
307            // `Long`. The receiver has to be one of the walk's own
308            // header locals: an IDL member may itself be called
309            // `length`, and reading that one as a `Long` would drop the
310            // conversion it needs.
311            kotlin::Expr::Field { value, name }
312                if matches!(name.as_str(), "length" | "extent") && Self::walk_local(value) =>
313            {
314                *type_ == Type::Primitive(P::Long)
315            }
316            // `zd_bits` is the bitset anchor and the bitmask value the
317            // walk declares for itself, and it declares both as
318            // `Scalar::UInt64` — `crates/lang-rules/src/emit/decode.rs`,
319            // where the name is written — which is a `Long` here.
320            kotlin::Expr::Path(path) if matches!(path.segments(), [single] if single.as_str() == "zd_bits") => {
321                *type_ == Type::Primitive(P::Long)
322            }
323            // Kotlin's bitwise functions answer in the type of the value
324            // they were called on: `Long.and(Long)` and `Long.ushr(Int)`
325            // are both `Long` (Kotlin language specification, "Bitwise
326            // operations"). The left operand therefore decides, which is
327            // how the anchor's width reaches the end of a field's
328            // extraction.
329            kotlin::Expr::Binary { op, lhs, .. }
330                if matches!(*op, "and" | "or" | "ushr" | "shl") =>
331            {
332                Self::already(type_, lhs)
333            }
334            // A runtime read, whose return type `runtime/kotlin/ZdWire.kt`
335            // fixes — the method name alone decides it, and its
336            // arguments (`readWchar`'s framing and order among them) do
337            // not change what it returns. A conversion method
338            // [`Kotlin::cast`] itself already wrote — `.toChar()` and
339            // kin — is the same kind of fact about its own result, and
340            // recognising it here is what keeps
341            // `crates/lang-rules/src/emit/decode.rs`'s own `cast` around
342            // a `char`/`wchar` read from earning a second one from
343            // [`Kotlin::store`] on top of it.
344            kotlin::Expr::Call {
345                name,
346                receiver: Some(_),
347                ..
348            } => {
349                let returns = match name.as_str() {
350                    "readBool" => Type::Primitive(P::Boolean),
351                    "readU8" | "readI8" | "readChar" | "readI16" | "readU16" | "readI32"
352                    | "readWchar" | "toInt" => Type::Primitive(P::Int),
353                    "readU32" | "readI64" | "readU64" | "readEnum" | "readBitmask"
354                    | "readBitset" | "toLong" => Type::Primitive(P::Long),
355                    "readF32" | "toFloat" => Type::Primitive(P::Float),
356                    "readF64" | "toDouble" => Type::Primitive(P::Double),
357                    "readString" | "readStringAlloc" | "readFixed" => Type::named("String"),
358                    "readF128" => Type::named("Float128"),
359                    "toByte" => Type::Primitive(P::Byte),
360                    "toShort" => Type::Primitive(P::Short),
361                    "toChar" => Type::Primitive(P::Char),
362                    _ => return false,
363                };
364                returns == *type_
365            }
366            _ => false,
367        }
368    }
369
370    fn reader_body(name: &str, sample: kotlin::Type, body: Vec<kotlin::Stmt>) -> Vec<kotlin::Stmt> {
371        let defaulter = crate::default_kotlin::defaulter_from_decoder(name);
372        let mut statements = vec![kotlin::Stmt::Declare {
373            mutable: true,
374            type_: sample,
375            name: Ident::new("v"),
376            value: Some(Self::call(&defaulter, Vec::new())),
377        }];
378        statements.extend(body);
379        statements.push(kotlin::Stmt::Return(Some(Self::whole("v"))));
380        statements
381    }
382}
383
384impl CodecTarget for Kotlin {
385    type Expr = kotlin::Expr;
386    type Stmt = kotlin::Stmt;
387    type Block = Vec<kotlin::Stmt>;
388    type Type = kotlin::Type;
389    type Decl = kotlin::Decl;
390
391    // -- types --------------------------------------------------------
392
393    /// `octet`, `unsigned short` and `wchar` widen to `Int`; `long long`
394    /// and `unsigned long` widen to `Long`. No unsigned type exists on
395    /// the JVM, so every wider-than-declared holder carries a **pattern**
396    /// and not a number past its own width — the same three-place
397    /// agreement `adapter/kotlin.rs` and `runtime/kotlin/ZdWire.kt`
398    /// share.
399    fn scalar(kind: Scalar) -> Self::Type {
400        use kotlin::Primitive as P;
401        kotlin::Type::Primitive(match kind {
402            Scalar::Bool => P::Boolean,
403            Scalar::Char => P::Char,
404            Scalar::Int8 => P::Byte,
405            Scalar::Int16 => P::Short,
406            Scalar::Index | Scalar::Size | Scalar::Int32 | Scalar::UInt8 | Scalar::UInt16 => P::Int,
407            Scalar::Int64 | Scalar::UInt32 | Scalar::UInt64 => P::Long,
408            Scalar::Float => P::Float,
409            Scalar::Double => P::Double,
410            // binary128 has no JVM primitive; the runtime carries it as
411            // a pair of `Long`s behind `Float128`.
412            Scalar::LongDouble => return kotlin::Type::named("Float128"),
413        })
414    }
415
416    fn named(name: &str) -> Self::Type {
417        kotlin::Type::named(name)
418    }
419
420    fn nothing() -> Self::Type {
421        kotlin::Type::Unit
422    }
423
424    /// The runtime's handles, all top-level declarations of
425    /// `runtime/kotlin/ZdWire.kt` — a `.kt` file needs no wrapper class
426    /// to nest them in the way `runtime/java`'s does, so the unqualified
427    /// name resolves once the file's `import zerodds.wire.*` is written.
428    fn handle(kind: Handle) -> Self::Type {
429        kotlin::Type::named(match kind {
430            Handle::DHeader => "DHeader",
431            Handle::EmHeader => "EmHeader",
432            Handle::Pid => "Pid",
433            Handle::TryConstruct => "TryConstruct",
434            Handle::ReaderMark => "ReaderMark",
435            Handle::PidRead => "PidRead",
436            Handle::EmHeaderRead => "EmHeaderRead",
437        })
438    }
439
440    /// A `char` member is a Kotlin `Char` and the runtime's parameter is
441    /// an `Int` code point, so the narrow form reads `.code`
442    /// (Kotlin language specification, "Char"); a `wchar` member is
443    /// already an `Int` — `adapter/kotlin.rs`'s memory-shape table says
444    /// so — and needs nothing.
445    fn code_point(value: Self::Expr, narrow: bool) -> Self::Expr {
446        if narrow {
447            return kotlin::Expr::field(value, "code");
448        }
449        value
450    }
451
452    /// `java.lang.Long.compareUnsigned` for the eight-octet width, whose
453    /// value lives in a `Long` as its bit pattern and whose own `<` is
454    /// signed. Kotlin declares no unsigned comparison of its own that
455    /// reaches a plain `Long`, so the JVM's own static method is called
456    /// by its fully qualified name — legal without an import (Kotlin
457    /// language specification, "Package and imports"). The narrower
458    /// widths live zero-extended in a wider signed holder, where the
459    /// inherited `<` is already the unsigned order.
460    fn unsigned_less(octets: u8, left: Self::Expr, right: Self::Expr) -> Self::Expr {
461        if octets < 8 {
462            return Self::binary("<", left, right);
463        }
464        Self::binary(
465            "<",
466            kotlin::Expr::Call {
467                receiver: Some(Box::new(kotlin::Expr::Path(Path::new(&[
468                    "java", "lang", "Long",
469                ])))),
470                name: Ident::new("compareUnsigned"),
471                args: vec![left, right],
472            },
473            Self::int(0),
474        )
475    }
476
477    /// `!left && right` — `false` before `true`. Kotlin's `<` refuses a
478    /// pair of `Boolean`s outright, the same class of target
479    /// [`CodecTarget::bool_less`]'s default is documented for.
480    fn bool_less(left: Self::Expr, right: Self::Expr) -> Self::Expr {
481        Self::binary("&&", kotlin::Expr::Not(Box::new(left)), right)
482    }
483
484    /// `runtime/kotlin/ZdWire.kt` publishes each as an `enum class`
485    /// constant — one type per policy — so each name carries its own.
486    ///
487    /// Qualified with the runtime's package: an IDL type of the same name
488    /// is declared in the generated file and wins over the star import.
489    /// `enum Status { OK, WARN, FAIL }` (`enum-sparse-values`) turned
490    /// `Status.DISCARDED` into an unresolved reference and
491    /// `readerStatus() != Status.OK` into a comparison with an `Int`.
492    fn constant(which: Constant) -> Self::Expr {
493        let qualified = |name: &str| format!("zerodds.wire.{name}");
494        kotlin::Expr::Path(Path::new(
495            &qualified(match which {
496                Constant::WcharWidth2 => "WcharWidth.OCTETS_2",
497                Constant::WcharWidth4 => "WcharWidth.OCTETS_4",
498                Constant::WcharFixed => "WcharFraming.FIXED",
499                Constant::WcharGiopLengthPrefixed => "WcharFraming.GIOP_LENGTH_PREFIXED",
500                Constant::WstringUnitsStream => "WstringUnitOrder.STREAM",
501                Constant::WstringUnitsBigEndianAlways => "WstringUnitOrder.BIG_ENDIAN_ALWAYS",
502                Constant::WstringUnitsByteOrderMark => "WstringUnitOrder.BYTE_ORDER_MARK",
503                Constant::WstringUnitsHostUnswapped => "WstringUnitOrder.HOST_UNSWAPPED",
504                Constant::WstringSpecification => "WstringLengthPolicy.SPECIFICATION",
505                Constant::WstringCharacterCount => "WstringLengthPolicy.CHARACTER_COUNT",
506                Constant::WstringCharacterCountTerminated => {
507                    "WstringLengthPolicy.CHARACTER_COUNT_TERMINATED"
508                }
509                Constant::WstringSpecificationWideUnits => {
510                    "WstringLengthPolicy.SPECIFICATION_WIDE_UNITS"
511                }
512                Constant::WstringRosTypeSupport => "WstringLengthPolicy.ROS_TYPE_SUPPORT",
513                Constant::WstringOctetsTerminated2 => "WstringLengthPolicy.OCTETS_TERMINATED_2",
514                Constant::WstringOctetsTerminated4 => "WstringLengthPolicy.OCTETS_TERMINATED_4",
515                Constant::WstringCharactersTerminated2 => {
516                    "WstringLengthPolicy.CHARACTERS_TERMINATED_2"
517                }
518                Constant::FixedDeclared => "FixedDigitCount.DECLARED",
519                Constant::FixedValuePrecision => "FixedDigitCount.VALUE_PRECISION",
520                Constant::FixedValueStringDigits => "FixedDigitCount.VALUE_STRING_DIGITS",
521                Constant::ParameterMember => "ParameterRole.MEMBER",
522                Constant::ParameterOptional => "ParameterRole.OPTIONAL",
523                Constant::TryConstructDiscard => "TryConstruct.DISCARD",
524                Constant::TryConstructTrim => "TryConstruct.TRIM",
525                Constant::TryConstructUseDefault => "TryConstruct.USE_DEFAULT",
526                Constant::StatusOk => "Status.OK",
527                Constant::StatusDiscarded => "Status.DISCARDED",
528                Constant::StatusImpossibleLength => "Status.IMPOSSIBLE_LENGTH",
529                Constant::StatusOutOfRange => "Status.OUT_OF_RANGE",
530                Constant::StatusTruncated => "Status.TRUNCATED",
531            })
532            .split('.')
533            .collect::<Vec<_>>(),
534        ))
535    }
536
537    /// The materialiser's answer, not a second reading of it: a
538    /// `@bit_bound` set is assembled in `Int` up to 32 bits and `Long`
539    /// past it, exactly what a bitmask member of the same bound is held
540    /// in — `adapter/kotlin.rs::unsigned_holder`.
541    fn unsigned_holder(bits: u16) -> Self::Type {
542        kotlin::Type::Primitive(unsigned_holder(bits))
543    }
544
545    /// Every free function `runtime/kotlin/ZdWire.kt` publishes at the
546    /// top level — `noDheader`, `bitPatternInt` and kin — is called
547    /// unqualified; there is no second namespace to route between the
548    /// way [`crate::emit::java::Java::runtime_static`] routes `Writer`
549    /// against `Reader`.
550    /// A free function of the runtime, unqualified — except
551    /// `pid_is_user_member`, which `runtime/kotlin/ZdWire.kt` publishes
552    /// as a method on [`PidRead`](kotlin::Type) itself rather than as a
553    /// free function of its own, the same shape [`crate::emit::zig::Zig`]
554    /// reaches it through.
555    fn runtime_static(method: &str, mut args: Vec<Self::Expr>) -> Self::Expr {
556        if method == "pid_is_user_member" && !args.is_empty() {
557            let header = args.remove(0);
558            return kotlin::Expr::method_call(header, "isUserMember", Vec::new());
559        }
560        Self::call(&Self::camel(method), args)
561    }
562
563    // -- expressions -----------------------------------------------------
564
565    fn name(text: &str) -> Self::Expr {
566        kotlin::Expr::name(text)
567    }
568
569    fn int(value: i128) -> Self::Expr {
570        kotlin::Expr::int(value)
571    }
572
573    fn literal(value: Literal) -> Self::Expr {
574        kotlin::Expr::Literal(value)
575    }
576
577    /// A generated codec is a top-level `fun`, so an unqualified call
578    /// reaches it without naming a class. `strcmp` is the walk's name
579    /// for a three-way octet-order comparison; here it is
580    /// `a.compareTo(b)`, comparing UTF-16 code units — the same reading
581    /// `emit/java.rs` gives, and true for the same corpus of
582    /// inside-the-BMP keys.
583    fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
584        if callee == "strcmp" {
585            let mut arguments = args.into_iter();
586            if let (Some(left), Some(right)) = (arguments.next(), arguments.next()) {
587                return kotlin::Expr::method_call(left, "compareTo", vec![right]);
588            }
589            return kotlin::Expr::call("strcmp", Vec::new());
590        }
591        kotlin::Expr::call(callee, args)
592    }
593
594    /// `w.writeU32(x)` — a method on the local the body holds the writer
595    /// or reader in.
596    /// `w.writeU32(x)`, with one width's argument widened first.
597    ///
598    /// `write_u32` is the one runtime operation the shared walk hands
599    /// two different Kotlin types: a genuine `unsigned long` member,
600    /// already a `Long` through [`Kotlin::scalar`], and — unlike every
601    /// other target, whose count is that width already — a *count*,
602    /// which `crates/lang-rules/src/emit/encode.rs` builds from
603    /// [`Kotlin::count_of`] and hands to `write_u32` with no cast around
604    /// it, because C's `uint32_t` and Java's implicitly-widened `int`
605    /// both already fit there. Kotlin widens nothing implicitly (the
606    /// module documentation says so), so this seam does what the walk
607    /// does not — but only where it has to: `kotlinc -Werror` fails a
608    /// build over "redundant call of conversion method" exactly as
609    /// readily on `v.x.toLong()` for an already-`Long` `x` as it does on
610    /// `v.size.toInt()`, so `Kotlin::to_array_size`'s recognition of
611    /// [`Kotlin::count_of`]'s two `Int` shapes is reused here in
612    /// reverse: widen only where the argument is recognisably one of
613    /// them, and leave a genuine `unsigned long` member's already-`Long`
614    /// value untouched.
615    ///
616    /// `write_i8` and `write_i16` are the same seam from the other side.
617    /// DDS-XTypes 1.3 §7.2.2.2.1.2 gives `int8` and `short` eight and
618    /// sixteen bits, and [`Kotlin::scalar`] holds them in the JVM types
619    /// of exactly those widths, `Byte` and `Short`; `runtime/kotlin/ZdWire.kt`
620    /// takes every integer narrower than 32 bits as an `Int`, because
621    /// that is the width the JVM computes in. Java's argument
622    /// conversion crosses that gap on its own and Kotlin's does not, so
623    /// the widening is written here.
624    ///
625    /// `write_bitmask` is the third: `crates/lang-rules/src/emit/encode.rs`
626    /// widens its value to the `unsigned long long` the C runtime takes,
627    /// and `adapter/kotlin.rs::unsigned_holder` has already put a
628    /// bitmask of more than thirty-two bits — DDS-XTypes 1.3 §7.3.1.2.4
629    /// allows a `@bit_bound` up to 64 — in the `Long` that widening
630    /// leads to. The bound is the call's own first argument, so the
631    /// conversion is dropped where it names one.
632    fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
633        let args = match method {
634            "write_u32" => args.into_iter().map(Self::widen_count).collect(),
635            "write_i8" | "write_i16" => args
636                .into_iter()
637                .map(|arg| Self::cast(kotlin::Type::Primitive(kotlin::Primitive::Int), arg))
638                .collect(),
639            "write_bitmask" if Self::wide_bit_bound(args.first()) => {
640                args.into_iter().map(Self::drop_to_long).collect()
641            }
642            // The same, one argument along: `bitfield` takes the value
643            // first and the field's width second.
644            "bitfield" if Self::wide_bit_bound(args.get(1)) => {
645                args.into_iter().map(Self::drop_to_long).collect()
646            }
647            _ => args,
648        };
649        kotlin::Expr::method_call(Self::name(receiver), &Self::camel(method), args)
650    }
651
652    /// `runtime/kotlin` carries the three HDDS axes: `beginPackedElement`
653    /// and `finishPackedElement` on the writer and the reader, the
654    /// DHEADER pair for a sequence element's own length, and
655    /// `writeWstringUtf8Octets` / `readWstringUtf8Octets`.
656    fn carries_hdds_axes() -> bool {
657        true
658    }
659
660    fn member(base: &str, field: &str) -> Self::Expr {
661        kotlin::Expr::field(Self::name(base), field)
662    }
663
664    fn whole(base: &str) -> Self::Expr {
665        Self::name(base)
666    }
667
668    /// `L` on anything that will not fit an `Int`, and the pattern on
669    /// anything that will not fit a `Long` — the same reasoning
670    /// `emit/java.rs::wide_int` gives. The suffix is spelled the same;
671    /// the minimum is not, which is why every `Long` here goes through
672    /// [`zerodds_lang_render::kotlin::long_text`].
673    fn wide_int(value: i128) -> Self::Expr {
674        let text = if value > i128::from(i64::MAX) {
675            #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
676            let pattern = value as u64 as i64;
677            zerodds_lang_render::kotlin::long_text(pattern)
678        } else if value > i128::from(i32::MAX) || value < i128::from(i32::MIN) {
679            #[allow(clippy::cast_possible_truncation)]
680            let pattern = value as i64;
681            zerodds_lang_render::kotlin::long_text(pattern)
682        } else {
683            format!("{value}")
684        };
685        kotlin::Expr::Literal(Literal::Int(text))
686    }
687
688    /// No unsigned literal exists; wide enough to hold itself, through
689    /// [`Kotlin::wide_int`].
690    fn unsigned_int(value: i128) -> Self::Expr {
691        Self::wide_int(value)
692    }
693
694    /// A 64-bit pattern in a `Long`, which is where Kotlin keeps one.
695    fn wide_unsigned(value: u64) -> Self::Expr {
696        #[allow(clippy::cast_possible_wrap)]
697        let pattern = value as i64;
698        kotlin::Expr::Literal(Literal::Int(zerodds_lang_render::kotlin::long_text(
699            pattern,
700        )))
701    }
702
703    /// A signed 64-bit constant, always suffixed: an enumerator's
704    /// pattern is compared against a `Long` and an unsuffixed literal
705    /// there is an `Int` Kotlin refuses to compare against one.
706    fn wide_signed(value: i128) -> Self::Expr {
707        #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
708        let pattern = if value > i128::from(i64::MAX) {
709            value as u64 as i64
710        } else {
711            value as i64
712        };
713        kotlin::Expr::Literal(Literal::Int(zerodds_lang_render::kotlin::long_text(
714            pattern,
715        )))
716    }
717
718    fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
719        kotlin::Expr::field(value, field)
720    }
721
722    fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
723        kotlin::Expr::Index {
724            value: Box::new(value),
725            index: Box::new(at),
726        }
727    }
728
729    /// `.size` — Kotlin's array property, for both a boxed `Array<T>`
730    /// and a specialised primitive array alike (Kotlin language
731    /// specification, "Array types"); `.length` is Java's and C's
732    /// spelling and not this column's. The bound the materialiser fixed
733    /// is written out where there is one.
734    fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
735        match fixed {
736            Some(bound) => kotlin::Expr::int(i128::from(bound)),
737            None => kotlin::Expr::field(Self::whole(base), "size"),
738        }
739    }
740
741    /// The operators as given, with the three Kotlin has no token for
742    /// translated to the infix functions that stand in: `&` is `and`,
743    /// `|` is `or`, and the walk's own `>>` is `ushr` and not `shr` —
744    /// every value it shifts is a bit set's anchor or a bitmask's
745    /// holder, unsigned in the specification's own terms even though it
746    /// lives in a signed JVM type, and `shr` would sign-extend a
747    /// negative pattern into the vacated bits rather than filling them
748    /// with zero. `&&` and `||` reach Kotlin's own tokens unchanged, as
749    /// every other operator the walk names does.
750    ///
751    /// An integer literal beside an operand `Kotlin::already` knows to
752    /// be a `Long` is suffixed. Kotlin's equality operators are not
753    /// defined across two integer widths at all — `zd_p.length != 0` is
754    /// "operator '!=' cannot be applied to 'Long' and 'Int'" and not a
755    /// widening — and the walk writes the bare literal because C and
756    /// Java both take one there.
757    fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
758        let spelled = match op {
759            "&" => "and",
760            "|" => "or",
761            ">>" => "ushr",
762            other => other,
763        };
764        // Kotlin declares the shift as `infix fun Long.ushr(bitCount:
765        // Int)`, so the distance is an `Int` whatever the value shifted
766        // is (Kotlin language specification, "Bitwise operations"). The
767        // walk hands it in the anchor's own width —
768        // `crates/lang-rules/src/emit/decode.rs` says why — and that is a
769        // `Long` here.
770        if spelled == "ushr" || spelled == "shl" {
771            return kotlin::Expr::Binary {
772                op: spelled,
773                lhs: Box::new(lhs),
774                rhs: Box::new(Self::narrow_shift(rhs)),
775            };
776        }
777        let long = kotlin::Type::Primitive(kotlin::Primitive::Long);
778        let (lhs, rhs) = if Self::already(&long, &lhs) {
779            (lhs, Self::suffix_long(rhs))
780        } else if Self::already(&long, &rhs) {
781            (Self::suffix_long(lhs), rhs)
782        } else {
783            (lhs, rhs)
784        };
785        kotlin::Expr::Binary {
786            op: spelled,
787            lhs: Box::new(lhs),
788            rhs: Box::new(rhs),
789        }
790    }
791
792    /// The walk's own two: `!` — Kotlin's real prefix operator, unlike
793    /// Java's, so [`kotlin::Expr::Not`] answers directly — and `~`,
794    /// which Kotlin has no operator for at all and reaches through
795    /// `.inv()` (Kotlin language specification, "Bitwise operations").
796    fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
797        match op {
798            "!" => kotlin::Expr::Not(Box::new(value)),
799            "~" => Self::convert(value, "inv"),
800            _ => value,
801        }
802    }
803
804    fn paren(value: Self::Expr) -> Self::Expr {
805        kotlin::Expr::Paren(Box::new(value))
806    }
807
808    /// The conversion, spelled by what it converts into. Unlike
809    /// `emit/java.rs`'s, every arm is written unconditionally: Kotlin
810    /// performs no implicit numeric widening at all (not even `Int` to
811    /// `Long`), so there is no redundant cast to avoid the way `javac`'s
812    /// `-Xlint:cast` makes Java avoid one, and `5.toInt()` on a value
813    /// already an `Int` compiles without complaint.
814    fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
815        if type_ == kotlin::Type::named(SIGNED_CHAR) {
816            return Self::convert(value, "toByte");
817        }
818        // A `boolean` discriminator's labels arrive as the integers the
819        // IDL evaluator gives — 0 and 1 — and this is the only position
820        // `cast` is ever asked to reach `Boolean` from; the label *is*
821        // the value it stands for.
822        if type_ == kotlin::Type::Primitive(kotlin::Primitive::Boolean) {
823            if let kotlin::Expr::Literal(Literal::Int(text)) = &value {
824                return kotlin::Expr::Literal(Literal::Bool(text.trim_end_matches('L') != "0"));
825            }
826        }
827        // `kotlinc -Werror` fails a build over "redundant call of
828        // conversion method" wherever the value's own static type
829        // already is the target — `runtime/kotlin/ZdWire.kt`'s own
830        // `readWchar` answering an `Int` where the member already is
831        // one is the case that was measured. `Kotlin::already` knows
832        // the small, closed set of shapes the shared walk hands `cast`
833        // that this backend can already tell the type of without a
834        // general type checker: a literal, `count_of`'s `.size`, and a
835        // runtime read whose return type `runtime/kotlin/ZdWire.kt`
836        // fixes.
837        if Self::already(&type_, &value) {
838            return value;
839        }
840        match type_ {
841            kotlin::Type::Primitive(kotlin::Primitive::Byte) => Self::convert(value, "toByte"),
842            kotlin::Type::Primitive(kotlin::Primitive::Short) => Self::convert(value, "toShort"),
843            kotlin::Type::Primitive(kotlin::Primitive::Int) => Self::convert(value, "toInt"),
844            kotlin::Type::Primitive(kotlin::Primitive::Long) => Self::convert(value, "toLong"),
845            kotlin::Type::Primitive(kotlin::Primitive::Float) => Self::convert(value, "toFloat"),
846            kotlin::Type::Primitive(kotlin::Primitive::Double) => Self::convert(value, "toDouble"),
847            kotlin::Type::Primitive(kotlin::Primitive::Char) => Self::convert(value, "toChar"),
848            // A `Boolean` reached by anything but the label form above,
849            // an aggregate, `Float128`, an array or `Unit`: none of
850            // these has a numeric conversion the walk asks `cast` for,
851            // so the value is handed back unchanged.
852            _ => value,
853        }
854    }
855
856    /// Nothing to take an address of and no array to decay: both forms
857    /// hand the value, as in `emit/java.rs`.
858    fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
859        value
860    }
861
862    // -- statements -------------------------------------------------------
863
864    fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
865        kotlin::Stmt::Declare {
866            mutable: true,
867            type_,
868            name: Ident::new(name),
869            value,
870        }
871    }
872
873    fn perform(value: Self::Expr) -> Self::Stmt {
874        kotlin::Stmt::Expr(value)
875    }
876
877    /// `return v;` where the walk asked for a bare `return`: only the
878    /// reader leaves a routine early, and a Kotlin decoder answers with
879    /// the sample it built.
880    fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
881        kotlin::Stmt::Return(Some(value.unwrap_or_else(|| Self::whole("v"))))
882    }
883
884    /// Nothing: `kotlinc` raises no error over an unread parameter.
885    fn mark_used(_name: &str) -> Vec<Self::Stmt> {
886        Vec::new()
887    }
888
889    /// The call on its own; a method call is already a Kotlin statement.
890    fn drop_value(value: Self::Expr) -> Self::Stmt {
891        kotlin::Stmt::Expr(value)
892    }
893
894    /// `place = zd_dec_T(r, behaviour)` — Kotlin has no address to pass
895    /// and no out-parameter to pass one in, so a decoder answers with
896    /// its value. [`Kotlin::routine`] and [`Kotlin::give_back`] are the
897    /// other two halves of that decision.
898    fn read_call(
899        callee: &str,
900        place: Self::Expr,
901        _form: Handoff,
902        behaviour: Self::Expr,
903    ) -> Self::Stmt {
904        kotlin::Stmt::Expr(kotlin::Expr::Binary {
905            op: "=",
906            lhs: Box::new(place),
907            rhs: Box::new(Self::call(callee, vec![Self::name("r"), behaviour])),
908        })
909    }
910
911    /// `v = zd_dec_T(r, zd_tc)` — a `typedef`, handed straight on.
912    fn read_forward(callee: &str) -> Self::Stmt {
913        Self::read_call(
914            callee,
915            Self::whole("v"),
916            Handoff::Value,
917            Self::name("zd_tc"),
918        )
919    }
920
921    /// `place = place.copyOfRange(0, count)` — Kotlin's array-slicing
922    /// extension, over both a specialised primitive array and a boxed
923    /// one alike.
924    ///
925    /// **Not `copyOf(count)`.** `Array<T>.copyOf(newSize)` is declared
926    /// `Array<T?>` in the Kotlin standard library, because the argument
927    /// may be larger than the receiver and the places it adds hold
928    /// `null`; assigning that back to the `Array<T>` the walk declared
929    /// is `assignment type mismatch: actual type is 'Array<String?>',
930    /// but 'Array<String>' was expected`, which is a `kotlinc` error and
931    /// not a warning. Every reference element type reaches it —
932    /// `string`, `wstring`, an aggregate, and a map's entry holder —
933    /// so a bounded `sequence<string, 8>` or `map<long, long, 4>` made
934    /// the whole file refuse to compile. `copyOfRange(fromIndex,
935    /// toIndex)` is declared `Array<T>` for the same receiver, and
936    /// `IntArray.copyOfRange` answers an `IntArray` exactly as
937    /// `IntArray.copyOf` does, so the one spelling still serves both
938    /// kinds of array.
939    ///
940    /// `toIndex` past the receiver's end would be
941    /// `IndexOutOfBoundsException` where `copyOf` had grown the array
942    /// instead, and this seam never reaches that: `emit::decode`'s
943    /// bounded-collection narrowing runs only under `zd_over`, which is
944    /// `zd_n > count`.
945    fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
946        vec![kotlin::Stmt::Expr(kotlin::Expr::Binary {
947            op: "=",
948            lhs: Box::new(Self::whole(base)),
949            rhs: Box::new(kotlin::Expr::method_call(
950                Self::whole(base),
951                "copyOfRange",
952                vec![
953                    Self::to_array_size(Self::unsigned_int(0)),
954                    Self::to_array_size(count),
955                ],
956            )),
957        })]
958    }
959
960    fn when(
961        condition: Self::Expr,
962        then: Self::Block,
963        otherwise: Option<Self::Block>,
964    ) -> Self::Stmt {
965        kotlin::Stmt::If {
966            condition,
967            then: kotlin::Block { statements: then },
968            otherwise: otherwise.map(|body| kotlin::Block { statements: body }),
969        }
970    }
971
972    /// `for (i in 0 until bound)`, with `bound` narrowed to `Int` first.
973    ///
974    /// The counter this renders is always `Int`-ranged — `0 until` an
975    /// `Int` literal — because every array this column's codec indexes
976    /// is a Kotlin array, whose subscript is `Int`
977    /// (Kotlin language specification, "Array types"). A sequence's own
978    /// length is a `uint32` on the wire and lives in a `Long` once read
979    /// (`Kotlin::scalar`'s `Scalar::UInt32` answer), so a bound built
980    /// from one needs the same narrowing [`Kotlin::make_room`]'s length
981    /// does, for the identical reason: a JVM array cannot be longer than
982    /// `Int.MAX_VALUE` regardless of what the wire declared.
983    fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
984        kotlin::Stmt::CountUp {
985            counter: Ident::new(counter),
986            bound: Box::new(Self::to_array_size(bound)),
987            body: kotlin::Block { statements: body },
988        }
989    }
990
991    fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
992        kotlin::Stmt::While {
993            condition,
994            body: kotlin::Block { statements: body },
995        }
996    }
997
998    fn leave() -> Self::Stmt {
999        kotlin::Stmt::Break
1000    }
1001
1002    fn note(doc: Doc) -> Self::Stmt {
1003        kotlin::Stmt::Comment(doc)
1004    }
1005
1006    // -- assembly -----------------------------------------------------
1007
1008    /// `place = Array(count) { Element() }`, or `IntArray(count)` for a
1009    /// primitive element — the module documentation says why no
1010    /// `assigned_whole`-conditioned pre-fill loop is needed here, unlike
1011    /// `emit/java.rs`'s.
1012    fn make_room(
1013        base: &str,
1014        element: Self::Type,
1015        count: Self::Expr,
1016        _assigned_whole: bool,
1017        fill: Vec<Self::Stmt>,
1018    ) -> Vec<Self::Stmt> {
1019        Self::allocate(base, element, count, None, fill)
1020    }
1021
1022    /// `Array(count) { 0 }` where the element is the `typealias` of a
1023    /// typedef of a plain value, which has no constructor for `T()`
1024    /// (`default_kotlin::alias_slot_start`); [`Kotlin::make_room`] for
1025    /// every other element.
1026    fn make_room_of(
1027        ast: &Ast,
1028        element_id: Id<TypeNode>,
1029        base: &str,
1030        element: Self::Type,
1031        count: Self::Expr,
1032        _assigned_whole: bool,
1033        fill: Vec<Self::Stmt>,
1034    ) -> Vec<Self::Stmt> {
1035        let init = crate::default_kotlin::alias_slot_start(ast, element_id);
1036        Self::allocate(base, element, count, init, fill)
1037    }
1038
1039    /// The narrowing `Byte`, `Short` and `Char` holders need, and no
1040    /// other: `readI8`/`readI16`/`readChar` all answer an `Int`, and a
1041    /// narrower field does not take one without being told to.
1042    fn store(place: Self::Expr, type_: Self::Type, value: Self::Expr) -> Self::Stmt {
1043        // `crates/lang-rules/src/emit/decode.rs::primitive_read_at`
1044        // already wraps a `char`/`wchar` read in its own `cast` to
1045        // `Kotlin::scalar`'s width — a `char` becomes `Char` there and a
1046        // `wchar` stays the `Int` it already reads as — before this is
1047        // ever reached, so applying [`Kotlin::already`]'s test here
1048        // finds the walk's own conversion and leaves it exactly once
1049        // rather than adding a second, redundant one on top of it.
1050        if Self::already(&type_, &value) {
1051            return kotlin::Stmt::Expr(kotlin::Expr::Binary {
1052                op: "=",
1053                lhs: Box::new(place),
1054                rhs: Box::new(value),
1055            });
1056        }
1057        let narrow = matches!(
1058            type_,
1059            kotlin::Type::Primitive(
1060                kotlin::Primitive::Byte | kotlin::Primitive::Short | kotlin::Primitive::Char
1061            )
1062        );
1063        let value = if narrow {
1064            Self::cast(type_, value)
1065        } else {
1066            value
1067        };
1068        kotlin::Stmt::Expr(kotlin::Expr::Binary {
1069            op: "=",
1070            lhs: Box::new(place),
1071            rhs: Box::new(value),
1072        })
1073    }
1074
1075    /// `val name = T()` — a Kotlin local of a class type is a reference
1076    /// to nothing until one is made, and every generated class has a
1077    /// zero-argument constructor because every one of its properties
1078    /// carries a default (`adapter/kotlin.rs`'s materialiser gives every
1079    /// one).
1080    fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
1081        kotlin::Stmt::Declare {
1082            mutable: false,
1083            type_: type_.clone(),
1084            name: Ident::new(name),
1085            value: Some(kotlin::Expr::New {
1086                type_: Box::new(type_),
1087                args: Vec::new(),
1088            }),
1089        }
1090    }
1091
1092    /// `runtime/kotlin/ZdWire.kt` publishes a header's parts as
1093    /// constructor properties, so the field the walk names is a
1094    /// property access — `.isEnd`, not `.isEnd()`: Kotlin generates no
1095    /// getter method beside a `val` the way Java's accessor-per-field
1096    /// convention does.
1097    fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
1098        kotlin::Expr::field(
1099            value,
1100            match field {
1101                "pid" => "pid",
1102                "end" => "isEnd",
1103                "ignore" => "ignore",
1104                "length" => "length",
1105                "member_id" => "memberId",
1106                "must_understand" => "mustUnderstand",
1107                "extent" => "extent",
1108                // A field the runtime does not publish is a `kotlinc`
1109                // error naming it, which is where a header that grew a
1110                // part shows up.
1111                other => other,
1112            },
1113        )
1114    }
1115
1116    fn present(base: &str, field: &str) -> Self::Expr {
1117        kotlin::Expr::Binary {
1118            op: "!=",
1119            lhs: Box::new(Self::member(base, field)),
1120            rhs: Box::new(Self::absent()),
1121        }
1122    }
1123
1124    fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
1125        if present {
1126            return Vec::new();
1127        }
1128        vec![kotlin::Stmt::Expr(kotlin::Expr::Binary {
1129            op: "=",
1130            lhs: Box::new(Self::member(base, field)),
1131            rhs: Box::new(Self::absent()),
1132        })]
1133    }
1134
1135    /// One property per case, directly on the class.
1136    fn case_member(base: &str, field: &str) -> Self::Expr {
1137        Self::member(base, field)
1138    }
1139
1140    fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
1141        kotlin::Expr::field(entry, if key { "key" } else { "value" })
1142    }
1143
1144    /// `v.x!!` — the value an `@optional` member holds, given that it
1145    /// holds one. The module documentation gives the reason the bare
1146    /// property is not enough: it is a `var`, and Kotlin does not
1147    /// smart-cast one.
1148    fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
1149        kotlin::Expr::NotNull(Box::new(Self::member(base, field)))
1150    }
1151
1152    fn sequence_element(
1153        base: &str,
1154        index: Self::Expr,
1155        _element: Self::Type,
1156    ) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
1157        (
1158            Vec::new(),
1159            Self::index(Self::elements(base), index),
1160            Vec::new(),
1161        )
1162    }
1163
1164    /// The boxed property itself; assigning to it is what makes the
1165    /// member present.
1166    fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
1167        (Vec::new(), Self::member(base, field))
1168    }
1169
1170    fn elements(base: &str) -> Self::Expr {
1171        Self::whole(base)
1172    }
1173
1174    fn absent() -> Self::Expr {
1175        kotlin::Expr::Literal(Literal::Null)
1176    }
1177
1178    /// Nothing: every value this column's runtime materialises on a
1179    /// successful read is a real, non-null Kotlin value — `readString`
1180    /// answers `String` and not `String?` — so there is no "the read
1181    /// produced nothing" state for a generated decoder to guard against,
1182    /// the same answer [`crate::emit::zig::Zig::materialised`] gives for
1183    /// the same reason and unlike Java's, whose `String` reference could
1184    /// in principle be `null`.
1185    fn materialised(_value: Self::Expr) -> Option<Self::Expr> {
1186        None
1187    }
1188
1189    fn block(statements: Vec<Self::Stmt>) -> Self::Block {
1190        statements
1191    }
1192
1193    fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
1194        block
1195    }
1196
1197    /// `fun zd_enc_T(w: Writer, v: T)` and `fun zd_dec_T(r: Reader, zd_tc: TryConstruct): T`.
1198    ///
1199    /// A top-level `fun`, needing no forward declaration: Kotlin resolves
1200    /// a call to a function declared later in the same file (Kotlin
1201    /// language specification, "Declarations"), so a recursive type
1202    /// needs no prototype ahead of its body — `body` absent therefore
1203    /// answers `None` outright, as `emit/java.rs::routine` does for the
1204    /// same reason.
1205    fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
1206        let body = spec.body?;
1207        let mut params = vec![kotlin::Param {
1208            name: Ident::new(if spec.writing { "w" } else { "r" }),
1209            type_: kotlin::Type::named(if spec.writing { "Writer" } else { "Reader" }),
1210        }];
1211        if spec.writing {
1212            params.push(kotlin::Param {
1213                name: Ident::new("v"),
1214                type_: spec.sample.clone(),
1215            });
1216        }
1217        params.extend(spec.extra.into_iter().map(|(type_, name)| kotlin::Param {
1218            name: Ident::new(&name),
1219            type_,
1220        }));
1221        let (returns, body) = if spec.writing {
1222            (kotlin::Type::Unit, body)
1223        } else {
1224            (
1225                spec.sample.clone(),
1226                Self::reader_body(&spec.name, spec.sample, body),
1227            )
1228        };
1229        Some(kotlin::Decl::Func(kotlin::FuncDecl {
1230            doc: spec.doc,
1231            name: Ident::new(&spec.name),
1232            params,
1233            returns,
1234            body,
1235        }))
1236    }
1237
1238    // -- the materialiser's boundary ------------------------------------
1239
1240    /// A typedef of a collection, an aggregate or a `long double` is seen
1241    /// through, so that an array the codec allocates starts each slot with
1242    /// a value of the target's own form (`IntArray(0)`, `emptyArray<T>()`,
1243    /// `T()`, `Float128.zero()`); a `typealias` is interchangeable with its
1244    /// target. A typedef of a plain value keeps its name: the declaration
1245    /// holds it boxed as `Array<Alias>`, which a specialised `IntArray`
1246    /// would not match, and [`Kotlin::make_room_of`] writes the slot's
1247    /// start value, because the alias has no constructor.
1248    fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
1249        let mut seen = id;
1250        for _ in 0..64 {
1251            let Ok(zerodds_ast::TypeKind::Alias { aliased }) =
1252                ast.type_(seen).map(|node| &node.kind)
1253            else {
1254                break;
1255            };
1256            let Some(target) = aliased.resolved() else {
1257                break;
1258            };
1259            seen = target;
1260        }
1261        let plain = matches!(
1262            ast.type_(seen).map(|node| &node.kind),
1263            Ok(zerodds_ast::TypeKind::Primitive(kind)) if *kind != zerodds_ast::PrimitiveKind::Float128
1264        ) || matches!(
1265            ast.type_(seen).map(|node| &node.kind),
1266            Ok(zerodds_ast::TypeKind::Enumeration { .. } | zerodds_ast::TypeKind::Bitmask { .. })
1267        );
1268        let shown = if plain { id } else { seen };
1269        kotlin_value_type(ast, shown, "codec")
1270            .unwrap_or_else(|_| kotlin::Type::named(&format!("UnresolvedType{}", id.slot())))
1271    }
1272
1273    /// The materialiser's own naming: `type_tag` refuses nothing this
1274    /// signature could pass on, and the fallback carries the arena slot
1275    /// so that two failures cannot collide onto one bare `unknown`.
1276    fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
1277        type_tag(ast, id, 0)
1278    }
1279
1280    fn escape(name: &str) -> String {
1281        property_name(name)
1282    }
1283
1284    /// The base is the first member `parent` (XTypes 1.3 §7.5.1.4.1.3).
1285    fn inherited_path(depth: usize) -> Option<String> {
1286        Some(format!("{}.", property_name("parent")).repeat(depth))
1287    }
1288
1289    fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
1290        kotlin_map_entry_name(ast, id)
1291    }
1292
1293    /// Nothing: a Kotlin file has one package and no nested modules, so
1294    /// there is no prefix for a declaration's own scope to add — the
1295    /// module flattening `adapter/kotlin.rs` already did the whole of
1296    /// this.
1297    fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
1298        String::new()
1299    }
1300
1301    /// # Errors
1302    /// As [`crate::default_kotlin::member_default`]: a `@default` this
1303    /// column cannot spell is refused rather than silently replaced by
1304    /// Table 9's zero.
1305    fn member_default(
1306        ast: &Ast,
1307        node: &MemberNode,
1308        type_: Id<TypeNode>,
1309        place: Self::Expr,
1310    ) -> Result<Vec<Self::Stmt>, EncodeError> {
1311        crate::default_kotlin::member_default(ast, node, type_, place)
1312    }
1313
1314    /// `place = zd_def_<T>()` — Table 9, through the generated
1315    /// defaulter.
1316    ///
1317    /// # Errors
1318    /// Never here; whether the type has a Kotlin default is decided
1319    /// where the defaulter is built.
1320    fn default_into(
1321        ast: &Ast,
1322        id: Id<TypeNode>,
1323        place: Self::Expr,
1324    ) -> Result<Self::Stmt, EncodeError> {
1325        Ok(crate::default_kotlin::default_into(ast, id, place))
1326    }
1327
1328    /// `Enum.LITERAL` — no outer path: every `object` the materialiser
1329    /// declares is already a top-level name.
1330    fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
1331        crate::default_kotlin::enumerator_constant(ast, id, enumerator)
1332    }
1333}