1use crate::writer::{Writer, comment_lines, escape_string, verbatim_lines};
34use alloc::string::{String, ToString};
35use alloc::vec::Vec;
36use zerodds_lang_cst::common::{Doc, Ident, Literal};
37use zerodds_lang_cst::julia::{
38 AbstractDecl, ConstDecl, Decl, EnumDecl, Enumerator, Expr, Field, File, FunctionDecl,
39 FunctionDef, Statement, StructDecl, Type,
40};
41
42#[must_use]
44pub fn file(file: &File) -> String {
45 let mut w = Writer::spaces4();
46 doc(&mut w, &file.doc);
47 for decl in &file.decls {
48 w.blank_line();
49 render_decl(&mut w, decl);
50 }
51 w.finish()
52}
53
54#[must_use]
56pub fn decl(node: &Decl) -> String {
57 let mut w = Writer::spaces4();
58 render_decl(&mut w, node);
59 w.finish()
60}
61
62#[must_use]
64pub fn type_(node: &Type) -> String {
65 type_text(node)
66}
67
68#[must_use]
70pub fn ident(name: &Ident) -> String {
71 name.as_str().to_string()
72}
73
74#[must_use]
76pub fn statement(node: &Statement) -> String {
77 let mut w = Writer::spaces4();
78 render_statement(&mut w, node);
79 w.finish()
80}
81
82#[must_use]
84pub fn expr(node: &Expr) -> String {
85 expr_text(node)
86}
87
88#[must_use]
93pub fn string_literal(text: &str) -> String {
94 let mut out = String::from("\"");
95 out.push_str(&julia_string(text));
96 out.push('"');
97 out
98}
99
100fn render_decl(w: &mut Writer, node: &Decl) {
101 match node {
102 Decl::Abstract(decl) => render_abstract(w, decl),
103 Decl::Struct(decl) => render_struct(w, decl),
104 Decl::Enum(decl) => render_enum(w, decl),
105 Decl::Const(decl) => render_const(w, decl),
106 Decl::Function(decl) => render_function(w, decl),
107 Decl::FunctionDef(decl) => render_function_def(w, decl),
108 Decl::Comment(lines) => doc(w, lines),
109 Decl::Verbatim(text) => verbatim_lines(w, text),
110 }
111}
112
113fn render_function_def(w: &mut Writer, node: &FunctionDef) {
115 doc(w, &node.doc);
116 let mut head = String::from("function ");
117 head.push_str(node.name.as_str());
118 head.push('(');
119 let params: Vec<String> = node
120 .params
121 .iter()
122 .map(|param| match ¶m.type_ {
123 Some(type_) => {
124 let mut out = String::from(param.name.as_str());
125 out.push_str("::");
126 out.push_str(&type_text(type_));
127 out
128 }
129 None => param.name.as_str().to_string(),
130 })
131 .collect();
132 head.push_str(¶ms.join(", "));
133 head.push(')');
134 if let Some(returns) = &node.returns {
135 head.push_str("::");
136 head.push_str(&type_text(returns));
137 }
138 w.line(&head);
139 w.indent();
140 for statement in &node.body {
141 render_statement(w, statement);
142 }
143 w.dedent();
144 w.line("end");
145}
146
147fn render_statement(w: &mut Writer, node: &Statement) {
158 match node {
159 Statement::Local { name, type_, value } => {
160 let mut line = String::from("local ");
161 line.push_str(name.as_str());
162 if let Some(type_) = type_ {
163 line.push_str("::");
164 line.push_str(&type_text(type_));
165 }
166 if let Some(value) = value {
167 line.push_str(" = ");
168 line.push_str(&expr_text(value));
169 }
170 w.line(&line);
171 }
172 Statement::Assign { place, value } => {
173 let mut line = expr_text(place);
174 line.push_str(" = ");
175 line.push_str(&expr_text(value));
176 w.line(&line);
177 }
178 Statement::Expr(value) => w.line(&expr_text(value)),
179 Statement::If {
180 condition,
181 then,
182 otherwise,
183 } => {
184 let mut head = String::from("if ");
185 head.push_str(&expr_text(condition));
186 w.line(&head);
187 w.indent();
188 for statement in then {
189 render_statement(w, statement);
190 }
191 w.dedent();
192 if let Some(otherwise) = otherwise {
193 w.line("else");
194 w.indent();
195 for statement in otherwise {
196 render_statement(w, statement);
197 }
198 w.dedent();
199 }
200 w.line("end");
201 }
202 Statement::For {
203 counter,
204 iter,
205 body,
206 } => {
207 let mut head = String::from("for ");
208 head.push_str(counter.as_str());
209 head.push_str(" in ");
210 head.push_str(&expr_text(iter));
211 w.line(&head);
212 w.indent();
213 for statement in body {
214 render_statement(w, statement);
215 }
216 w.dedent();
217 w.line("end");
218 }
219 Statement::While { condition, body } => {
220 let mut head = String::from("while ");
221 head.push_str(&expr_text(condition));
222 w.line(&head);
223 w.indent();
224 for statement in body {
225 render_statement(w, statement);
226 }
227 w.dedent();
228 w.line("end");
229 }
230 Statement::Break => w.line("break"),
231 Statement::Return(value) => match value {
232 Some(value) => {
233 let mut line = String::from("return ");
234 line.push_str(&expr_text(value));
235 w.line(&line);
236 }
237 None => w.line("return"),
238 },
239 Statement::Comment(lines) => doc(w, lines),
240 }
241}
242
243fn render_abstract(w: &mut Writer, node: &AbstractDecl) {
244 doc(w, &node.doc);
245 let mut line = String::from("abstract type ");
246 line.push_str(node.name.as_str());
247 line.push_str(" end");
248 w.line(&line);
249}
250
251fn render_struct(w: &mut Writer, node: &StructDecl) {
256 doc(w, &node.doc);
257 let mut head = String::from("struct ");
258 head.push_str(node.name.as_str());
259 if let Some(supertype) = &node.supertype {
260 head.push_str(" <: ");
261 head.push_str(supertype.as_str());
262 }
263 w.line(&head);
264 w.indent();
265 for text in &node.body_text.begin {
266 verbatim_lines(w, text);
267 }
268 for field in &node.fields {
269 render_field(w, field);
270 }
271 for text in &node.body_text.end {
272 verbatim_lines(w, text);
273 }
274 w.dedent();
275 w.line("end");
276}
277
278fn render_field(w: &mut Writer, node: &Field) {
279 doc(w, &node.doc);
280 let mut line = String::from(node.name.as_str());
281 line.push_str("::");
282 line.push_str(&type_text(&node.type_));
283 w.line(&line);
284}
285
286fn render_enum(w: &mut Writer, node: &EnumDecl) {
292 doc(w, &node.doc);
293 let mut head = String::from("@enum ");
294 head.push_str(node.name.as_str());
295 head.push_str("::");
296 head.push_str(node.base.as_str());
297 head.push_str(" begin");
298 w.line(&head);
299 w.indent();
300 for member in &node.members {
301 render_enumerator(w, member);
302 }
303 w.dedent();
304 w.line("end");
305}
306
307fn render_enumerator(w: &mut Writer, node: &Enumerator) {
308 doc(w, &node.doc);
309 let mut line = String::from(node.name.as_str());
310 line.push_str(" = ");
311 line.push_str(&expr_text(&node.value));
312 w.line(&line);
313}
314
315fn render_const(w: &mut Writer, node: &ConstDecl) {
316 doc(w, &node.doc);
317 let mut line = String::from("const ");
318 line.push_str(node.name.as_str());
319 line.push_str(" = ");
320 line.push_str(&expr_text(&node.value));
321 w.line(&line);
322}
323
324fn render_function(w: &mut Writer, node: &FunctionDecl) {
325 doc(w, &node.doc);
326 let mut line = String::from("function ");
327 line.push_str(node.name.as_str());
328 line.push_str(" end");
329 w.line(&line);
330}
331
332fn type_text(node: &Type) -> String {
336 match node {
337 Type::Primitive(primitive) => primitive.as_str().to_string(),
338 Type::Named(name) => name.as_str().to_string(),
339 Type::Vector(element) => {
340 let mut out = String::from("Vector{");
341 out.push_str(&type_text(element));
342 out.push('}');
343 out
344 }
345 Type::NTuple { length, element } => {
346 let mut out = String::from("NTuple{");
347 out.push_str(&expr_text(length));
348 out.push_str(", ");
349 out.push_str(&type_text(element));
350 out.push('}');
351 out
352 }
353 Type::Union(arms) => {
354 let mut out = String::from("Union{");
355 let parts: Vec<String> = arms.iter().map(type_text).collect();
356 out.push_str(&parts.join(", "));
357 out.push('}');
358 out
359 }
360 }
361}
362
363fn expr_text(node: &Expr) -> String {
370 match node {
371 Expr::Literal(literal) => literal_text(literal),
372 Expr::Name(name) => name.as_str().to_string(),
373 Expr::Type(type_) => type_text(type_),
374 Expr::Call { func, args } => {
375 let mut out = type_text(func);
376 out.push('(');
377 let parts: Vec<String> = args.iter().map(expr_text).collect();
378 out.push_str(&parts.join(", "));
379 out.push(')');
380 out
381 }
382 Expr::Binary { op, lhs, rhs } => {
383 let mut out = expr_text(lhs);
384 out.push(' ');
385 out.push_str(op);
386 out.push(' ');
387 out.push_str(&expr_text(rhs));
388 out
389 }
390 Expr::Paren(inner) => {
391 let mut out = String::from("(");
392 out.push_str(&expr_text(inner));
393 out.push(')');
394 out
395 }
396 Expr::FreeCall { func, args } => {
397 let mut out = String::from(func.as_str());
398 out.push('(');
399 let parts: Vec<String> = args.iter().map(expr_text).collect();
400 out.push_str(&parts.join(", "));
401 out.push(')');
402 out
403 }
404 Expr::Field { value, name } => {
405 let mut out = expr_text(value);
406 out.push('.');
407 out.push_str(name.as_str());
408 out
409 }
410 Expr::Index { value, index } => {
411 let mut out = expr_text(value);
412 out.push('[');
413 out.push_str(&expr_text(index));
414 out.push(']');
415 out
416 }
417 Expr::Range { start, stop } => {
421 let mut out = expr_text(start);
422 out.push(':');
423 out.push_str(&expr_text(stop));
424 out
425 }
426 Expr::Unary { op, value } => {
427 let mut out = String::from(*op);
428 out.push_str(&expr_text(value));
429 out
430 }
431 Expr::AnyVector(items) => {
432 let mut out = String::from("Any[");
433 let parts: Vec<String> = items.iter().map(expr_text).collect();
434 out.push_str(&parts.join(", "));
435 out.push(']');
436 out
437 }
438 }
439}
440
441fn literal_text(literal: &Literal) -> String {
442 match literal {
443 Literal::Int(text) | Literal::Float(text) => text.clone(),
444 Literal::Bool(value) => {
445 if *value {
446 "true".to_string()
447 } else {
448 "false".to_string()
449 }
450 }
451 Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
456 Literal::Str(text) => {
457 let mut out = String::from("\"");
458 out.push_str(&julia_string(text));
459 out.push('"');
460 out
461 }
462 Literal::Bytes(bytes) => {
463 let mut out = String::from("UInt8[");
464 let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02x}")).collect();
465 out.push_str(&parts.join(", "));
466 out.push(']');
467 out
468 }
469 Literal::Null => "nothing".to_string(),
470 }
471}
472
473fn julia_string(text: &str) -> String {
478 let mut out = String::new();
479 for ch in escape_string(text).chars() {
480 if ch == '$' {
481 out.push('\\');
482 }
483 out.push(ch);
484 }
485 out
486}
487
488fn doc(w: &mut Writer, doc: &Doc) {
489 if doc.is_empty() {
490 return;
491 }
492 comment_lines(w, "# ", doc.as_lines());
493}
494
495#[cfg(test)]
496mod tests {
497 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
498 use super::*;
499 use alloc::vec;
500 use zerodds_lang_cst::julia::Primitive;
501
502 fn field(name: &str, type_: Type) -> Field {
503 Field {
504 doc: Doc::none(),
505 name: Ident::new(name),
506 type_,
507 }
508 }
509
510 #[test]
511 fn a_struct_writes_its_fields_with_a_double_colon() {
512 let text = decl(&Decl::Struct(StructDecl {
513 body_text: zerodds_lang_cst::common::BodyText::none(),
514 doc: Doc::none(),
515 name: Ident::new("Pose"),
516 supertype: None,
517 fields: vec![
518 field("x", Type::Primitive(Primitive::Int32)),
519 field("y", Type::Primitive(Primitive::Float64)),
520 ],
521 }));
522 assert_eq!(text, "struct Pose\n x::Int32\n y::Float64\nend\n");
523 }
524
525 #[test]
526 fn an_empty_struct_is_still_a_struct() {
527 let text = decl(&Decl::Struct(StructDecl {
530 body_text: zerodds_lang_cst::common::BodyText::none(),
531 doc: Doc::none(),
532 name: Ident::new("Empty"),
533 supertype: None,
534 fields: Vec::new(),
535 }));
536 assert_eq!(text, "struct Empty\nend\n");
537 }
538
539 #[test]
540 fn a_struct_with_a_supertype_writes_the_subtype_operator() {
541 let text = decl(&Decl::Struct(StructDecl {
542 body_text: zerodds_lang_cst::common::BodyText::none(),
543 doc: Doc::none(),
544 name: Ident::new("B"),
545 supertype: Some(Ident::new("ZdFwd_B")),
546 fields: vec![field("x", Type::Primitive(Primitive::Int32))],
547 }));
548 assert_eq!(text, "struct B <: ZdFwd_B\n x::Int32\nend\n");
549 }
550
551 #[test]
552 fn an_abstract_type_is_one_line() {
553 let text = decl(&Decl::Abstract(AbstractDecl {
554 doc: Doc::none(),
555 name: Ident::new("Control"),
556 }));
557 assert_eq!(text, "abstract type Control end\n");
558 }
559
560 #[test]
561 fn an_enum_writes_its_base_and_every_value() {
562 let text = decl(&Decl::Enum(EnumDecl {
563 doc: Doc::none(),
564 name: Ident::new("Mode"),
565 base: Primitive::Int32,
566 members: vec![
567 Enumerator {
568 doc: Doc::none(),
569 name: Ident::new("IDLE"),
570 value: Expr::int(0),
571 },
572 Enumerator {
573 doc: Doc::none(),
574 name: Ident::new("ACTIVE"),
575 value: Expr::int(4),
576 },
577 ],
578 }));
579 assert_eq!(
580 text,
581 "@enum Mode::Int32 begin\n IDLE = 0\n ACTIVE = 4\nend\n"
582 );
583 }
584
585 #[test]
586 fn a_const_writes_a_type_as_readily_as_a_value() {
587 let alias = decl(&Decl::Const(ConstDecl {
588 doc: Doc::none(),
589 name: Ident::new("Distance"),
590 value: Expr::Type(alloc::boxed::Box::new(Type::Primitive(Primitive::Int32))),
591 }));
592 assert_eq!(alias, "const Distance = Int32\n");
593
594 let value = decl(&Decl::Const(ConstDecl {
595 doc: Doc::none(),
596 name: Ident::new("LIMIT"),
597 value: Expr::convert(Type::Primitive(Primitive::UInt8), Expr::int(8)),
598 }));
599 assert_eq!(value, "const LIMIT = UInt8(8)\n");
600 }
601
602 #[test]
603 fn a_bitmask_constant_is_a_shift_of_a_converted_one() {
604 let text = decl(&Decl::Const(ConstDecl {
605 doc: Doc::none(),
606 name: Ident::new("MOVING"),
607 value: Expr::Binary {
608 op: "<<",
609 lhs: alloc::boxed::Box::new(Expr::convert(Type::named("Flags"), Expr::int(1))),
610 rhs: alloc::boxed::Box::new(Expr::int(4)),
611 },
612 }));
613 assert_eq!(text, "const MOVING = Flags(1) << 4\n");
614 }
615
616 #[test]
617 fn a_function_stub_binds_the_name_and_adds_no_method() {
618 let text = decl(&Decl::Function(FunctionDecl {
619 doc: Doc::line("go(target::Pose)::Nothing"),
620 name: Ident::new("go"),
621 }));
622 assert_eq!(text, "# go(target::Pose)::Nothing\nfunction go end\n");
623 }
624
625 #[test]
626 fn a_comment_declaration_writes_only_comment_lines() {
627 let text = decl(&Decl::Comment(Doc::lines(&["first", "second"])));
628 assert_eq!(text, "# first\n# second\n");
629 }
630
631 #[test]
632 fn every_collection_writes_its_element_inside_braces() {
633 assert_eq!(
634 type_(&Type::vector(Type::Primitive(Primitive::Int32))),
635 "Vector{Int32}"
636 );
637 assert_eq!(
638 type_(&Type::ntuple(
639 2,
640 Type::ntuple(3, Type::Primitive(Primitive::Int32))
641 )),
642 "NTuple{2, NTuple{3, Int32}}"
643 );
644 assert_eq!(
645 type_(&Type::optional(Type::named("Pose"))),
646 "Union{Pose, Nothing}"
647 );
648 }
649
650 #[test]
651 fn a_string_literal_escapes_the_interpolation_marker() {
652 let text = decl(&Decl::Const(ConstDecl {
656 doc: Doc::none(),
657 name: Ident::new("PROMPT"),
658 value: Expr::Literal(Literal::string("$HOME\"x\"")),
659 }));
660 assert_eq!(text, "const PROMPT = \"\\$HOME\\\"x\\\"\"\n");
661 }
662
663 #[test]
664 fn the_other_literal_forms_write_what_julia_reads() {
665 assert_eq!(expr_text(&Expr::Literal(Literal::Bool(true))), "true");
666 assert_eq!(expr_text(&Expr::Literal(Literal::Null)), "nothing");
667 assert_eq!(expr_text(&Expr::Literal(Literal::Char('A'))), "65");
668 assert_eq!(
669 expr_text(&Expr::Literal(Literal::Float("1.5".into()))),
670 "1.5"
671 );
672 assert_eq!(
673 expr_text(&Expr::Literal(Literal::Bytes(alloc::vec![1, 255]))),
674 "UInt8[0x01, 0xff]"
675 );
676 assert_eq!(
677 expr_text(&Expr::Paren(alloc::boxed::Box::new(Expr::Name(
678 Ident::new("x")
679 )))),
680 "(x)"
681 );
682 }
683
684 #[test]
685 fn a_file_separates_its_declarations_with_one_blank_line() {
686 let text = file(&File {
687 doc: Doc::line("Generated."),
688 decls: vec![
689 Decl::Abstract(AbstractDecl {
690 doc: Doc::none(),
691 name: Ident::new("A"),
692 }),
693 Decl::Abstract(AbstractDecl {
694 doc: Doc::none(),
695 name: Ident::new("B"),
696 }),
697 ],
698 });
699 assert_eq!(
700 text,
701 "# Generated.\n\nabstract type A end\n\nabstract type B end\n"
702 );
703 }
704
705 #[test]
706 fn a_named_identifier_renders_as_itself() {
707 assert_eq!(ident(&Ident::new("Pose")), "Pose");
708 }
709
710 #[test]
711 fn the_codec_expressions_render_what_julia_reads() {
712 assert_eq!(
713 expr(&Expr::free_call(
714 "write_u32",
715 vec![Expr::name("w"), Expr::field(Expr::name("v"), "x")]
716 )),
717 "write_u32(w, v.x)"
718 );
719 assert_eq!(
720 expr(&Expr::index(
721 Expr::name("v"),
722 Expr::Binary {
723 op: "+",
724 lhs: alloc::boxed::Box::new(Expr::Paren(alloc::boxed::Box::new(Expr::name(
725 "zd_i"
726 )))),
727 rhs: alloc::boxed::Box::new(Expr::int(1)),
728 }
729 )),
730 "v[(zd_i) + 1]"
731 );
732 assert_eq!(
733 expr(&Expr::range(
734 Expr::int(0),
735 Expr::Paren(alloc::boxed::Box::new(Expr::name("zd_n")))
736 )),
737 "0:(zd_n)"
738 );
739 assert_eq!(
740 expr(&Expr::Unary {
741 op: "!",
742 value: alloc::boxed::Box::new(Expr::name("zd_sent")),
743 }),
744 "!zd_sent"
745 );
746 assert_eq!(
747 expr(&Expr::AnyVector(vec![
748 Expr::Literal(Literal::Null),
749 Expr::int(2)
750 ])),
751 "Any[nothing, 2]"
752 );
753 }
754
755 #[test]
756 fn a_function_definition_renders_params_returns_and_body() {
757 let text = decl(&Decl::FunctionDef(FunctionDef {
758 doc: Doc::none(),
759 name: Ident::new("zd_enc_Pose"),
760 params: vec![
761 zerodds_lang_cst::julia::Param {
762 name: Ident::new("w"),
763 type_: Some(Type::named("ZdWriter")),
764 },
765 zerodds_lang_cst::julia::Param {
766 name: Ident::new("v"),
767 type_: None,
768 },
769 ],
770 returns: None,
771 body: vec![Statement::Expr(Expr::free_call(
772 "write_u32",
773 vec![Expr::name("w"), Expr::field(Expr::name("v"), "x")],
774 ))],
775 }));
776 assert_eq!(
777 text,
778 "function zd_enc_Pose(w::ZdWriter, v)\n write_u32(w, v.x)\nend\n"
779 );
780 }
781
782 #[test]
783 fn the_statement_forms_render_their_julia_shapes() {
784 let local = statement(&Statement::Local {
785 name: Ident::new("zd_n"),
786 type_: Some(Type::Primitive(Primitive::UInt32)),
787 value: Some(Expr::free_call("read_u32", vec![Expr::name("r")])),
788 });
789 assert_eq!(local, "local zd_n::UInt32 = read_u32(r)\n");
790
791 let bare = statement(&Statement::Local {
792 name: Ident::new("zd_h"),
793 type_: None,
794 value: None,
795 });
796 assert_eq!(bare, "local zd_h\n");
797
798 let chain = statement(&Statement::If {
799 condition: Expr::name("a"),
800 then: vec![Statement::Break],
801 otherwise: Some(vec![Statement::Return(Some(Expr::name("v")))]),
802 });
803 assert_eq!(chain, "if a\n break\nelse\n return v\nend\n");
804
805 let counted = statement(&Statement::For {
806 counter: Ident::new("zd_i"),
807 iter: Expr::range(Expr::int(0), Expr::int(2)),
808 body: vec![Statement::Assign {
809 place: Expr::name("x"),
810 value: Expr::name("zd_i"),
811 }],
812 });
813 assert_eq!(counted, "for zd_i in 0:2\n x = zd_i\nend\n");
814
815 let looped = statement(&Statement::While {
816 condition: Expr::name("ok"),
817 body: vec![Statement::Expr(Expr::name("x"))],
818 });
819 assert_eq!(looped, "while ok\n x\nend\n");
820 }
821
822 #[test]
823 fn a_string_literal_helper_matches_the_literal_form() {
824 assert_eq!(string_literal("$HOME\"x\""), "\"\\$HOME\\\"x\\\"\"");
825 }
826
827 #[test]
828 fn an_empty_if_body_still_renders_a_closed_block() {
829 let text = statement(&Statement::If {
832 condition: Expr::name("a"),
833 then: Vec::new(),
834 otherwise: None,
835 });
836 assert_eq!(text, "if a\nend\n");
837 }
838}