1use crate::writer::{Writer, comment_lines, verbatim_lines};
36use alloc::string::{String, ToString};
37use alloc::vec::Vec;
38use zerodds_lang_cst::common::{BodyText, Doc, Ident, Literal};
39use zerodds_lang_cst::zig::{
40 AnalyseAll, ConstDecl, Decl, Expr, Field, File, FuncDecl, Param, Stmt, Type, Variant,
41};
42
43#[must_use]
45pub fn file(file: &File) -> String {
46 let mut w = Writer::spaces4();
47 if !file.doc.is_empty() {
48 comment_lines(&mut w, "//! ", file.doc.as_lines());
49 }
50 for decl in &file.decls {
51 w.blank_line();
52 render_decl(&mut w, decl);
53 }
54 w.finish()
55}
56
57#[must_use]
59pub fn decl(node: &Decl) -> String {
60 let mut w = Writer::spaces4();
61 render_decl(&mut w, node);
62 w.finish()
63}
64
65#[must_use]
67pub fn type_(node: &Type) -> String {
68 type_text(node)
69}
70
71#[must_use]
73pub fn ident(name: &Ident) -> String {
74 name.as_str().to_string()
75}
76
77fn render_decl(w: &mut Writer, node: &Decl) {
78 match node {
79 Decl::Const(decl) => render_const(w, decl),
80 Decl::Func(decl) => render_func(w, decl),
81 Decl::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
82 Decl::Verbatim(text) => verbatim_lines(w, text),
83 Decl::AnalyseAll(block) => render_analyse_all(w, block),
84 }
85}
86
87fn render_func(w: &mut Writer, node: &FuncDecl) {
89 doc(w, &node.doc);
90 let mut head = String::new();
91 if node.is_pub {
92 head.push_str("pub ");
93 }
94 head.push_str("fn ");
95 head.push_str(node.name.as_str());
96 head.push('(');
97 let params: Vec<String> = node
98 .params
99 .iter()
100 .map(|param| {
101 let mut out = String::from(param.name.as_str());
102 out.push_str(": ");
103 out.push_str(&type_text(¶m.type_));
104 out
105 })
106 .collect();
107 head.push_str(¶ms.join(", "));
108 head.push_str(") ");
109 head.push_str(&type_text(&node.returns));
110 head.push_str(" {");
111 w.line(&head);
112 w.indent();
113 for statement in &node.body {
114 render_stmt(w, statement);
115 }
116 w.dedent();
117 w.line("}");
118}
119
120fn render_stmt(w: &mut Writer, node: &Stmt) {
127 match node {
128 Stmt::Var {
129 name,
130 type_,
131 value,
132 mutable,
133 } => {
134 let mut line = String::from(if *mutable { "var " } else { "const " });
135 line.push_str(name.as_str());
136 if let Some(type_) = type_ {
137 line.push_str(": ");
138 line.push_str(&type_text(type_));
139 }
140 line.push_str(" = ");
141 match value {
142 Some(value) => line.push_str(&expr_text(value)),
143 None => line.push_str("undefined"),
144 }
145 line.push(';');
146 w.line(&line);
147 }
148 Stmt::Assign { place, value } => {
149 let mut line = expr_text(place);
150 line.push_str(" = ");
151 line.push_str(&expr_text(value));
152 line.push(';');
153 w.line(&line);
154 }
155 Stmt::Expr(value) => {
156 let mut line = expr_text(value);
157 line.push(';');
158 w.line(&line);
159 }
160 Stmt::DiscardExpr(value) => {
161 let mut line = String::from("_ = ");
162 line.push_str(&expr_text(value));
163 line.push(';');
164 w.line(&line);
165 }
166 Stmt::Discard(name) => {
167 let mut line = String::from("_ = ");
168 line.push_str(name.as_str());
169 line.push(';');
170 w.line(&line);
171 }
172 Stmt::Return(value) => match value {
173 Some(value) => {
174 let mut line = String::from("return ");
175 line.push_str(&expr_text(value));
176 line.push(';');
177 w.line(&line);
178 }
179 None => w.line("return;"),
180 },
181 Stmt::If { .. } => render_if(w, node, "if ("),
182 Stmt::CountUp {
183 counter,
184 type_,
185 bound,
186 body,
187 } => {
188 w.line("{");
191 w.indent();
192 let mut line = String::from("var ");
193 line.push_str(counter.as_str());
194 line.push_str(": ");
195 line.push_str(&type_text(type_));
196 line.push_str(" = 0;");
197 w.line(&line);
198 let mut head = String::from("while (");
199 head.push_str(counter.as_str());
200 head.push_str(" < ");
201 head.push_str(&expr_text(bound));
202 head.push_str(") : (");
203 head.push_str(counter.as_str());
204 head.push_str(" += 1) {");
205 w.line(&head);
206 w.indent();
207 for statement in body {
208 render_stmt(w, statement);
209 }
210 w.dedent();
211 w.line("}");
212 w.dedent();
213 w.line("}");
214 }
215 Stmt::While { condition, body } => {
216 let mut head = String::from("while (");
217 head.push_str(&expr_text(condition));
218 head.push_str(") {");
219 w.line(&head);
220 w.indent();
221 for statement in body {
222 render_stmt(w, statement);
223 }
224 w.dedent();
225 w.line("}");
226 }
227 Stmt::Break => w.line("break;"),
228 Stmt::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
229 }
230}
231
232fn render_if(w: &mut Writer, node: &Stmt, keyword: &str) {
249 let mut node = node;
250 let mut keyword = keyword;
251 loop {
252 let Stmt::If {
253 condition,
254 then,
255 otherwise,
256 } = node
257 else {
258 return;
259 };
260 let mut head = String::from(keyword);
261 head.push_str(&expr_text(condition));
262 head.push_str(") {");
263 w.line(&head);
264 w.indent();
265 for statement in then {
266 render_stmt(w, statement);
267 }
268 w.dedent();
269 match otherwise {
270 None => {
271 w.line("}");
272 return;
273 }
274 Some(chain) if chain.len() == 1 && matches!(chain[0], Stmt::If { .. }) => {
275 node = &chain[0];
276 keyword = "} else if (";
277 }
278 Some(block) => {
279 w.line("} else {");
280 w.indent();
281 for statement in block {
282 render_stmt(w, statement);
283 }
284 w.dedent();
285 w.line("}");
286 return;
287 }
288 }
289 }
290}
291
292fn render_const(w: &mut Writer, node: &ConstDecl) {
299 doc(w, &node.doc);
300 let mut head = String::new();
301 if node.is_pub {
302 head.push_str("pub ");
303 }
304 head.push_str("const ");
305 head.push_str(node.name.as_str());
306 if let Some(type_) = &node.type_ {
307 head.push_str(": ");
308 head.push_str(&type_text(type_));
309 }
310 head.push_str(" = ");
311 match &node.value {
312 Expr::Type(type_) => render_container(w, head, type_, &node.body_text),
313 other => {
314 head.push_str(&expr_text(other));
315 head.push(';');
316 w.line(&head);
317 }
318 }
319}
320
321fn render_container(w: &mut Writer, mut head: String, node: &Type, body_text: &BodyText) {
324 let has_text = !body_text.begin.is_empty() || !body_text.end.is_empty();
325 match node {
326 Type::Struct(fields) if !fields.is_empty() || has_text => {
327 head.push_str("struct {");
328 w.line(&head);
329 w.indent();
330 for text in &body_text.begin {
331 verbatim_lines(w, text);
332 }
333 for field in fields {
334 render_field(w, field);
335 }
336 for text in &body_text.end {
337 verbatim_lines(w, text);
338 }
339 w.dedent();
340 w.line("};");
341 }
342 Type::Enum { tag, variants } if !variants.is_empty() => {
343 head.push_str("enum(");
344 head.push_str(&type_text(tag));
345 head.push_str(") {");
346 w.line(&head);
347 w.indent();
348 for variant in variants {
349 render_variant(w, variant);
350 }
351 w.dedent();
352 w.line("};");
353 }
354 other => {
355 head.push_str(&type_text(other));
356 head.push(';');
357 w.line(&head);
358 }
359 }
360}
361
362fn render_field(w: &mut Writer, node: &Field) {
363 doc(w, &node.doc);
364 let mut line = String::from(node.name.as_str());
365 line.push_str(": ");
366 line.push_str(&type_text(&node.type_));
367 if let Some(default) = &node.default {
368 line.push_str(" = ");
369 line.push_str(&expr_text(default));
370 }
371 line.push(',');
372 w.line(&line);
373}
374
375fn render_variant(w: &mut Writer, node: &Variant) {
376 doc(w, &node.doc);
377 let mut line = String::from(node.name.as_str());
378 if let Some(value) = &node.value {
379 line.push_str(" = ");
380 line.push_str(&expr_text(value));
381 }
382 line.push(',');
383 w.line(&line);
384}
385
386fn render_analyse_all(w: &mut Writer, node: &AnalyseAll) {
391 if !node.doc.is_empty() {
392 comment_lines(w, "// ", node.doc.as_lines());
393 }
394 w.line("test {");
402 w.indent();
403 w.line("const zd_walk = struct {");
404 w.indent();
405 w.line("fn all(comptime zd_T: type) void {");
410 w.indent();
411 w.line("inline for (comptime @import(\"std\").meta.declarations(zd_T)) |zd_decl| {");
412 w.indent();
413 w.line("const zd_field = @field(zd_T, zd_decl.name);");
414 w.line("if (@TypeOf(zd_field) == type) {");
415 w.indent();
416 w.line("switch (@typeInfo(zd_field)) {");
417 w.indent();
418 w.line(".@\"struct\", .@\"enum\", .@\"union\", .@\"opaque\" => all(zd_field),");
419 w.line("else => {},");
420 w.dedent();
421 w.line("}");
422 w.dedent();
423 w.line("}");
424 w.line("_ = &zd_field;");
425 w.dedent();
426 w.line("}");
427 w.dedent();
428 w.line("}");
429 w.dedent();
430 w.line("}.all;");
431 w.line("zd_walk(@This());");
432 w.dedent();
433 w.line("}");
434}
435
436fn container_text(node: &Type) -> String {
442 let mut w = Writer::spaces4();
443 render_container(&mut w, String::new(), node, &BodyText::none());
444 w.finish().trim_end().trim_end_matches(';').to_string()
448}
449
450fn type_text(node: &Type) -> String {
454 match node {
455 Type::Primitive(primitive) => primitive.as_str().to_string(),
456 Type::Named(name) => name.as_str().to_string(),
457 Type::Slice { is_const, element } => {
458 let mut out = String::from("[]");
459 if *is_const {
460 out.push_str("const ");
461 }
462 out.push_str(&type_text(element));
463 out
464 }
465 Type::Array { length, element } => {
466 let mut out = String::from("[");
467 out.push_str(&expr_text(length));
468 out.push(']');
469 out.push_str(&type_text(element));
470 out
471 }
472 Type::Optional(inner) => {
473 let mut out = String::from("?");
474 out.push_str(&type_text(inner));
475 out
476 }
477 Type::Pointer { is_const, target } => {
478 let mut out = String::from("*");
479 if *is_const {
480 out.push_str("const ");
481 }
482 out.push_str(&type_text(target));
483 out
484 }
485 Type::Struct(fields) => {
486 if fields.is_empty() {
487 return "struct {}".to_string();
488 }
489 container_text(node)
490 }
491 Type::Enum { tag, variants } => {
492 if variants.is_empty() {
493 let mut out = String::from("enum(");
494 out.push_str(&type_text(tag));
495 out.push_str(") {}");
496 return out;
497 }
498 container_text(node)
499 }
500 Type::Fn { params, returns } => {
502 let mut out = String::from("fn (");
503 let parts: Vec<String> = params.iter().map(param_text).collect();
504 out.push_str(&parts.join(", "));
505 out.push_str(") ");
506 out.push_str(&type_text(returns));
507 out
508 }
509 Type::ErrorUnion { error_set, payload } => {
510 let mut out = type_text(error_set);
511 out.push('!');
512 out.push_str(&type_text(payload));
513 out
514 }
515 }
516}
517
518fn param_text(param: &Param) -> String {
519 let mut out = String::new();
520 if let Some(name) = ¶m.name {
521 out.push_str(name.as_str());
522 out.push_str(": ");
523 }
524 out.push_str(&type_text(¶m.type_));
525 out
526}
527
528fn expr_text(node: &Expr) -> String {
535 match node {
536 Expr::Literal(literal) => literal_text(literal),
537 Expr::Name(name) => name.as_str().to_string(),
538 Expr::Binary { op, lhs, rhs } => {
539 let mut out = expr_text(lhs);
540 out.push(' ');
541 out.push_str(op);
542 out.push(' ');
543 out.push_str(&expr_text(rhs));
544 out
545 }
546 Expr::Paren(inner) => {
547 let mut out = String::from("(");
548 out.push_str(&expr_text(inner));
549 out.push(')');
550 out
551 }
552 Expr::Type(type_) => type_text(type_),
553 Expr::Call { func, args } => {
554 let mut out = expr_text(func);
555 out.push('(');
556 let parts: Vec<String> = args.iter().map(expr_text).collect();
557 out.push_str(&parts.join(", "));
558 out.push(')');
559 out
560 }
561 Expr::Dot { value, name } => {
562 let mut out = expr_text(value);
563 out.push('.');
564 out.push_str(name.as_str());
565 out
566 }
567 Expr::Index { value, index } => {
568 let mut out = expr_text(value);
569 out.push('[');
570 out.push_str(&expr_text(index));
571 out.push(']');
572 out
573 }
574 Expr::Unary { op, value } => {
575 let mut out = String::from(*op);
576 out.push_str(&expr_text(value));
577 out
578 }
579 Expr::Try(value) => {
580 let mut out = String::from("try ");
581 out.push_str(&expr_text(value));
582 out
583 }
584 Expr::StructLit(fields) => {
585 if fields.is_empty() {
586 return ".{}".to_string();
587 }
588 let mut out = String::from(".{ ");
589 let parts: Vec<String> = fields
590 .iter()
591 .map(|(name, value)| {
592 let mut part = String::from(".");
593 part.push_str(name.as_str());
594 part.push_str(" = ");
595 part.push_str(&expr_text(value));
596 part
597 })
598 .collect();
599 out.push_str(&parts.join(", "));
600 out.push_str(" }");
601 out
602 }
603 Expr::ListLit(elements) => {
604 if elements.is_empty() {
605 return ".{}".to_string();
606 }
607 let mut out = String::from(".{ ");
608 let parts: Vec<String> = elements.iter().map(expr_text).collect();
609 out.push_str(&parts.join(", "));
610 out.push_str(" }");
611 out
612 }
613 }
614}
615
616fn literal_text(literal: &Literal) -> String {
617 match literal {
618 Literal::Int(text) | Literal::Float(text) => text.clone(),
619 Literal::Bool(value) => {
620 if *value {
621 "true".to_string()
622 } else {
623 "false".to_string()
624 }
625 }
626 Literal::Char(ch) => {
627 let mut out = String::from("'");
628 out.push_str(&char_text(*ch));
629 out.push('\'');
630 out
631 }
632 Literal::Str(text) => {
633 let mut out = String::from("\"");
634 out.push_str(&string_text(text));
635 out.push('"');
636 out
637 }
638 Literal::Bytes(bytes) => {
642 let mut out = String::from("&[_]u8{ ");
643 let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("0x{b:02x}")).collect();
644 out.push_str(&parts.join(", "));
645 out.push_str(" }");
646 out
647 }
648 Literal::Null => "null".to_string(),
649 }
650}
651
652#[must_use]
667pub fn string_text(text: &str) -> String {
668 let mut out = String::new();
669 for ch in text.chars() {
670 push_escaped(&mut out, ch, '"');
671 }
672 out
673}
674
675#[must_use]
677pub fn char_text(ch: char) -> String {
678 let mut out = String::new();
679 push_escaped(&mut out, ch, '\'');
680 out
681}
682
683fn push_escaped(out: &mut String, ch: char, delimiter: char) {
684 match ch {
685 '\\' => out.push_str("\\\\"),
686 '\n' => out.push_str("\\n"),
687 '\r' => out.push_str("\\r"),
688 '\t' => out.push_str("\\t"),
689 c if c == delimiter => {
690 out.push('\\');
691 out.push(c);
692 }
693 c if (c as u32) < 0x20 || c as u32 == 0x7F => {
694 out.push_str(&alloc::format!("\\x{:02x}", c as u32));
695 }
696 other => out.push(other),
697 }
698}
699
700fn doc(w: &mut Writer, doc: &Doc) {
701 if doc.is_empty() {
702 return;
703 }
704 comment_lines(w, "/// ", doc.as_lines());
705}
706
707#[cfg(test)]
708mod tests {
709 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
710 use super::*;
711 use zerodds_lang_cst::zig::Primitive;
712
713 fn struct_decl(fields: Vec<Field>) -> Decl {
714 Decl::Const(ConstDecl {
715 body_text: zerodds_lang_cst::common::BodyText::none(),
716 doc: Doc::none(),
717 is_pub: true,
718 name: Ident::new("S"),
719 type_: None,
720 value: Expr::type_(Type::Struct(fields)),
721 })
722 }
723
724 fn field(name: &str, type_: Type) -> Field {
725 Field {
726 doc: Doc::none(),
727 name: Ident::new(name),
728 type_,
729 default: None,
730 }
731 }
732
733 #[test]
734 fn a_struct_declaration_indents_by_four_and_ends_every_field_in_a_comma() {
735 let text = decl(&struct_decl(alloc::vec![
736 field("x", Type::Primitive(Primitive::I32)),
737 field("y", Type::Primitive(Primitive::F64)),
738 ]));
739 assert_eq!(
740 text,
741 "pub const S = struct {\n x: i32,\n y: f64,\n};\n"
742 );
743 }
744
745 #[test]
746 fn an_empty_struct_stays_on_one_line() {
747 assert_eq!(decl(&struct_decl(Vec::new())), "pub const S = struct {};\n");
748 }
749
750 #[test]
751 fn a_type_reads_left_to_right_with_no_declarator_to_split() {
752 assert_eq!(
753 type_(&Type::array(
754 2,
755 Type::array(3, Type::Primitive(Primitive::I32))
756 )),
757 "[2][3]i32"
758 );
759 assert_eq!(type_(&Type::text()), "[]const u8");
760 assert_eq!(type_(&Type::slice(Type::named("Entry"))), "[]Entry");
761 assert_eq!(
762 type_(&Type::optional(Type::Primitive(Primitive::U8))),
763 "?u8"
764 );
765 assert_eq!(
766 type_(&Type::pointer(Type::Primitive(Primitive::AnyOpaque))),
767 "*anyopaque"
768 );
769 }
770
771 #[test]
772 fn a_function_pointer_carries_its_parameter_names() {
773 let type_ = Type::const_pointer(Type::Fn {
774 params: alloc::vec![Param {
775 name: Some(Ident::new("target")),
776 type_: Type::named("Pose"),
777 }],
778 returns: alloc::boxed::Box::new(Type::Primitive(Primitive::Void)),
779 });
780 assert_eq!(super::type_(&type_), "*const fn (target: Pose) void");
781 }
782
783 #[test]
784 fn an_optional_field_writes_its_absence_as_the_default() {
785 let mut optional = field("b", Type::optional(Type::Primitive(Primitive::U8)));
786 optional.default = Some(Expr::Literal(Literal::Null));
787 let text = decl(&struct_decl(alloc::vec![optional]));
788 assert!(text.contains("b: ?u8 = null,"), "{text}");
789 }
790
791 #[test]
792 fn an_enum_writes_its_tag_type_and_every_value() {
793 let text = decl(&Decl::Const(ConstDecl {
794 body_text: zerodds_lang_cst::common::BodyText::none(),
795 doc: Doc::none(),
796 is_pub: true,
797 name: Ident::new("Mode"),
798 type_: None,
799 value: Expr::type_(Type::Enum {
800 tag: alloc::boxed::Box::new(Type::Primitive(Primitive::I32)),
801 variants: alloc::vec![
802 Variant {
803 doc: Doc::none(),
804 name: Ident::new("IDLE"),
805 value: Some(Expr::int(0)),
806 },
807 Variant {
808 doc: Doc::none(),
809 name: Ident::new("ACTIVE"),
810 value: Some(Expr::int(4)),
811 },
812 ],
813 }),
814 }));
815 assert_eq!(
816 text,
817 "pub const Mode = enum(i32) {\n IDLE = 0,\n ACTIVE = 4,\n};\n"
818 );
819 }
820
821 #[test]
822 fn a_constant_writes_its_ascription_before_its_value() {
823 let text = decl(&Decl::Const(ConstDecl {
824 body_text: zerodds_lang_cst::common::BodyText::none(),
825 doc: Doc::none(),
826 is_pub: true,
827 name: Ident::new("READY"),
828 type_: Some(Type::named("Flags")),
829 value: Expr::Binary {
830 op: "<<",
831 lhs: alloc::boxed::Box::new(Expr::int(1)),
832 rhs: alloc::boxed::Box::new(Expr::int(4)),
833 },
834 }));
835 assert_eq!(text, "pub const READY: Flags = 1 << 4;\n");
836 }
837
838 #[test]
839 fn a_nul_is_written_as_a_hex_escape_because_zig_has_no_backslash_zero() {
840 assert_eq!(string_text("a\0b"), "a\\x00b");
843 assert_eq!(string_text("a\"b\\c"), "a\\\"b\\\\c");
844 assert_eq!(string_text("line\nnext\ttab"), "line\\nnext\\ttab");
845 assert_eq!(string_text("hä"), "hä");
848 }
849
850 #[test]
851 fn the_file_comment_is_the_container_form_and_the_test_block_has_none() {
852 let text = file(&File {
855 doc: Doc::lines(&["Generated.", "", "Do not edit."]),
856 decls: alloc::vec![Decl::AnalyseAll(AnalyseAll {
857 doc: Doc::line("Why this block is here."),
858 })],
859 });
860 assert!(
861 text.starts_with("//! Generated.\n//!\n//! Do not edit.\n"),
862 "{text}"
863 );
864 assert!(text.contains("// Why this block is here.\n"), "{text}");
865 assert!(text.contains("test {\n"), "{text}");
866 assert!(
867 !text.contains("/// Why"),
868 "a documentation comment was attached to a test block\n{text}"
869 );
870 }
871
872 #[test]
873 fn a_doc_comment_sits_above_the_field_it_belongs_to() {
874 let mut documented = field("type_", Type::Primitive(Primitive::I32));
875 documented.doc = Doc::line("IDL: `type`.");
876 let text = decl(&struct_decl(alloc::vec![documented]));
877 assert_eq!(
878 text,
879 "pub const S = struct {\n /// IDL: `type`.\n type_: i32,\n};\n"
880 );
881 }
882}