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}