1use 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#[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#[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#[must_use]
76pub fn type_(node: &Type) -> String {
77 type_text(node)
78}
79
80#[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
99fn 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
146fn 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
166fn 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
176fn 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
188fn 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(¶ms.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(¶m.type_));
212 out
213}
214
215fn 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
281fn 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
344fn is_specialised(element: &Type) -> bool {
351 matches!(element, Type::Primitive(_))
352}
353
354fn 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
372fn 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
380fn 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 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
407fn 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
446fn 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 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#[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#[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#[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 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}