1use crate::writer::{Writer, comment_lines, escape_string, verbatim_lines};
23use alloc::string::{String, ToString};
24use alloc::vec::Vec;
25use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
26use zerodds_lang_cst::d::{
27 AliasDecl, ConstDecl, Decl, EnumDecl, EnumMember, Expr, Field, File, FuncDecl, Import,
28 InterfaceDecl, Member, Method, Param, Stmt, StructDecl, Type, Uda,
29};
30
31#[must_use]
33pub fn file(file: &File) -> String {
34 let mut w = Writer::spaces4();
35 doc(&mut w, &file.doc);
36 let mut head = String::from("module ");
37 head.push_str(&path_text(&file.module));
38 head.push(';');
39 w.line(&head);
40 if !file.imports.is_empty() {
41 w.blank_line();
42 for import in &file.imports {
43 w.line(&import_text(import));
44 }
45 }
46 for decl in &file.decls {
47 w.blank_line();
48 render_decl(&mut w, decl);
49 }
50 w.finish()
51}
52
53#[must_use]
55pub fn decl(node: &Decl) -> String {
56 let mut w = Writer::spaces4();
57 render_decl(&mut w, node);
58 w.finish()
59}
60
61#[must_use]
63pub fn type_(node: &Type) -> String {
64 type_text(node)
65}
66
67#[must_use]
69pub fn ident(name: &Ident) -> String {
70 name.as_str().to_string()
71}
72
73fn path_text(path: &Path) -> String {
75 path.segments()
76 .iter()
77 .map(|segment| segment.as_str())
78 .collect::<Vec<&str>>()
79 .join(".")
80}
81
82fn import_text(import: &Import) -> String {
83 let mut out = String::from("import ");
84 out.push_str(&path_text(&import.path));
85 if !import.symbols.is_empty() {
86 out.push_str(" : ");
87 out.push_str(
88 &import
89 .symbols
90 .iter()
91 .map(Ident::as_str)
92 .collect::<Vec<&str>>()
93 .join(", "),
94 );
95 }
96 out.push(';');
97 out
98}
99
100fn render_func(w: &mut Writer, node: &FuncDecl) {
102 doc(w, &node.doc);
103 let mut head = String::new();
104 match &node.returns {
105 Some(returns) => head.push_str(&type_text(returns)),
106 None => head.push_str("void"),
107 }
108 head.push(' ');
109 head.push_str(node.name.as_str());
110 head.push('(');
111 let params: Vec<String> = node.params.iter().map(param_text).collect();
112 head.push_str(¶ms.join(", "));
113 head.push(')');
114 w.line(&head);
115 w.line("{");
116 w.indent();
117 render_stmts(w, &node.body);
118 w.dedent();
119 w.line("}");
120}
121
122fn render_stmts(w: &mut Writer, body: &[Stmt]) {
129 for statement in body {
130 render_stmt(w, statement);
131 }
132}
133
134fn render_stmt(w: &mut Writer, node: &Stmt) {
141 match node {
142 Stmt::Var { name, type_, value } => {
143 let mut line;
144 match (type_, value) {
145 (None, Some(value)) => {
147 line = String::from("auto ");
148 line.push_str(name.as_str());
149 line.push_str(" = ");
150 line.push_str(&expr_text(value));
151 }
152 (Some(type_), value) => {
153 line = type_text(type_);
154 line.push(' ');
155 line.push_str(name.as_str());
156 if let Some(value) = value {
157 line.push_str(" = ");
158 line.push_str(&expr_text(value));
159 }
160 }
161 (None, None) => {
164 line = String::from("auto ");
165 line.push_str(name.as_str());
166 }
167 }
168 line.push(';');
169 w.line(&line);
170 }
171 Stmt::Assign { place, value } => {
172 let mut line = expr_text(place);
173 line.push_str(" = ");
174 line.push_str(&expr_text(value));
175 line.push(';');
176 w.line(&line);
177 }
178 Stmt::Expr(value) => {
179 let mut line = expr_text(value);
180 line.push(';');
181 w.line(&line);
182 }
183 Stmt::Return(value) => match value {
184 Some(value) => {
185 let mut line = String::from("return ");
186 line.push_str(&expr_text(value));
187 line.push(';');
188 w.line(&line);
189 }
190 None => w.line("return;"),
191 },
192 Stmt::If {
193 condition,
194 then,
195 otherwise,
196 } => render_if(w, condition, then, otherwise.as_deref(), "if ("),
197 Stmt::For {
198 counter,
199 type_,
200 bound,
201 body,
202 } => {
203 let mut line = String::from("for (");
204 line.push_str(&type_text(type_));
205 line.push(' ');
206 line.push_str(counter.as_str());
207 line.push_str(" = 0; ");
208 line.push_str(counter.as_str());
209 line.push_str(" < ");
210 line.push_str(&expr_text(bound));
211 line.push_str("; ");
212 line.push_str(counter.as_str());
213 line.push_str("++)");
214 w.line(&line);
215 w.line("{");
216 w.indent();
217 render_stmts(w, body);
218 w.dedent();
219 w.line("}");
220 }
221 Stmt::While { condition, body } => {
222 let mut line = String::from("while (");
223 line.push_str(&expr_text(condition));
224 line.push(')');
225 w.line(&line);
226 w.line("{");
227 w.indent();
228 render_stmts(w, body);
229 w.dedent();
230 w.line("}");
231 }
232 Stmt::Break => w.line("break;"),
233 Stmt::Comment(lines) => doc(w, lines),
234 }
235}
236
237fn render_if(
254 w: &mut Writer,
255 condition: &Expr,
256 then: &[Stmt],
257 otherwise: Option<&[Stmt]>,
258 head: &str,
259) {
260 let mut condition = condition;
261 let mut then = then;
262 let mut otherwise = otherwise;
263 let mut head = head;
264 loop {
265 let mut line = String::from(head);
266 line.push_str(&expr_text(condition));
267 line.push(')');
268 w.line(&line);
269 w.line("{");
270 w.indent();
271 render_stmts(w, then);
272 w.dedent();
273 w.line("}");
274 match otherwise {
275 None => return,
276 Some(
277 [
278 Stmt::If {
279 condition: next_condition,
280 then: next_then,
281 otherwise: next_otherwise,
282 },
283 ],
284 ) => {
285 condition = next_condition;
286 then = next_then;
287 otherwise = next_otherwise.as_deref();
288 head = "else if (";
289 }
290 Some(branch) => {
291 w.line("else");
292 w.line("{");
293 w.indent();
294 render_stmts(w, branch);
295 w.dedent();
296 w.line("}");
297 return;
298 }
299 }
300 }
301}
302
303fn render_decl(w: &mut Writer, node: &Decl) {
304 match node {
305 Decl::Struct(decl) => render_struct(w, decl),
306 Decl::Enum(decl) => render_enum(w, decl),
307 Decl::Alias(decl) => render_alias(w, decl),
308 Decl::Const(decl) => render_const(w, decl),
309 Decl::Interface(decl) => render_interface(w, decl),
310 Decl::Func(decl) => render_func(w, decl),
311 Decl::Comment(lines) => doc(w, lines),
312 Decl::Verbatim(text) => verbatim_lines(w, text),
313 }
314}
315
316fn render_struct(w: &mut Writer, node: &StructDecl) {
317 doc(w, &node.doc);
318 let mut head = String::from("struct ");
319 head.push_str(node.name.as_str());
320 w.line(&head);
321 w.line("{");
322 w.indent();
323 for member in &node.members {
324 render_member(w, member);
325 }
326 w.dedent();
327 w.line("}");
328}
329
330fn render_member(w: &mut Writer, node: &Member) {
331 match node {
332 Member::Field(field) => render_field(w, field),
333 Member::Verbatim(text) => verbatim_lines(w, text),
334 Member::Union(fields) => {
335 if fields.is_empty() {
338 return;
339 }
340 w.line("union");
341 w.line("{");
342 w.indent();
343 for field in fields {
344 render_field(w, field);
345 }
346 w.dedent();
347 w.line("}");
348 }
349 }
350}
351
352fn render_field(w: &mut Writer, node: &Field) {
353 doc(w, &node.doc);
354 let mut line = String::new();
355 for uda in &node.udas {
356 line.push_str(&uda_text(uda));
357 line.push(' ');
358 }
359 line.push_str(&type_text(&node.type_));
360 line.push(' ');
361 line.push_str(node.name.as_str());
362 line.push(';');
363 w.line(&line);
364}
365
366fn uda_text(uda: &Uda) -> String {
373 let mut out = String::from("@(\"");
374 out.push_str(&escape_string(&uda.text));
375 out.push_str("\")");
376 out
377}
378
379fn render_enum(w: &mut Writer, node: &EnumDecl) {
380 doc(w, &node.doc);
381 let mut head = String::from("enum ");
382 head.push_str(node.name.as_str());
383 if let Some(base) = &node.base {
384 head.push_str(" : ");
385 head.push_str(&type_text(base));
386 }
387 w.line(&head);
388 w.line("{");
389 w.indent();
390 for (index, member) in node.members.iter().enumerate() {
391 let last = index + 1 == node.members.len();
392 render_enum_member(w, member, last);
393 }
394 w.dedent();
395 w.line("}");
396}
397
398fn render_enum_member(w: &mut Writer, node: &EnumMember, last: bool) {
403 doc(w, &node.doc);
404 let mut line = String::from(node.name.as_str());
405 line.push_str(" = ");
406 line.push_str(&expr_text(&node.value));
407 if !last {
408 line.push(',');
409 }
410 w.line(&line);
411}
412
413fn render_alias(w: &mut Writer, node: &AliasDecl) {
414 doc(w, &node.doc);
415 let mut line = String::from("alias ");
419 line.push_str(node.name.as_str());
420 line.push_str(" = ");
421 line.push_str(&type_text(&node.type_));
422 line.push(';');
423 w.line(&line);
424}
425
426fn render_const(w: &mut Writer, node: &ConstDecl) {
435 doc(w, &node.doc);
436 let mut line = String::from("enum ");
437 if let Some(type_) = &node.type_ {
438 line.push_str(&type_text(type_));
439 line.push(' ');
440 }
441 line.push_str(node.name.as_str());
442 line.push_str(" = ");
443 line.push_str(&expr_text(&node.value));
444 line.push(';');
445 w.line(&line);
446}
447
448fn render_interface(w: &mut Writer, node: &InterfaceDecl) {
449 doc(w, &node.doc);
450 let mut head = String::from("interface ");
451 head.push_str(node.name.as_str());
452 w.line(&head);
453 w.line("{");
454 w.indent();
455 for method in &node.methods {
456 render_method(w, method);
457 }
458 w.dedent();
459 w.line("}");
460}
461
462fn render_method(w: &mut Writer, node: &Method) {
463 doc(w, &node.doc);
464 let mut line = match &node.returns {
465 Some(type_) => type_text(type_),
466 None => "void".to_string(),
467 };
468 line.push(' ');
469 line.push_str(node.name.as_str());
470 line.push('(');
471 let params: Vec<String> = node.params.iter().map(param_text).collect();
472 line.push_str(¶ms.join(", "));
473 line.push_str(");");
474 w.line(&line);
475}
476
477fn param_text(param: &Param) -> String {
478 let mut out = type_text(¶m.type_);
479 out.push(' ');
480 out.push_str(param.name.as_str());
481 out
482}
483
484fn type_text(node: &Type) -> String {
495 match node {
496 Type::Primitive(primitive) => primitive.as_str().to_string(),
497 Type::Named(name) => name.as_str().to_string(),
498 Type::Slice(element) => {
499 let mut out = type_text(element);
500 out.push_str("[]");
501 out
502 }
503 Type::Array { length, element } => {
504 let mut out = type_text(element);
505 out.push('[');
506 out.push_str(&expr_text(length));
507 out.push(']');
508 out
509 }
510 Type::Pointer(target) => {
511 let mut out = type_text(target);
512 out.push('*');
513 out
514 }
515 Type::Nullable(target) => {
519 let mut out = String::from("Nullable!(");
520 out.push_str(&type_text(target));
521 out.push(')');
522 out
523 }
524 }
525}
526
527fn expr_text(node: &Expr) -> String {
534 match node {
535 Expr::Literal(literal) => literal_text(literal),
536 Expr::Name(name) => name.as_str().to_string(),
537 Expr::Binary { op, lhs, rhs } => {
538 let mut out = expr_text(lhs);
539 out.push(' ');
540 out.push_str(op);
541 out.push(' ');
542 out.push_str(&expr_text(rhs));
543 out
544 }
545 Expr::Cast { type_, value } => {
546 let mut out = String::from("cast(");
547 out.push_str(&type_text(type_));
548 out.push(')');
549 out.push_str(&expr_text(value));
550 out
551 }
552 Expr::Paren(inner) => {
553 let mut out = String::from("(");
554 out.push_str(&expr_text(inner));
555 out.push(')');
556 out
557 }
558 Expr::Call { func, args } => {
559 let mut out = expr_text(func);
560 out.push('(');
561 let parts: Vec<String> = args.iter().map(expr_text).collect();
562 out.push_str(&parts.join(", "));
563 out.push(')');
564 out
565 }
566 Expr::Dot { value, name } => {
567 let mut out = expr_text(value);
568 out.push('.');
569 out.push_str(name.as_str());
570 out
571 }
572 Expr::Index { value, index } => {
573 let mut out = expr_text(value);
574 out.push('[');
575 out.push_str(&expr_text(index));
576 out.push(']');
577 out
578 }
579 Expr::SliceTo { value, high } => {
580 let mut out = expr_text(value);
581 out.push_str("[0 .. ");
582 out.push_str(&expr_text(high));
583 out.push(']');
584 out
585 }
586 Expr::Unary { op, value } => {
587 let mut out = String::from(*op);
588 out.push_str(&expr_text(value));
589 out
590 }
591 Expr::StructLit { name, fields } => {
592 let mut out = String::from(name.as_str());
593 out.push('(');
594 let parts: Vec<String> = fields
595 .iter()
596 .map(|(key, value)| match key {
597 Some(key) => {
598 let mut part = String::from(key.as_str());
599 part.push_str(": ");
600 part.push_str(&expr_text(value));
601 part
602 }
603 None => expr_text(value),
604 })
605 .collect();
606 out.push_str(&parts.join(", "));
607 out.push(')');
608 out
609 }
610 Expr::ArrayLit(elements) => {
611 let mut out = String::from("[");
612 let parts: Vec<String> = elements.iter().map(expr_text).collect();
613 out.push_str(&parts.join(", "));
614 out.push(']');
615 out
616 }
617 Expr::NewSlice { element, length } => {
618 let mut out = String::from("new ");
619 out.push_str(&type_text(element));
620 out.push_str("[](");
621 out.push_str(&expr_text(length));
622 out.push(')');
623 out
624 }
625 }
626}
627
628fn literal_text(literal: &Literal) -> String {
629 match literal {
630 Literal::Int(text) | Literal::Float(text) => text.clone(),
631 Literal::Bool(value) => {
632 if *value {
633 "true".to_string()
634 } else {
635 "false".to_string()
636 }
637 }
638 Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
645 Literal::Str(text) => {
646 let mut out = String::from("\"");
647 out.push_str(&escape_string(text));
648 out.push('"');
649 out
650 }
651 Literal::Bytes(bytes) => {
655 let mut out = String::from("[");
656 let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02X}")).collect();
657 out.push_str(&parts.join(", "));
658 out.push(']');
659 out
660 }
661 Literal::Null => "null".to_string(),
662 }
663}
664
665fn doc(w: &mut Writer, doc: &Doc) {
666 if doc.is_empty() {
667 return;
668 }
669 comment_lines(w, "/// ", doc.as_lines());
670}
671
672#[cfg(test)]
673mod tests {
674 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
675 use super::*;
676 use alloc::vec;
677 use zerodds_lang_cst::d::Primitive;
678
679 fn field(name: &str, type_: Type) -> Field {
680 Field {
681 doc: Doc::none(),
682 udas: Vec::new(),
683 name: Ident::new(name),
684 type_,
685 }
686 }
687
688 #[test]
689 fn a_file_declares_its_module_first() {
690 let rendered = file(&File::default());
691 assert_eq!(rendered, "module zdtypes;\n");
692 }
693
694 #[test]
695 fn an_import_is_selective_when_it_names_symbols() {
696 let mut source = File::default();
697 source
698 .imports
699 .push(Import::selective(&["std", "typecons"], "Nullable"));
700 let rendered = file(&source);
701 assert!(
702 rendered.contains("import std.typecons : Nullable;"),
703 "{rendered}"
704 );
705 }
706
707 #[test]
708 fn a_struct_writes_its_brace_on_a_line_of_its_own() {
709 let rendered = decl(&Decl::Struct(StructDecl {
710 doc: Doc::none(),
711 name: Ident::new("Pose"),
712 members: vec![Member::Field(field("x", Type::Primitive(Primitive::Int)))],
713 }));
714 assert_eq!(rendered, "struct Pose\n{\n int x;\n}\n");
715 }
716
717 #[test]
718 fn a_field_carries_its_uda_before_the_type() {
719 let mut node = field("version_", Type::Primitive(Primitive::Int));
720 node.udas.push(Uda::keyed("idl", "version"));
721 let mut w = Writer::spaces4();
722 render_field(&mut w, &node);
723 assert_eq!(w.finish(), "@(\"idl:version\") int version_;\n");
724 }
725
726 #[test]
727 fn an_array_writes_its_dimensions_right_to_left() {
728 let type_ = Type::array(2, Type::array(3, Type::Primitive(Primitive::Int)));
732 assert_eq!(type_text(&type_), "int[3][2]");
733 }
734
735 #[test]
736 fn a_slice_of_a_static_array_keeps_the_two_apart() {
737 let type_ = Type::slice(Type::array(3, Type::Primitive(Primitive::Int)));
740 assert_eq!(type_text(&type_), "int[3][]");
741 let other = Type::array(2, Type::slice(Type::Primitive(Primitive::Int)));
743 assert_eq!(type_text(&other), "int[][2]");
744 }
745
746 #[test]
747 fn a_nullable_parenthesises_its_argument() {
748 let type_ = Type::nullable(Type::slice(Type::Primitive(Primitive::Int)));
752 assert_eq!(type_text(&type_), "Nullable!(int[])");
753 }
754
755 #[test]
756 fn a_pointer_writes_its_star_after_the_type() {
757 assert_eq!(
758 type_text(&Type::pointer(Type::Primitive(Primitive::Void))),
759 "void*"
760 );
761 }
762
763 #[test]
764 fn an_enum_states_its_base_and_every_value() {
765 let rendered = decl(&Decl::Enum(EnumDecl {
766 doc: Doc::none(),
767 name: Ident::new("Mode"),
768 base: Some(Type::Primitive(Primitive::Int)),
769 members: vec![
770 EnumMember {
771 doc: Doc::none(),
772 name: Ident::new("IDLE"),
773 value: Expr::int(0),
774 },
775 EnumMember {
776 doc: Doc::none(),
777 name: Ident::new("ACTIVE"),
778 value: Expr::int(4),
779 },
780 ],
781 }));
782 assert_eq!(
783 rendered,
784 "enum Mode : int\n{\n IDLE = 0,\n ACTIVE = 4\n}\n"
785 );
786 }
787
788 #[test]
789 fn a_manifest_constant_is_an_enum_and_not_an_immutable() {
790 let rendered = decl(&Decl::Const(ConstDecl {
791 doc: Doc::none(),
792 name: Ident::new("LIMIT"),
793 type_: Some(Type::Primitive(Primitive::UByte)),
794 value: Expr::int(8),
795 }));
796 assert_eq!(rendered, "enum ubyte LIMIT = 8;\n");
797 }
798
799 #[test]
800 fn an_alias_is_written_in_the_new_order() {
801 let rendered = decl(&Decl::Alias(AliasDecl {
802 doc: Doc::none(),
803 name: Ident::new("Distance"),
804 type_: Type::Primitive(Primitive::Int),
805 }));
806 assert_eq!(rendered, "alias Distance = int;\n");
807 }
808
809 #[test]
810 fn an_anonymous_union_nests_inside_the_struct() {
811 let rendered = decl(&Decl::Struct(StructDecl {
812 doc: Doc::none(),
813 name: Ident::new("Payload"),
814 members: vec![
815 Member::Field(field("_d", Type::named("Mode"))),
816 Member::Union(vec![
817 field("idle", Type::Primitive(Primitive::Int)),
818 field("active", Type::Primitive(Primitive::Double)),
819 ]),
820 ],
821 }));
822 assert_eq!(
823 rendered,
824 "struct Payload\n{\n Mode _d;\n union\n {\n int idle;\n \
825 double active;\n }\n}\n"
826 );
827 }
828
829 #[test]
830 fn an_empty_union_member_writes_nothing() {
831 let rendered = decl(&Decl::Struct(StructDecl {
834 doc: Doc::none(),
835 name: Ident::new("Empty"),
836 members: vec![Member::Union(Vec::new())],
837 }));
838 assert_eq!(rendered, "struct Empty\n{\n}\n");
839 }
840
841 #[test]
842 fn an_interface_writes_a_signature_per_method() {
843 let rendered = decl(&Decl::Interface(InterfaceDecl {
844 doc: Doc::none(),
845 name: Ident::new("Control"),
846 methods: vec![
847 Method {
848 doc: Doc::none(),
849 name: Ident::new("go"),
850 returns: None,
851 params: vec![Param {
852 name: Ident::new("target"),
853 type_: Type::named("Pose"),
854 }],
855 },
856 Method {
857 doc: Doc::none(),
858 name: Ident::new("status"),
859 returns: Some(Type::Primitive(Primitive::Int)),
860 params: Vec::new(),
861 },
862 ],
863 }));
864 assert_eq!(
865 rendered,
866 "interface Control\n{\n void go(Pose target);\n int status();\n}\n"
867 );
868 }
869
870 #[test]
871 fn a_documentation_comment_uses_the_ddoc_marker() {
872 let rendered = decl(&Decl::Comment(Doc::lines(&["first", "", "third"])));
873 assert_eq!(rendered, "/// first\n///\n/// third\n");
874 }
875
876 #[test]
877 fn a_character_literal_is_written_as_its_code_point() {
878 assert_eq!(literal_text(&Literal::Char('a')), "97");
879 }
880
881 #[test]
882 fn a_string_literal_is_escaped_for_d() {
883 assert_eq!(
884 literal_text(&Literal::Str("a\"b\n".into())),
885 "\"a\\\"b\\n\""
886 );
887 }
888
889 #[test]
890 fn the_null_literal_is_ds_own_spelling() {
891 assert_eq!(literal_text(&Literal::Null), "null");
892 }
893
894 #[test]
895 fn a_shift_expression_renders_with_its_suffix() {
896 let expr = Expr::Binary {
897 op: "<<",
898 lhs: alloc::boxed::Box::new(Expr::suffixed_int(1, "UL")),
899 rhs: alloc::boxed::Box::new(Expr::int(63)),
900 };
901 assert_eq!(expr_text(&expr), "1UL << 63");
902 }
903
904 #[test]
905 fn a_cast_writes_its_target_in_parentheses() {
906 let expr = Expr::Cast {
907 type_: alloc::boxed::Box::new(Type::Primitive(Primitive::UByte)),
908 value: alloc::boxed::Box::new(Expr::int(3)),
909 };
910 assert_eq!(expr_text(&expr), "cast(ubyte)3");
911 }
912}