Skip to main content

zerodds_lang_render/
kotlin.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Rendering the Kotlin CST. Spec §5.
4//!
5//! Four spaces, the opening brace on the same line, `//` and `/** */`
6//! comments — `ktfmt`'s and `ktlint`'s shared conventions, and close
7//! enough to [`crate::java`]'s that the two read the same way beside
8//! each other. Nothing is decided here that
9//! [`zerodds_lang_cst::kotlin`] or `zerodds_lang_rules::adapter::kotlin`
10//! did not decide first.
11//!
12//! ## Where this differs from [`crate::java`]
13//!
14//! - **No wrapper class.** [`file()`] writes top-level declarations
15//!   directly, because a `.kt` file is not limited to one public type
16//!   the way a `.java` file is (Kotlin language specification, "Kotlin
17//!   files").
18//! - **The specialised array factories.** `array_of` picks
19//!   `intArrayOf`/`byteArrayOf`/… for a primitive element and
20//!   `arrayOf<T>` otherwise, and `array_type` does the matching
21//!   choice for the *type*: `IntArray` against `Array<Pose>`. Getting
22//!   the two out of step is a type Kotlin refuses to unify —
23//!   `arrayOf(1, 2)` against a declared `IntArray` is
24//!   "type mismatch: inferred type is Array\<Int\> but IntArray was
25//!   expected" — so both read the element type through one function,
26//!   `is_specialised`.
27//! - **No `new`.** Kotlin object creation is the type name applied to
28//!   arguments (Kotlin language specification, "Object creation
29//!   expressions"), so `expr_text`'s [`zerodds_lang_cst::kotlin::Expr::New`]
30//!   arm writes none.
31
32use crate::writer::{Writer, comment_lines, verbatim_lines};
33use alloc::format;
34use alloc::string::{String, ToString};
35use alloc::vec::Vec;
36use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
37use zerodds_lang_cst::kotlin::{
38    AliasDecl, Class, ConstDecl, Decl, Expr, File, FuncDecl, Object, Param, Property, Stmt, Type,
39};
40
41/// Render a whole file.
42#[must_use]
43pub fn file(file: &File) -> String {
44    let mut w = Writer::spaces4();
45    if !file.doc.is_empty() {
46        comment_lines(&mut w, "// ", file.doc.as_lines());
47        w.blank_line();
48    }
49    if let Some(package) = &file.package {
50        w.line(&alloc::format!("package {}", path_text(package)));
51        w.blank_line();
52    }
53    if !file.imports.is_empty() {
54        for import in &file.imports {
55            w.line(&alloc::format!("import {}", path_text(import)));
56        }
57        w.blank_line();
58    }
59    for decl in &file.decls {
60        render_decl(&mut w, decl);
61        w.blank_line();
62    }
63    w.finish()
64}
65
66/// Render one declaration, for a caller that has only one.
67#[must_use]
68pub fn decl(node: &Decl) -> String {
69    let mut w = Writer::spaces4();
70    render_decl(&mut w, node);
71    w.finish()
72}
73
74/// Render one type, for a caller that has only one.
75#[must_use]
76pub fn type_(node: &Type) -> String {
77    type_text(node)
78}
79
80/// The identifier a name renders as, for a caller assembling one.
81#[must_use]
82pub fn ident(name: &Ident) -> String {
83    name.as_str().to_string()
84}
85
86fn render_decl(w: &mut Writer, node: &Decl) {
87    match node {
88        Decl::Class(class) => render_class(w, class, true),
89        Decl::Interface(class) => render_class(w, class, false),
90        Decl::Object(object) => render_object(w, object),
91        Decl::Alias(alias) => render_alias(w, alias),
92        Decl::Const(const_) => render_const(w, const_),
93        Decl::Func(func) => render_func(w, func),
94        Decl::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
95        Decl::Verbatim(text) => verbatim_lines(w, text),
96    }
97}
98
99/// `data class Name(var a: T = x, ...)`, or `class Name(val a: T, ...)`
100/// where `as_data` is false.
101fn render_class(w: &mut Writer, node: &Class, as_data: bool) {
102    doc(w, &node.doc);
103    let mut head = String::new();
104    if as_data && node.is_data {
105        head.push_str("data ");
106    }
107    head.push_str("class ");
108    head.push_str(node.name.as_str());
109    head.push('(');
110    w.line(&head);
111    w.indent();
112    let last = node.properties.len().saturating_sub(1);
113    for (index, property) in node.properties.iter().enumerate() {
114        render_property(w, property, index == last);
115    }
116    w.dedent();
117    if node.body_text.begin.is_empty() && node.body_text.end.is_empty() {
118        w.line(")");
119        return;
120    }
121    w.line(") {");
122    w.indent();
123    for text in node.body_text.begin.iter().chain(&node.body_text.end) {
124        verbatim_lines(w, text);
125    }
126    w.dedent();
127    w.line("}");
128}
129
130fn render_property(w: &mut Writer, node: &Property, last: bool) {
131    doc(w, &node.doc);
132    let mut line = String::from(if node.mutable { "var " } else { "val " });
133    line.push_str(node.name.as_str());
134    line.push_str(": ");
135    line.push_str(&type_text(&node.type_));
136    if let Some(default) = &node.default {
137        line.push_str(" = ");
138        line.push_str(&expr_text(default));
139    }
140    if !last {
141        line.push(',');
142    }
143    w.line(&line);
144}
145
146/// `object Name { const val A: T = x }`.
147fn render_object(w: &mut Writer, node: &Object) {
148    doc(w, &node.doc);
149    let mut head = String::from("object ");
150    head.push_str(node.name.as_str());
151    if node.members.is_empty() {
152        head.push_str(" {}");
153        w.line(&head);
154        return;
155    }
156    head.push_str(" {");
157    w.line(&head);
158    w.indent();
159    for member in &node.members {
160        render_const(w, member);
161    }
162    w.dedent();
163    w.line("}");
164}
165
166/// `typealias Name = T`.
167fn render_alias(w: &mut Writer, node: &AliasDecl) {
168    doc(w, &node.doc);
169    let mut line = String::from("typealias ");
170    line.push_str(node.name.as_str());
171    line.push_str(" = ");
172    line.push_str(&type_text(&node.target));
173    w.line(&line);
174}
175
176/// `const val Name: T = value`, or `val` where `is_const` is false.
177fn render_const(w: &mut Writer, node: &ConstDecl) {
178    doc(w, &node.doc);
179    let mut line = String::from(if node.is_const { "const val " } else { "val " });
180    line.push_str(node.name.as_str());
181    line.push_str(": ");
182    line.push_str(&type_text(&node.type_));
183    line.push_str(" = ");
184    line.push_str(&expr_text(&node.value));
185    w.line(&line);
186}
187
188/// `fun name(a: T, b: U): R { body }`.
189fn render_func(w: &mut Writer, node: &FuncDecl) {
190    doc(w, &node.doc);
191    let mut head = String::from("fun ");
192    head.push_str(node.name.as_str());
193    head.push('(');
194    let params: Vec<String> = node.params.iter().map(param_text).collect();
195    head.push_str(&params.join(", "));
196    head.push_str("): ");
197    head.push_str(&type_text(&node.returns));
198    head.push_str(" {");
199    w.line(&head);
200    w.indent();
201    for statement in &node.body {
202        render_stmt(w, statement);
203    }
204    w.dedent();
205    w.line("}");
206}
207
208fn param_text(param: &Param) -> String {
209    let mut out = String::from(param.name.as_str());
210    out.push_str(": ");
211    out.push_str(&type_text(&param.type_));
212    out
213}
214
215/// zerodds-lint: recursion-depth 64 (one level per nested block of a
216/// generated body. The codec walk emits a routine per type rather than
217/// inlining one — `lang-rules/src/emit/encode.rs` — so a body nests by
218/// its own framing and by one loop per array dimension, never by the
219/// type graph, whose descent stops at 64 anyway; the else-if chain, one
220/// arm per union case, is walked iteratively and costs no depth.)
221fn render_stmt(w: &mut Writer, node: &Stmt) {
222    match node {
223        Stmt::Declare {
224            mutable,
225            type_,
226            name,
227            value,
228        } => {
229            let mut line = String::from(if *mutable { "var " } else { "val " });
230            line.push_str(name.as_str());
231            line.push_str(": ");
232            line.push_str(&type_text(type_));
233            if let Some(value) = value {
234                line.push_str(" = ");
235                line.push_str(&expr_text(value));
236            }
237            w.line(&line);
238        }
239        Stmt::Expr(value) => w.line(&expr_text(value)),
240        Stmt::Return(value) => match value {
241            Some(value) => w.line(&alloc::format!("return {}", expr_text(value))),
242            None => w.line("return"),
243        },
244        Stmt::If { .. } => render_if(w, node, "if ("),
245        Stmt::CountUp {
246            counter,
247            bound,
248            body,
249        } => {
250            let mut head = String::from("for (");
251            head.push_str(counter.as_str());
252            head.push_str(" in 0 until ");
253            head.push_str(&expr_text(bound));
254            head.push_str(") {");
255            w.line(&head);
256            w.indent();
257            for statement in &body.statements {
258                render_stmt(w, statement);
259            }
260            w.dedent();
261            w.line("}");
262        }
263        Stmt::While { condition, body } => {
264            let mut head = String::from("while (");
265            head.push_str(&expr_text(condition));
266            head.push_str(") {");
267            w.line(&head);
268            w.indent();
269            for statement in &body.statements {
270                render_stmt(w, statement);
271            }
272            w.dedent();
273            w.line("}");
274        }
275        Stmt::Break => w.line("break"),
276        Stmt::Throw(value) => w.line(&alloc::format!("throw {}", expr_text(value))),
277        Stmt::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
278    }
279}
280
281/// An `if`, with an `else`-block of exactly one `if` collapsed into
282/// `else if` — as in the Zig and Java columns, the shape a long dispatch
283/// chain stays readable in.
284///
285/// The next arm is taken in a loop and not by a call to this function
286/// again. The chain's length is the union's case count, the
287/// enumeration's enumerator count or the `@mutable` aggregate's member
288/// count — `lang-rules/src/emit/encode.rs` builds one `If` per arm, each
289/// in the previous one's `else` — and nothing in the model bounds any of
290/// the three.
291///
292/// zerodds-lint: recursion-depth 64 (block nesting only; the arm chain
293/// is walked iteratively, and what remains is one level per nested block
294/// of a generated body — the codec walk emits a routine per type rather
295/// than inlining, so a body nests by its framing and by one loop per
296/// array dimension, never by the type graph)
297fn render_if(w: &mut Writer, node: &Stmt, keyword: &str) {
298    let mut node = node;
299    let mut keyword = keyword;
300    loop {
301        let Stmt::If {
302            condition,
303            then,
304            otherwise,
305        } = node
306        else {
307            return;
308        };
309        let mut head = String::from(keyword);
310        head.push_str(&expr_text(condition));
311        head.push_str(") {");
312        w.line(&head);
313        w.indent();
314        for statement in &then.statements {
315            render_stmt(w, statement);
316        }
317        w.dedent();
318        match otherwise {
319            None => {
320                w.line("}");
321                return;
322            }
323            Some(chain)
324                if chain.statements.len() == 1
325                    && matches!(chain.statements[0], Stmt::If { .. }) =>
326            {
327                node = &chain.statements[0];
328                keyword = "} else if (";
329            }
330            Some(block) => {
331                w.line("} else {");
332                w.indent();
333                for statement in &block.statements {
334                    render_stmt(w, statement);
335                }
336                w.dedent();
337                w.line("}");
338                return;
339            }
340        }
341    }
342}
343
344/// Whether `element` is one of the eight types the JVM has a
345/// specialised, unboxed array for — `BooleanArray`, `ByteArray`,
346/// `ShortArray`, `IntArray`, `LongArray`, `FloatArray`, `DoubleArray`,
347/// `CharArray` (Kotlin language specification, "Array types"). Anything
348/// else, including a nullable primitive, is a boxed `Array<T>`: a `null`
349/// has no representation in an unboxed array.
350fn is_specialised(element: &Type) -> bool {
351    matches!(element, Type::Primitive(_))
352}
353
354/// The value each slot of a new `Array<T>` starts with, before the
355/// codec fills it. `T()` is right for a generated class, whose every
356/// property has a default, and wrong for three other element kinds:
357/// a nested array (`IntArray()` and `Array<T>()` take arguments), a
358/// `Float128` (its constructor is private; `Float128.zero()` is the
359/// runtime's) and a nullable (`null`).
360fn element_init(element: &Type) -> String {
361    match element {
362        Type::Array(inner) => match inner.as_ref() {
363            Type::Primitive(primitive) => alloc::format!("{}Array(0)", primitive.as_str()),
364            other => alloc::format!("emptyArray<{}>()", type_text(other)),
365        },
366        Type::Nullable(_) => "null".to_string(),
367        _ if type_text(element) == "Float128" => "Float128.zero()".to_string(),
368        _ => alloc::format!("{}()", type_text(element)),
369    }
370}
371
372/// `IntArray`, or `Array<T>` where `T` is not one of the eight.
373fn array_type(element: &Type) -> String {
374    if let Type::Primitive(primitive) = element {
375        return alloc::format!("{}Array", primitive.as_str());
376    }
377    alloc::format!("Array<{}>", type_text(element))
378}
379
380/// `intArrayOf(a, b)`, or `arrayOf<T>(a, b)`.
381fn array_of(element: &Type, elements: &[Expr]) -> String {
382    let parts: Vec<String> = elements.iter().map(expr_text).collect();
383    if let Type::Primitive(primitive) = element {
384        let mut factory = primitive.as_str().to_string();
385        // `Int` -> `intArrayOf`, `Boolean` -> `booleanArrayOf`: the
386        // factory's own first letter is lower-cased and the rest is the
387        // primitive's name unchanged.
388        let mut chars = factory.drain(..);
389        let mut out = String::new();
390        if let Some(first) = chars.next() {
391            out.extend(first.to_lowercase());
392        }
393        out.push_str(chars.as_str());
394        out.push_str("ArrayOf(");
395        out.push_str(&parts.join(", "));
396        out.push(')');
397        return out;
398    }
399    let mut out = String::from("arrayOf<");
400    out.push_str(&type_text(element));
401    out.push_str(">(");
402    out.push_str(&parts.join(", "));
403    out.push(')');
404    out
405}
406
407/// zerodds-lint: recursion-depth 64 (one level per collection, alias or
408/// generic argument of the spelled type; `MAX_TYPE_DEPTH` in
409/// `lang-rules/src/adapter/kotlin.rs` refuses a type that nests deeper)
410fn type_text(node: &Type) -> String {
411    match node {
412        Type::Primitive(primitive) => primitive.as_str().to_string(),
413        Type::Named { path, args } => {
414            let mut out = path_text(path);
415            if !args.is_empty() {
416                out.push('<');
417                let parts: Vec<String> = args.iter().map(type_text).collect();
418                out.push_str(&parts.join(", "));
419                out.push('>');
420            }
421            out
422        }
423        Type::Array(element) => array_type(element),
424        Type::Nullable(inner) => {
425            let mut out = type_text(inner);
426            out.push('?');
427            out
428        }
429        Type::Function { params, returns } => {
430            let mut out = String::from("(");
431            let parts: Vec<String> = params.iter().map(type_text).collect();
432            out.push_str(&parts.join(", "));
433            out.push_str(") -> ");
434            out.push_str(&type_text(returns));
435            out
436        }
437        Type::Unit => "Unit".to_string(),
438    }
439}
440
441fn path_text(path: &Path) -> String {
442    let parts: Vec<&str> = path.segments().iter().map(Ident::as_str).collect();
443    parts.join(".")
444}
445
446/// zerodds-lint: recursion-depth = the built expression's nesting. A walk
447/// calls a routine per type rather than nesting one, so the one operand
448/// tree that grows with the IDL is an array's default value: one composite
449/// literal per dimension, built in `lang-rules/src/default_kotlin.rs`, and
450/// nothing bounds the dimension count. No number is claimed: this
451/// signature returns no error.
452fn expr_text(node: &Expr) -> String {
453    match node {
454        Expr::Literal(literal) => literal_text(literal),
455        Expr::Path(path) => path_text(path),
456        Expr::Call {
457            receiver,
458            name,
459            args,
460        } => {
461            let mut out = match receiver {
462                Some(receiver) => {
463                    let mut out = expr_text(receiver);
464                    out.push('.');
465                    out
466                }
467                None => String::new(),
468            };
469            out.push_str(name.as_str());
470            out.push('(');
471            let parts: Vec<String> = args.iter().map(expr_text).collect();
472            out.push_str(&parts.join(", "));
473            out.push(')');
474            out
475        }
476        Expr::Field { value, name } => {
477            let mut out = expr_text(value);
478            out.push('.');
479            out.push_str(name.as_str());
480            out
481        }
482        Expr::Index { value, index } => {
483            let mut out = expr_text(value);
484            out.push('[');
485            out.push_str(&expr_text(index));
486            out.push(']');
487            out
488        }
489        Expr::New { type_, args } => {
490            let mut out = type_text(type_);
491            out.push('(');
492            let parts: Vec<String> = args.iter().map(expr_text).collect();
493            out.push_str(&parts.join(", "));
494            out.push(')');
495            out
496        }
497        Expr::NewArray {
498            element,
499            length,
500            init,
501        } => {
502            if is_specialised(element) {
503                alloc::format!("{}({})", array_type(element), expr_text(length))
504            } else {
505                let start = match init {
506                    Some(init) => expr_text(init),
507                    None => element_init(element),
508                };
509                alloc::format!("Array({}) {{ {} }}", expr_text(length), start)
510            }
511        }
512        Expr::ArrayOf { element, elements } => array_of(element, elements),
513        Expr::Binary { op, lhs, rhs } => {
514            let mut out = expr_text(lhs);
515            out.push(' ');
516            out.push_str(op);
517            out.push(' ');
518            out.push_str(&expr_text(rhs));
519            out
520        }
521        Expr::Paren(inner) => {
522            let mut out = String::from("(");
523            out.push_str(&expr_text(inner));
524            out.push(')');
525            out
526        }
527        Expr::As { value, type_ } => {
528            let mut out = expr_text(value);
529            out.push_str(" as ");
530            out.push_str(&type_text(type_));
531            out
532        }
533        Expr::If {
534            condition,
535            then,
536            otherwise,
537        } => alloc::format!(
538            "if ({}) {} else {}",
539            expr_text(condition),
540            expr_text(then),
541            expr_text(otherwise)
542        ),
543        Expr::Not(value) => {
544            let mut out = String::from("!");
545            out.push_str(&expr_text(value));
546            out
547        }
548        Expr::NotNull(value) => {
549            let mut out = expr_text(value);
550            out.push_str("!!");
551            out
552        }
553    }
554}
555
556fn literal_text(literal: &Literal) -> String {
557    match literal {
558        Literal::Int(text) | Literal::Float(text) => text.clone(),
559        Literal::Bool(value) => {
560            if *value {
561                "true".to_string()
562            } else {
563                "false".to_string()
564            }
565        }
566        Literal::Char(ch) => {
567            let mut out = String::from("'");
568            out.push_str(&char_text(*ch));
569            out.push('\'');
570            out
571        }
572        Literal::Str(text) => {
573            let mut out = String::from("\"");
574            out.push_str(&string_text(text));
575            out.push('"');
576            out
577        }
578        // Kotlin has no byte-string literal; the caller writes a
579        // `byteArrayOf(0x1, 0x2, ..)` call instead of reaching this arm.
580        Literal::Bytes(bytes) => {
581            let mut out = String::from("byteArrayOf(");
582            let parts: Vec<String> = bytes
583                .iter()
584                .map(|b| alloc::format!("{}", i16::from(*b as i8)))
585                .collect();
586            out.push_str(&parts.join(", "));
587            out.push(')');
588            out
589        }
590        Literal::Null => "null".to_string(),
591    }
592}
593
594/// Escape a string for a Kotlin string literal.
595///
596/// Kotlin's escapes are `\t`, `\b`, `\n`, `\r`, `\'`, `\"`, `\\`, `\$`
597/// and `\uXXXX` (Kotlin language specification, "String literals");
598/// there is no `\0`, as in Zig, so a NUL goes out `\\u0000`. `$` is
599/// escaped everywhere, not only before `{` or a letter: Kotlin's string
600/// template only ever *reads* `$name` or `${expr}`, but escaping the
601/// character unconditionally means this function never has to look
602/// ahead at what follows a `$` in the source text to decide.
603#[must_use]
604pub fn string_text(text: &str) -> String {
605    let mut out = String::new();
606    for ch in text.chars() {
607        push_escaped(&mut out, ch, '"');
608    }
609    out
610}
611
612/// A 64-bit pattern as the source text of a Kotlin `Long`.
613///
614/// Every value but one is its own decimal with the `L` suffix. Kotlin
615/// has no negative integer literal at all — the minus is a prefix
616/// operator applied to the literal that follows (Kotlin language
617/// specification, "Integer literals") — so `-9223372036854775808L`
618/// asks the compiler for a literal one past `Long.MAX_VALUE` and is
619/// "value out of range", where the same characters are how Java spells
620/// its own minimum. `Long.MIN_VALUE` is the value itself.
621#[must_use]
622pub fn long_text(pattern: i64) -> String {
623    if pattern == i64::MIN {
624        return "Long.MIN_VALUE".to_string();
625    }
626    format!("{pattern}L")
627}
628
629/// The same, for a character literal.
630#[must_use]
631pub fn char_text(ch: char) -> String {
632    let mut out = String::new();
633    push_escaped(&mut out, ch, '\'');
634    out
635}
636
637fn push_escaped(out: &mut String, ch: char, delimiter: char) {
638    match ch {
639        '\\' => out.push_str("\\\\"),
640        '\n' => out.push_str("\\n"),
641        '\r' => out.push_str("\\r"),
642        '\t' => out.push_str("\\t"),
643        '\u{8}' => out.push_str("\\b"),
644        '$' => out.push_str("\\$"),
645        c if c == delimiter => {
646            out.push('\\');
647            out.push(c);
648        }
649        c if (c as u32) < 0x20 || c as u32 == 0x7F => {
650            out.push_str(&alloc::format!("\\u{:04x}", c as u32));
651        }
652        other => out.push(other),
653    }
654}
655
656fn doc(w: &mut Writer, doc: &Doc) {
657    if doc.is_empty() {
658        return;
659    }
660    comment_lines(w, "// ", doc.as_lines());
661}
662
663#[cfg(test)]
664mod tests {
665    #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
666    use super::*;
667    use zerodds_lang_cst::common::Doc;
668    use zerodds_lang_cst::kotlin::Primitive;
669
670    fn data_class(properties: Vec<Property>) -> Decl {
671        Decl::Class(Class {
672            body_text: zerodds_lang_cst::common::BodyText::none(),
673            doc: Doc::none(),
674            is_data: true,
675            name: Ident::new("S"),
676            properties,
677        })
678    }
679
680    fn property(name: &str, type_: Type, default: Expr) -> Property {
681        Property {
682            doc: Doc::none(),
683            mutable: true,
684            name: Ident::new(name),
685            type_,
686            default: Some(default),
687        }
688    }
689
690    #[test]
691    fn a_data_class_indents_by_four_and_ends_every_property_but_the_last_in_a_comma() {
692        let text = decl(&data_class(alloc::vec![
693            property("x", Type::Primitive(Primitive::Int), Expr::int(0)),
694            property("y", Type::Primitive(Primitive::Double), Expr::int(0)),
695        ]));
696        assert_eq!(
697            text,
698            "data class S(\n    var x: Int = 0,\n    var y: Double = 0\n)\n"
699        );
700    }
701
702    #[test]
703    fn an_int_array_is_the_specialised_form_and_a_named_type_is_boxed() {
704        assert_eq!(
705            type_(&Type::array(Type::Primitive(Primitive::Int))),
706            "IntArray"
707        );
708        assert_eq!(type_(&Type::array(Type::named("Pose"))), "Array<Pose>");
709        // A nullable primitive cannot live in the specialised form: no
710        // `null` fits an unboxed `Int`.
711        assert_eq!(
712            type_(&Type::array(Type::nullable(Type::Primitive(
713                Primitive::Int
714            )))),
715            "Array<Int?>"
716        );
717    }
718
719    #[test]
720    fn the_array_literal_factory_matches_the_array_type() {
721        assert_eq!(
722            expr_text(&Expr::ArrayOf {
723                element: alloc::boxed::Box::new(Type::Primitive(Primitive::Int)),
724                elements: alloc::vec![Expr::int(1), Expr::int(2)],
725            }),
726            "intArrayOf(1, 2)"
727        );
728        assert_eq!(
729            expr_text(&Expr::ArrayOf {
730                element: alloc::boxed::Box::new(Type::named("Pose")),
731                elements: alloc::vec![Expr::name("a")],
732            }),
733            "arrayOf<Pose>(a)"
734        );
735    }
736
737    #[test]
738    fn a_constructor_call_writes_no_new() {
739        assert_eq!(
740            expr_text(&Expr::New {
741                type_: alloc::boxed::Box::new(Type::named("Pose")),
742                args: Vec::new(),
743            }),
744            "Pose()"
745        );
746    }
747
748    #[test]
749    fn a_nul_is_written_as_a_unicode_escape_because_kotlin_has_no_backslash_zero() {
750        assert_eq!(string_text("a\0b"), "a\\u0000b");
751        assert_eq!(string_text("a\"b\\c"), "a\\\"b\\\\c");
752        assert_eq!(string_text("price: $5"), "price: \\$5");
753    }
754
755    #[test]
756    fn an_object_of_constants_holds_an_enumerations_values() {
757        let object = Object {
758            doc: Doc::none(),
759            name: Ident::new("Mode"),
760            members: alloc::vec![
761                ConstDecl {
762                    doc: Doc::none(),
763                    is_const: true,
764                    name: Ident::new("IDLE"),
765                    type_: Type::Primitive(Primitive::Int),
766                    value: Expr::int(0),
767                },
768                ConstDecl {
769                    doc: Doc::none(),
770                    is_const: true,
771                    name: Ident::new("ACTIVE"),
772                    type_: Type::Primitive(Primitive::Int),
773                    value: Expr::int(4),
774                },
775            ],
776        };
777        assert_eq!(
778            decl(&Decl::Object(object)),
779            "object Mode {\n    const val IDLE: Int = 0\n    const val ACTIVE: Int = 4\n}\n"
780        );
781    }
782
783    #[test]
784    fn a_file_has_no_wrapper_class_around_its_declarations() {
785        let text = file(&File {
786            doc: Doc::line("Generated."),
787            package: Some(Path::single("zerodds")),
788            imports: Vec::new(),
789            decls: alloc::vec![Decl::Alias(AliasDecl {
790                doc: Doc::none(),
791                name: Ident::new("Distance"),
792                target: Type::Primitive(Primitive::Int),
793            })],
794        });
795        assert!(text.contains("package zerodds\n"), "{text}");
796        assert!(text.contains("typealias Distance = Int\n"), "{text}");
797        assert!(!text.contains("class "), "{text}");
798    }
799}