1use crate::writer::{Writer, verbatim_lines};
37use alloc::format;
38use alloc::string::{String, ToString};
39use alloc::vec::Vec;
40use zerodds_lang_cst::common::{Doc, Ident, Literal};
41use zerodds_lang_cst::nim::{
42 ConstDecl, EnumField, Expr, Field, File, Import, Param, ProcDecl, Section, Stmt, Type,
43 TypeBody, TypeDecl,
44};
45
46const UNIT: &str = " ";
51
52#[must_use]
54pub fn file(node: &File) -> String {
55 let mut w = Writer::new(UNIT);
56 comment(&mut w, &node.doc);
57 if !node.imports.is_empty() {
58 w.blank_line();
59 for import in &node.imports {
60 w.line(&import_text(import));
61 }
62 }
63 for section in &node.sections {
64 w.blank_line();
65 render_section(&mut w, section);
66 }
67 w.finish()
68}
69
70#[must_use]
72pub fn section(node: &Section) -> String {
73 let mut w = Writer::new(UNIT);
74 render_section(&mut w, node);
75 w.finish()
76}
77
78#[must_use]
80pub fn type_(node: &Type) -> String {
81 type_text(node)
82}
83
84#[must_use]
86pub fn expr(node: &Expr) -> String {
87 expr_text(node)
88}
89
90#[must_use]
92pub fn ident(name: &Ident) -> String {
93 name.as_str().to_string()
94}
95
96fn import_text(node: &Import) -> String {
97 format!("import {}", node.path)
98}
99
100fn render_section(w: &mut Writer, node: &Section) {
106 match node {
107 Section::Type(decls) => {
108 if decls.is_empty() {
109 return;
110 }
111 w.line("type");
112 w.indent();
113 for (index, decl) in decls.iter().enumerate() {
114 if index > 0 {
115 w.blank_line();
116 }
117 render_type_decl(w, decl);
118 }
119 w.dedent();
120 }
121 Section::Const(decls) => {
122 if decls.is_empty() {
123 return;
124 }
125 w.line("const");
126 w.indent();
127 for decl in decls {
128 render_const(w, decl);
129 }
130 w.dedent();
131 }
132 Section::Comment(doc) => comment(w, doc),
133 Section::Verbatim(text) => verbatim_lines(w, text),
134 }
135}
136
137fn render_type_decl(w: &mut Writer, node: &TypeDecl) {
138 for text in &node.text.before {
139 verbatim_lines(w, text);
140 }
141 render_type_decl_proper(w, node);
142 for text in &node.text.after {
143 verbatim_lines(w, text);
144 }
145}
146
147fn render_type_decl_proper(w: &mut Writer, node: &TypeDecl) {
148 comment(w, &node.doc);
152 let head = format!("{} = ", name_text(&node.name, node.exported));
153 match &node.body {
154 TypeBody::Object(fields) => {
155 w.line(&format!("{head}object"));
156 let body = &node.text.body;
157 if fields.is_empty() && body.begin.is_empty() && body.end.is_empty() {
158 return;
159 }
160 w.indent();
161 for text in &body.begin {
162 verbatim_lines(w, text);
163 }
164 for field in fields {
165 render_field(w, field);
166 }
167 for text in &body.end {
168 verbatim_lines(w, text);
169 }
170 w.dedent();
171 }
172 TypeBody::Enum(fields) => {
173 w.line(&format!("{head}enum"));
174 w.indent();
175 for field in fields {
176 render_enum_field(w, field);
177 }
178 w.dedent();
179 }
180 TypeBody::Alias(type_) => w.line(&format!("{head}{}", type_text(type_))),
181 }
182}
183
184fn render_field(w: &mut Writer, node: &Field) {
185 comment(w, &node.doc);
186 w.line(&format!(
187 "{}: {}",
188 name_text(&node.name, node.exported),
189 type_text(&node.type_)
190 ));
191}
192
193fn render_enum_field(w: &mut Writer, node: &EnumField) {
200 comment(w, &node.doc);
201 let mut line = node.name.as_str().to_string();
202 if let Some(value) = &node.value {
203 line.push_str(" = ");
204 line.push_str(&expr_text(value));
205 }
206 w.line(&line);
207}
208
209fn render_const(w: &mut Writer, node: &ConstDecl) {
210 comment(w, &node.doc);
211 let mut line = name_text(&node.name, node.exported);
212 if let Some(type_) = &node.type_ {
213 line.push_str(": ");
214 line.push_str(&type_text(type_));
215 }
216 line.push_str(" = ");
217 line.push_str(&expr_text(&node.value));
218 w.line(&line);
219}
220
221fn name_text(name: &Ident, exported: bool) -> String {
226 let mut out = name.as_str().to_string();
227 if exported {
228 out.push('*');
229 }
230 out
231}
232
233fn type_text(node: &Type) -> String {
237 match node {
238 Type::Primitive(primitive) => primitive.as_str().to_string(),
239 Type::Named(name) => name.as_str().to_string(),
240 Type::Seq(element) => format!("seq[{}]", type_text(element)),
241 Type::Array { length, element } => {
242 format!("array[{}, {}]", expr_text(length), type_text(element))
243 }
244 Type::Option(inner) => format!("Option[{}]", type_text(inner)),
245 Type::Ref(target) => format!("ref {}", type_text(target)),
249 Type::Proc { params, result } => {
250 let parts: Vec<String> = params.iter().map(param_text).collect();
251 let mut out = format!("proc ({})", parts.join(", "));
252 if let Some(result) = result {
253 out.push_str(": ");
254 out.push_str(&type_text(result));
255 }
256 out
257 }
258 }
259}
260
261fn param_text(node: &Param) -> String {
262 format!("{}: {}", node.name.as_str(), type_text(&node.type_))
263}
264
265fn expr_text(node: &Expr) -> String {
266 match node {
267 Expr::Literal(literal) => literal_text(literal),
268 Expr::Name(name) => name.as_str().to_string(),
269 Expr::Binary { op, lhs, rhs } => {
270 format!("{} {op} {}", expr_text(lhs), expr_text(rhs))
271 }
272 Expr::Convert { type_, value } => {
273 format!("{}({})", type_text(type_), expr_text(value))
274 }
275 Expr::Paren(inner) => format!("({})", expr_text(inner)),
276 Expr::Call { func, args } => {
277 let parts: Vec<String> = args.iter().map(expr_text).collect();
278 format!("{}({})", expr_text(func), parts.join(", "))
279 }
280 Expr::Dot { value, name } => format!("{}.{}", expr_text(value), name.as_str()),
281 Expr::Index { value, index } => format!("{}[{}]", expr_text(value), expr_text(index)),
282 Expr::Unary { op, value } => format!("{op} {}", expr_text(value)),
286 Expr::Cast { type_, value } => {
287 format!("cast[{}]({})", type_text(type_), expr_text(value))
288 }
289 Expr::NewSeq { element, length } => {
290 format!("newSeq[{}]({})", type_text(element), expr_text(length))
291 }
292 Expr::Deref(value) => format!("{}[]", expr_text(value)),
293 }
294}
295
296#[must_use]
298pub fn proc_decl(node: &ProcDecl) -> String {
299 let mut w = Writer::new(UNIT);
300 render_proc(&mut w, node);
301 w.finish()
302}
303
304#[must_use]
306pub fn proc_decls(nodes: &[ProcDecl]) -> String {
307 let mut w = Writer::new(UNIT);
308 for (index, node) in nodes.iter().enumerate() {
309 if index > 0 {
310 w.blank_line();
311 }
312 render_proc(&mut w, node);
313 }
314 w.finish()
315}
316
317fn render_proc(w: &mut Writer, node: &ProcDecl) {
318 comment(w, &node.doc);
319 let params: Vec<String> = node.params.iter().map(param_text).collect();
320 let marker = if node.exported { "*" } else { "" };
324 let mut head = format!(
325 "proc {}{}({})",
326 node.name.as_str(),
327 marker,
328 params.join(", ")
329 );
330 if let Some(returns) = &node.returns {
331 head.push_str(": ");
332 head.push_str(&type_text(returns));
333 }
334 let Some(body) = &node.body else {
335 w.line(&head);
339 return;
340 };
341 head.push_str(" =");
342 w.line(&head);
343 w.indent();
344 render_body(w, body);
345 w.dedent();
346}
347
348fn render_body(w: &mut Writer, body: &[Stmt]) {
363 if body.iter().all(|stmt| matches!(stmt, Stmt::Comment(_))) {
364 for stmt in body {
365 render_stmt(w, stmt);
366 }
367 w.line("discard");
368 return;
369 }
370 for stmt in body {
371 render_stmt(w, stmt);
372 }
373}
374
375fn render_stmt(w: &mut Writer, node: &Stmt) {
386 match node {
387 Stmt::Var { name, type_, value } => {
388 let mut line = format!("var {}", name.as_str());
389 if let Some(type_) = type_ {
390 line.push_str(": ");
391 line.push_str(&type_text(type_));
392 }
393 if let Some(value) = value {
394 line.push_str(" = ");
395 line.push_str(&expr_text(value));
396 }
397 w.line(&line);
398 }
399 Stmt::Assign { place, value } => {
400 w.line(&format!("{} = {}", expr_text(place), expr_text(value)));
401 }
402 Stmt::Expr(value) => w.line(&expr_text(value)),
403 Stmt::Discard(value) => w.line(&format!("discard {}", expr_text(value))),
404 Stmt::Return(value) => match value {
405 Some(value) => w.line(&format!("return {}", expr_text(value))),
406 None => w.line("return"),
407 },
408 Stmt::If {
409 condition,
410 then,
411 otherwise,
412 } => {
413 w.line(&format!("if {}:", expr_text(condition)));
414 w.indent();
415 render_body(w, then);
416 w.dedent();
417 if let Some(otherwise) = otherwise {
418 w.line("else:");
419 w.indent();
420 render_body(w, otherwise);
421 w.dedent();
422 }
423 }
424 Stmt::For {
425 counter,
426 bound,
427 body,
428 } => {
429 w.line(&format!(
433 "for {} in 0'u32 ..< {}:",
434 counter.as_str(),
435 expr_text(bound)
436 ));
437 w.indent();
438 render_body(w, body);
439 w.dedent();
440 }
441 Stmt::While { condition, body } => {
442 w.line(&format!("while {}:", expr_text(condition)));
443 w.indent();
444 render_body(w, body);
445 w.dedent();
446 }
447 Stmt::Break => w.line("break"),
448 Stmt::Comment(doc) => comment(w, doc),
449 }
450}
451
452fn literal_text(literal: &Literal) -> String {
453 match literal {
454 Literal::Int(text) | Literal::Float(text) => text.clone(),
455 Literal::Bool(value) => {
456 if *value {
457 "true".to_string()
458 } else {
459 "false".to_string()
460 }
461 }
462 Literal::Char(ch) => char_literal(*ch),
469 Literal::Str(text) => string_literal(text),
470 Literal::Bytes(bytes) => {
473 let parts: Vec<String> = bytes.iter().map(|b| format!("0x{b:02x}'u8")).collect();
474 format!("@[{}]", parts.join(", "))
475 }
476 Literal::Null => "nil".to_string(),
477 }
478}
479
480#[must_use]
491pub fn char_literal(ch: char) -> String {
492 let point = u32::from(ch);
493 match ch {
494 '\'' => "'\\''".to_string(),
495 '\\' => "'\\\\'".to_string(),
496 _ if (0x20..0x7f).contains(&point) => format!("'{ch}'"),
497 _ if point <= 0xff => format!("'\\x{point:02x}'"),
498 _ => format!("{point}"),
503 }
504}
505
506#[must_use]
516pub fn string_literal(text: &str) -> String {
517 let mut out = String::from("\"");
518 out.push_str(&escape_ascii(text));
519 out.push('"');
520 out
521}
522
523fn escape_ascii(text: &str) -> String {
525 let mut out = String::new();
526 let mut buffer = [0_u8; 4];
527 for ch in text.chars() {
528 match ch {
529 '"' => out.push_str("\\\""),
530 '\\' => out.push_str("\\\\"),
531 '\n' => out.push_str("\\n"),
532 '\r' => out.push_str("\\r"),
533 '\t' => out.push_str("\\t"),
534 other => {
535 let point = u32::from(other);
536 if (0x20..0x7f).contains(&point) {
537 out.push(other);
538 } else {
539 for byte in other.encode_utf8(&mut buffer).as_bytes() {
540 out.push_str(&format!("\\x{byte:02x}"));
541 }
542 }
543 }
544 }
545 }
546 out
547}
548
549fn comment(w: &mut Writer, doc: &Doc) {
556 for line in doc.as_lines() {
557 if line.is_empty() {
558 w.line("#");
559 } else {
560 w.line(&format!("# {}", escape_ascii(line)));
561 }
562 }
563}
564
565#[cfg(test)]
566mod tests {
567 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
568 use super::*;
569 use alloc::boxed::Box;
570 use alloc::vec;
571 use zerodds_lang_cst::nim::Primitive;
572
573 fn field(name: &str, type_: Type) -> Field {
574 Field {
575 doc: Doc::none(),
576 name: Ident::new(name),
577 exported: true,
578 type_,
579 }
580 }
581
582 #[test]
583 fn an_object_indents_its_fields_by_two_under_the_type_section() {
584 let text = section(&Section::Type(vec![TypeDecl {
588 text: zerodds_lang_cst::nim::TypeText::default(),
589 doc: Doc::none(),
590 name: Ident::new("Pose"),
591 exported: true,
592 body: TypeBody::Object(vec![
593 field("x", Type::Primitive(Primitive::Int32)),
594 field("y", Type::Primitive(Primitive::Float64)),
595 ]),
596 }]));
597 assert_eq!(
598 text,
599 "type\n Pose* = object\n x*: int32\n y*: float64\n"
600 );
601 }
602
603 #[test]
604 fn an_object_with_no_fields_is_a_bare_object() {
605 let text = section(&Section::Type(vec![TypeDecl {
608 text: zerodds_lang_cst::nim::TypeText::default(),
609 doc: Doc::line("Empty."),
610 name: Ident::new("E"),
611 exported: true,
612 body: TypeBody::Object(Vec::new()),
613 }]));
614 assert_eq!(text, "type\n # Empty.\n E* = object\n");
615 }
616
617 #[test]
618 fn an_enumeration_writes_one_field_per_line_with_no_export_marker() {
619 let text = section(&Section::Type(vec![TypeDecl {
620 text: zerodds_lang_cst::nim::TypeText::default(),
621 doc: Doc::none(),
622 name: Ident::new("Mode"),
623 exported: true,
624 body: TypeBody::Enum(vec![
625 EnumField {
626 doc: Doc::none(),
627 name: Ident::new("IDLE"),
628 value: Some(Expr::int(0)),
629 },
630 EnumField {
631 doc: Doc::none(),
632 name: Ident::new("ACTIVE"),
633 value: Some(Expr::int(4)),
634 },
635 ]),
636 }]));
637 assert_eq!(text, "type\n Mode* = enum\n IDLE = 0\n ACTIVE = 4\n");
638 }
639
640 #[test]
641 fn an_empty_section_writes_nothing_because_the_keyword_alone_is_a_syntax_error() {
642 assert_eq!(section(&Section::Type(Vec::new())), "");
643 assert_eq!(section(&Section::Const(Vec::new())), "");
644 }
645
646 #[test]
647 fn a_const_carries_its_declared_type_before_the_value() {
648 let text = section(&Section::Const(vec![ConstDecl {
649 doc: Doc::none(),
650 name: Ident::new("LIMIT"),
651 exported: true,
652 type_: Some(Type::Primitive(Primitive::UInt8)),
653 value: Expr::int(8),
654 }]));
655 assert_eq!(text, "const\n LIMIT*: uint8 = 8\n");
656 }
657
658 #[test]
659 fn a_bitmask_constant_shifts_in_the_masks_own_width() {
660 let text = expr(&Expr::Binary {
664 op: "shl",
665 lhs: Box::new(Expr::Convert {
666 type_: Box::new(Type::named("Flags")),
667 value: Box::new(Expr::int(1)),
668 }),
669 rhs: Box::new(Expr::int(63)),
670 });
671 assert_eq!(text, "Flags(1) shl 63");
672 }
673
674 #[test]
675 fn every_collection_writes_the_bracket_form_nim_reads() {
676 assert_eq!(
677 type_(&Type::seq(Type::Primitive(Primitive::Int32))),
678 "seq[int32]"
679 );
680 assert_eq!(
681 type_(&Type::array(
682 2,
683 Type::array(3, Type::Primitive(Primitive::Int32))
684 )),
685 "array[2, array[3, int32]]"
686 );
687 assert_eq!(
688 type_(&Type::option(Type::Primitive(Primitive::UInt8))),
689 "Option[uint8]"
690 );
691 }
692
693 #[test]
694 fn a_reference_is_a_prefix_and_takes_no_brackets_of_its_own() {
695 assert_eq!(
699 type_(&Type::option(Type::reference(Type::named("Node")))),
700 "Option[ref Node]"
701 );
702 assert_eq!(
705 type_(&Type::reference(Type::seq(Type::Primitive(
706 Primitive::Int32
707 )))),
708 "ref seq[int32]"
709 );
710 }
711
712 #[test]
713 fn a_procedural_type_writes_its_parameters_and_its_result() {
714 assert_eq!(
715 type_(&Type::Proc {
716 params: vec![Param {
717 name: Ident::new("target"),
718 type_: Type::named("Pose"),
719 }],
720 result: None,
721 }),
722 "proc (target: Pose)"
723 );
724 assert_eq!(
725 type_(&Type::Proc {
726 params: Vec::new(),
727 result: Some(Box::new(Type::Primitive(Primitive::Int32))),
728 }),
729 "proc (): int32"
730 );
731 }
732
733 #[test]
734 fn a_string_literal_leaves_the_file_pure_ascii_one_escape_per_octet() {
735 assert_eq!(string_literal("Z"), "\"Z\"");
736 assert_eq!(string_literal("\u{e4}"), "\"\\xc3\\xa4\"");
739 assert_eq!(string_literal("a\"b\\c"), "\"a\\\"b\\\\c\"");
740 assert_eq!(string_literal("a\nb"), "\"a\\nb\"");
741 }
742
743 #[test]
744 fn a_character_literal_takes_the_hex_escape_where_nim_has_no_letter_one() {
745 assert_eq!(char_literal('a'), "'a'");
746 assert_eq!(char_literal('\''), "'\\''");
747 assert_eq!(char_literal('\n'), "'\\x0a'");
749 }
750
751 #[test]
752 fn a_file_puts_its_imports_above_the_sections_and_below_the_comment() {
753 let text = file(&File {
754 doc: Doc::line("Generated."),
755 imports: vec![Import::new("std/options")],
756 sections: vec![Section::Type(vec![TypeDecl {
757 text: zerodds_lang_cst::nim::TypeText::default(),
758 doc: Doc::none(),
759 name: Ident::new("S"),
760 exported: true,
761 body: TypeBody::Object(vec![field(
762 "a",
763 Type::option(Type::Primitive(Primitive::Int32)),
764 )]),
765 }])],
766 });
767 assert_eq!(
768 text,
769 "# Generated.\n\nimport std/options\n\ntype\n S* = object\n a*: Option[int32]\n"
770 );
771 }
772
773 #[test]
774 fn every_expression_form_writes_the_nim_spelling() {
775 assert_eq!(
776 expr(&Expr::call(
777 Expr::dot(Expr::name("w"), "writeU32"),
778 vec![Expr::dot(Expr::name("v"), "x")],
779 )),
780 "w.writeU32(v.x)"
781 );
782 assert_eq!(
783 expr(&Expr::Index {
784 value: Box::new(Expr::name("v")),
785 index: Box::new(Expr::name("i")),
786 }),
787 "v[i]"
788 );
789 assert_eq!(
790 expr(&Expr::Unary {
791 op: "not",
792 value: Box::new(Expr::name("zd_sent")),
793 }),
794 "not zd_sent"
795 );
796 assert_eq!(
799 expr(&Expr::Cast {
800 type_: Box::new(Type::Primitive(Primitive::Int8)),
801 value: Box::new(Expr::name("k")),
802 }),
803 "cast[int8](k)"
804 );
805 assert_eq!(
806 expr(&Expr::NewSeq {
807 element: Box::new(Type::Primitive(Primitive::Int32)),
808 length: Box::new(Expr::name("zd_n")),
809 }),
810 "newSeq[int32](zd_n)"
811 );
812 assert_eq!(expr(&Expr::Deref(Box::new(Expr::name("p")))), "p[]");
813 }
814
815 #[test]
816 fn a_proc_indents_its_body_and_a_forward_declaration_has_no_equals() {
817 let node = ProcDecl {
818 doc: Doc::none(),
819 exported: false,
820 name: Ident::new("zd_enc_S"),
821 params: vec![
822 Param {
823 name: Ident::new("w"),
824 type_: Type::named("zdwire.Writer"),
825 },
826 Param {
827 name: Ident::new("v"),
828 type_: Type::named("S"),
829 },
830 ],
831 returns: None,
832 body: Some(vec![Stmt::Expr(Expr::call(
833 Expr::dot(Expr::name("w"), "writeBool"),
834 vec![Expr::dot(Expr::name("v"), "a")],
835 ))]),
836 };
837 assert_eq!(
838 proc_decl(&node),
839 "proc zd_enc_S(w: zdwire.Writer, v: S) =\n w.writeBool(v.a)\n"
840 );
841 let forward = ProcDecl { body: None, ..node };
842 assert_eq!(
846 proc_decl(&forward),
847 "proc zd_enc_S(w: zdwire.Writer, v: S)\n"
848 );
849 }
850
851 #[test]
852 fn a_proc_with_a_result_writes_it_and_an_empty_body_is_discard() {
853 let node = ProcDecl {
854 doc: Doc::line("Table 9."),
855 exported: false,
856 name: Ident::new("zd_def_p_long"),
857 params: Vec::new(),
858 returns: Some(Type::Primitive(Primitive::Int32)),
859 body: Some(vec![Stmt::Return(Some(Expr::int(0)))]),
860 };
861 assert_eq!(
862 proc_decl(&node),
863 "# Table 9.\nproc zd_def_p_long(): int32 =\n return 0\n"
864 );
865 let empty = ProcDecl {
866 doc: Doc::none(),
867 exported: false,
868 name: Ident::new("zd_noop"),
869 params: Vec::new(),
870 returns: None,
871 body: Some(Vec::new()),
872 };
873 assert_eq!(proc_decl(&empty), "proc zd_noop() =\n discard\n");
876 }
877
878 #[test]
879 fn every_statement_form_indents_the_way_nim_reads() {
880 let body = vec![
881 Stmt::Var {
882 name: Ident::new("zd_n"),
883 type_: Some(Type::Primitive(Primitive::UInt32)),
884 value: Some(Expr::call(
885 Expr::dot(Expr::name("r"), "readU32"),
886 Vec::new(),
887 )),
888 },
889 Stmt::For {
890 counter: Ident::new("zd_i"),
891 bound: Box::new(Expr::name("zd_n")),
892 body: vec![Stmt::Assign {
893 place: Expr::Index {
894 value: Box::new(Expr::name("v")),
895 index: Box::new(Expr::Convert {
896 type_: Box::new(Type::named("int")),
897 value: Box::new(Expr::name("zd_i")),
898 }),
899 },
900 value: Expr::int(0),
901 }],
902 },
903 Stmt::While {
904 condition: Expr::Literal(Literal::Bool(true)),
905 body: vec![Stmt::Break],
906 },
907 Stmt::If {
908 condition: Expr::name("zd_over"),
909 then: vec![Stmt::Discard(Expr::name("zd_tc"))],
910 otherwise: Some(vec![Stmt::Return(None)]),
911 },
912 Stmt::Comment(Doc::line("done")),
913 ];
914 let node = ProcDecl {
915 doc: Doc::none(),
916 exported: false,
917 name: Ident::new("f"),
918 params: Vec::new(),
919 returns: None,
920 body: Some(body),
921 };
922 assert_eq!(
923 proc_decl(&node),
924 "proc f() =\n\
925 \x20 var zd_n: uint32 = r.readU32()\n\
926 \x20 for zd_i in 0'u32 ..< zd_n:\n\
927 \x20 v[int(zd_i)] = 0\n\
928 \x20 while true:\n\
929 \x20 break\n\
930 \x20 if zd_over:\n\
931 \x20 discard zd_tc\n\
932 \x20 else:\n\
933 \x20 return\n\
934 \x20 # done\n"
935 );
936 }
937
938 #[test]
939 fn no_line_carries_trailing_whitespace_or_a_tab() {
940 let text = file(&File {
944 doc: Doc::lines(&["one", "", "three"]),
945 imports: Vec::new(),
946 sections: vec![
947 Section::Type(vec![TypeDecl {
948 text: zerodds_lang_cst::nim::TypeText::default(),
949 doc: Doc::none(),
950 name: Ident::new("A"),
951 exported: true,
952 body: TypeBody::Object(vec![field("a", Type::Primitive(Primitive::Bool))]),
953 }]),
954 Section::Const(vec![ConstDecl {
955 doc: Doc::none(),
956 name: Ident::new("N"),
957 exported: true,
958 type_: None,
959 value: Expr::int(1),
960 }]),
961 ],
962 });
963 for line in text.lines() {
964 assert_eq!(line, line.trim_end(), "trailing whitespace: {line:?}");
965 assert!(!line.contains('\t'), "a tab reached the output: {line:?}");
966 }
967 }
968}