1use crate::writer::{Writer, comment_lines, verbatim_lines};
39use alloc::string::{String, ToString};
40use alloc::vec::Vec;
41use zerodds_lang_cst::common::{Doc, Ident, Literal};
42use zerodds_lang_cst::fsharp::{
43 ClassField, Decl, Expr, File, FnParam, LetDecl, Primitive, Stmt, Type, TypeBody, TypeDecl,
44 Variant,
45};
46
47#[must_use]
49pub fn file(file: &File) -> String {
50 let mut w = Writer::spaces4();
51 if !file.doc.is_empty() {
52 comment_lines(&mut w, "// ", file.doc.as_lines());
53 }
54 let mut head = String::from("module rec ");
55 head.push_str(file.module.as_str());
56 w.line(&head);
57 for decl in &file.decls {
58 w.blank_line();
59 render_decl(&mut w, decl);
60 }
61 w.finish()
62}
63
64#[must_use]
66pub fn decl(node: &Decl) -> String {
67 let mut w = Writer::spaces4();
68 render_decl(&mut w, node);
69 w.finish()
70}
71
72#[must_use]
74pub fn type_(node: &Type) -> String {
75 type_text(node)
76}
77
78#[must_use]
82pub fn expr(node: &Expr) -> String {
83 expr_text(node)
84}
85
86#[must_use]
88pub fn ident(name: &Ident) -> String {
89 name.as_str().to_string()
90}
91
92fn render_decl(w: &mut Writer, node: &Decl) {
93 match node {
94 Decl::Type(decl) => render_type_decl(w, decl),
95 Decl::Let(decl) => render_let(w, decl),
96 Decl::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
97 Decl::Verbatim(text) => verbatim_lines(w, text),
98 }
99}
100
101fn render_type_decl(w: &mut Writer, node: &TypeDecl) {
103 doc(w, &node.doc);
104 match &node.body {
105 TypeBody::Alias(target) => {
106 let mut line = String::from("type ");
107 line.push_str(node.name.as_str());
108 line.push_str(" = ");
109 line.push_str(&type_text(target));
110 w.line(&line);
111 }
112 TypeBody::Class(fields) => {
113 let mut head = String::from("type ");
114 head.push_str(node.name.as_str());
115 head.push_str("() =");
116 w.line(&head);
117 w.indent();
118 let text = &node.body_text;
119 let has_text = !text.begin.is_empty() || !text.end.is_empty();
120 if fields.is_empty() && !has_text {
121 w.line("class end");
125 } else if fields.is_empty() {
126 w.line("class");
130 w.indent();
131 for line in text.begin.iter().chain(&text.end) {
132 verbatim_lines(w, line);
133 }
134 w.dedent();
135 w.line("end");
136 } else {
137 for line in &text.begin {
138 verbatim_lines(w, line);
139 }
140 for field in fields {
141 render_class_field(w, field);
142 }
143 for line in &text.end {
144 verbatim_lines(w, line);
145 }
146 }
147 w.dedent();
148 }
149 TypeBody::Enum { backing, variants } => {
150 let mut head = String::from("type ");
151 head.push_str(node.name.as_str());
152 head.push_str(" =");
153 w.line(&head);
154 w.indent();
155 for variant in variants {
156 render_variant(w, backing, variant);
157 }
158 w.dedent();
159 }
160 }
161}
162
163fn render_class_field(w: &mut Writer, node: &ClassField) {
164 doc(w, &node.doc);
165 let mut line = String::from("member val ");
166 line.push_str(node.name.as_str());
167 line.push_str(" : ");
168 line.push_str(&type_text(&node.type_));
169 line.push_str(" = ");
170 line.push_str(&expr_text(&node.default));
171 line.push_str(" with get, set");
172 w.line(&line);
173}
174
175fn render_variant(w: &mut Writer, backing: &Type, node: &Variant) {
178 let _ = backing;
179 doc(w, &node.doc);
180 let mut line = String::from("| ");
181 line.push_str(node.name.as_str());
182 line.push_str(" = ");
183 line.push_str(&expr_text(&node.value));
184 w.line(&line);
185}
186
187fn render_let(w: &mut Writer, node: &LetDecl) {
189 doc(w, &node.doc);
190 let mut head = String::from("let ");
191 head.push_str(node.name.as_str());
192 if node.params.is_empty() {
193 if let Some(type_) = &node.type_ {
194 head.push_str(" : ");
195 head.push_str(&type_text(type_));
196 }
197 } else {
198 for param in &node.params {
199 head.push_str(" (");
200 head.push_str(param_text(param).as_str());
201 head.push(')');
202 }
203 if let Some(type_) = &node.type_ {
204 head.push_str(" : ");
205 head.push_str(&type_text(type_));
206 }
207 }
208 head.push_str(" =");
209 if let [Stmt::Return(value)] = node.body.as_slice() {
212 head.push(' ');
213 head.push_str(&expr_text(value));
214 w.line(&head);
215 return;
216 }
217 w.line(&head);
218 w.indent();
219 for statement in &node.body {
220 render_stmt(w, statement);
221 }
222 w.dedent();
223}
224
225fn param_text(param: &FnParam) -> String {
226 let mut out = String::from(param.name.as_str());
227 out.push_str(": ");
228 out.push_str(&type_text(¶m.type_));
229 out
230}
231
232fn render_stmt(w: &mut Writer, node: &Stmt) {
239 match node {
240 Stmt::Let {
241 name,
242 type_,
243 value,
244 mutable,
245 } => {
246 let mut line = String::from(if *mutable { "let mutable " } else { "let " });
247 line.push_str(name.as_str());
248 if let Some(type_) = type_ {
249 line.push_str(" : ");
250 line.push_str(&type_text(type_));
251 }
252 line.push_str(" = ");
253 line.push_str(&expr_text(value));
254 w.line(&line);
255 }
256 Stmt::Assign { place, value } => {
257 let mut line = expr_text(place);
258 line.push_str(" <- ");
259 line.push_str(&expr_text(value));
260 w.line(&line);
261 }
262 Stmt::Expr(value) => w.line(&expr_text(value)),
263 Stmt::If { .. } => render_if(w, node, "if "),
264 Stmt::CountUp {
265 counter,
266 type_,
267 bound,
268 body,
269 } => {
270 let mut start = String::from("for ");
273 start.push_str(counter.as_str());
274 start.push_str(" = ");
275 start.push_str(&zero_of(type_));
276 start.push_str(" to (");
277 start.push_str(&expr_text(bound));
278 start.push_str(") - ");
279 start.push_str(&one_of(type_));
280 start.push_str(" do");
281 w.line(&start);
282 w.indent();
283 for statement in body {
284 render_stmt(w, statement);
285 }
286 w.dedent();
287 }
288 Stmt::Until { condition, body } => {
289 w.line("try");
290 w.indent();
291 let mut head = String::from("while ");
292 head.push_str(&expr_text(condition));
293 head.push_str(" do");
294 w.line(&head);
295 w.indent();
296 for statement in body {
297 render_stmt(w, statement);
298 }
299 w.dedent();
300 w.dedent();
301 w.line("with");
302 w.indent();
303 w.line(":? ZdWire.LoopBreak -> ()");
304 w.dedent();
305 }
306 Stmt::Leave => w.line("raise (ZdWire.LoopBreak())"),
307 Stmt::GiveBack => w.line("raise (ZdWire.GiveBack())"),
308 Stmt::Guarded { body } => {
309 w.line("try");
310 w.indent();
311 for statement in body {
312 render_stmt(w, statement);
313 }
314 w.dedent();
315 w.line("with");
316 w.indent();
317 w.line(":? ZdWire.GiveBack -> ()");
318 w.dedent();
319 }
320 Stmt::Comment(lines) => comment_lines(w, "// ", lines.as_lines()),
321 Stmt::Return(value) => w.line(&expr_text(value)),
322 }
323}
324
325fn zero_of(type_: &Type) -> String {
329 if *type_ == Type::Primitive(Primitive::UInt32) {
330 "0u".to_string()
331 } else {
332 "0".to_string()
333 }
334}
335
336fn one_of(type_: &Type) -> String {
337 if *type_ == Type::Primitive(Primitive::UInt32) {
338 "1u".to_string()
339 } else {
340 "1".to_string()
341 }
342}
343
344fn render_if(w: &mut Writer, node: &Stmt, keyword: &str) {
360 let mut node = node;
361 let mut keyword = keyword;
362 loop {
363 let Stmt::If {
364 condition,
365 then,
366 otherwise,
367 } = node
368 else {
369 return;
370 };
371 let mut head = String::from(keyword);
372 head.push_str(&expr_text(condition));
373 head.push_str(" then");
374 w.line(&head);
375 w.indent();
376 if then.is_empty() {
377 w.line("()");
378 }
379 for statement in then {
380 render_stmt(w, statement);
381 }
382 w.dedent();
383 match otherwise {
384 None => return,
385 Some(chain) if chain.len() == 1 && matches!(chain[0], Stmt::If { .. }) => {
386 node = &chain[0];
387 keyword = "elif ";
388 }
389 Some(block) => {
390 w.line("else");
391 w.indent();
392 if block.is_empty() {
393 w.line("()");
394 }
395 for statement in block {
396 render_stmt(w, statement);
397 }
398 w.dedent();
399 return;
400 }
401 }
402 }
403}
404
405fn type_text(node: &Type) -> String {
409 match node {
410 Type::Primitive(primitive) => primitive.as_str().to_string(),
411 Type::Named(name) => name.as_str().to_string(),
412 Type::Array(element) => {
413 let mut out = type_text(element);
414 out.push_str(" []");
415 out
416 }
417 Type::Option(inner) => {
418 let mut out = type_text(inner);
419 out.push_str(" option");
420 out
421 }
422 Type::Fn { params, returns } => {
423 if params.is_empty() {
424 let mut out = String::from("unit -> ");
425 out.push_str(&type_text(returns));
426 return out;
427 }
428 let parts: Vec<String> = params.iter().map(type_text).collect();
429 let mut out = parts.join(" -> ");
430 out.push_str(" -> ");
431 out.push_str(&type_text(returns));
432 out
433 }
434 }
435}
436
437fn expr_text(node: &Expr) -> String {
444 match node {
445 Expr::Literal(literal) => literal_text(literal),
446 Expr::Name(name) => name.as_str().to_string(),
447 Expr::Binary { op, lhs, rhs } => {
448 let mut out = expr_text(lhs);
449 out.push(' ');
450 out.push_str(op);
451 out.push(' ');
452 out.push_str(&expr_text(rhs));
453 out
454 }
455 Expr::Paren(inner) => {
456 let mut out = String::from("(");
457 out.push_str(&expr_text(inner));
458 out.push(')');
459 out
460 }
461 Expr::Call { func, args } => {
462 let mut out = expr_text(func);
463 out.push('(');
464 let parts: Vec<String> = args.iter().map(expr_text).collect();
465 out.push_str(&parts.join(", "));
466 out.push(')');
467 out
468 }
469 Expr::CurriedCall { func, args } => {
470 let mut out = expr_text(func);
471 if args.is_empty() {
472 out.push_str(" ()");
477 return out;
478 }
479 for arg in args {
480 out.push_str(" (");
481 out.push_str(&expr_text(arg));
482 out.push(')');
483 }
484 out
485 }
486 Expr::Dot { value, name } => {
487 let mut out = expr_text(value);
488 out.push('.');
489 out.push_str(name.as_str());
490 out
491 }
492 Expr::Index { value, index } => {
493 let mut out = expr_text(value);
494 out.push_str(".[");
495 out.push_str(&expr_text(index));
496 out.push(']');
497 out
498 }
499 Expr::Unary { op, value } => {
500 let mut out = String::from(*op);
501 out.push_str(&expr_text(value));
502 out
503 }
504 Expr::Some_(inner) => {
505 let mut out = String::from("Some (");
506 out.push_str(&expr_text(inner));
507 out.push(')');
508 out
509 }
510 Expr::None_ => "None".to_string(),
511 Expr::New { type_, args } => {
512 let mut out = type_.as_str().to_string();
513 out.push('(');
514 let parts: Vec<String> = args.iter().map(expr_text).collect();
515 out.push_str(&parts.join(", "));
516 out.push(')');
517 out
518 }
519 Expr::ObjectInit { type_, fields } => {
520 let mut out = type_.as_str().to_string();
521 out.push('(');
522 let parts: Vec<String> = fields
523 .iter()
524 .map(|(name, value)| {
525 let mut part = name.as_str().to_string();
526 part.push_str(" = ");
527 part.push_str(&expr_text(value));
528 part
529 })
530 .collect();
531 out.push_str(&parts.join(", "));
532 out.push(')');
533 out
534 }
535 Expr::ArrayLit(elements) => {
536 if elements.is_empty() {
537 return "[||]".to_string();
538 }
539 let mut out = String::from("[| ");
540 let parts: Vec<String> = elements.iter().map(expr_text).collect();
541 out.push_str(&parts.join("; "));
542 out.push_str(" |]");
543 out
544 }
545 Expr::ArrayZeroCreate { element, count } => {
546 let mut out = String::from("Array.zeroCreate<");
547 out.push_str(&type_text(element));
548 out.push_str("> (");
549 out.push_str(&expr_text(count));
550 out.push(')');
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 alloc::format!("{}", *ch as u32)
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) => {
579 let mut out = String::from("[| ");
580 let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("{b}uy")).collect();
581 out.push_str(&parts.join("; "));
582 out.push_str(" |]");
583 out
584 }
585 Literal::Null => "None".to_string(),
586 }
587}
588
589#[must_use]
595pub fn string_text(text: &str) -> String {
596 crate::writer::escape_string(text)
597}
598
599fn doc(w: &mut Writer, doc: &Doc) {
600 if doc.is_empty() {
601 return;
602 }
603 comment_lines(w, "/// ", doc.as_lines());
604}
605
606#[cfg(test)]
607mod tests {
608 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
609 use super::*;
610 use alloc::boxed::Box;
611 use zerodds_lang_cst::fsharp::Primitive;
612
613 fn class_decl(fields: Vec<ClassField>) -> Decl {
614 Decl::Type(TypeDecl {
615 body_text: zerodds_lang_cst::common::BodyText::none(),
616 doc: Doc::none(),
617 name: Ident::new("Pose"),
618 body: TypeBody::Class(fields),
619 })
620 }
621
622 fn field(name: &str, type_: Type, default: Expr) -> ClassField {
623 ClassField {
624 doc: Doc::none(),
625 name: Ident::new(name),
626 type_,
627 default,
628 }
629 }
630
631 #[test]
632 fn a_class_declaration_indents_by_four_and_writes_member_val() {
633 let text = decl(&class_decl(alloc::vec![
634 field("X", Type::Primitive(Primitive::Int32), Expr::int(0)),
635 field(
636 "Y",
637 Type::Primitive(Primitive::Double),
638 Expr::Literal(Literal::Float("0.0".to_string())),
639 ),
640 ]));
641 assert_eq!(
642 text,
643 "type Pose() =\n member val X : int32 = 0 with get, set\n \
644 member val Y : float = 0.0 with get, set\n"
645 );
646 }
647
648 #[test]
649 fn an_empty_class_writes_class_end() {
650 assert_eq!(
651 decl(&class_decl(Vec::new())),
652 "type Pose() =\n class end\n"
653 );
654 }
655
656 #[test]
657 fn a_type_reads_left_to_right_with_no_declarator_to_split() {
658 assert_eq!(
659 type_(&Type::array(Type::array(Type::Primitive(Primitive::Int32)))),
660 "int32 [] []"
661 );
662 assert_eq!(type_(&Type::text()), "string");
663 assert_eq!(type_(&Type::array(Type::named("Entry"))), "Entry []");
664 assert_eq!(
665 type_(&Type::option(Type::Primitive(Primitive::Byte))),
666 "byte option"
667 );
668 }
669
670 #[test]
671 fn an_enum_writes_its_backing_type_once_and_every_value() {
672 let text = decl(&Decl::Type(TypeDecl {
673 body_text: zerodds_lang_cst::common::BodyText::none(),
674 doc: Doc::none(),
675 name: Ident::new("Mode"),
676 body: TypeBody::Enum {
677 backing: Type::Primitive(Primitive::Int32),
678 variants: alloc::vec![
679 Variant {
680 doc: Doc::none(),
681 name: Ident::new("IDLE"),
682 value: Expr::int(0),
683 },
684 Variant {
685 doc: Doc::none(),
686 name: Ident::new("ACTIVE"),
687 value: Expr::int(4),
688 },
689 ],
690 },
691 }));
692 assert_eq!(text, "type Mode =\n | IDLE = 0\n | ACTIVE = 4\n");
693 }
694
695 #[test]
696 fn a_constant_writes_its_ascription_before_its_value() {
697 let text = decl(&Decl::Let(LetDecl {
698 doc: Doc::none(),
699 name: Ident::new("LIMIT"),
700 params: Vec::new(),
701 type_: Some(Type::Primitive(Primitive::Byte)),
702 body: alloc::vec![Stmt::Return(Expr::int(8))],
703 }));
704 assert_eq!(text, "let LIMIT : byte = 8\n");
705 }
706
707 #[test]
708 fn a_function_writes_its_parameters_curried() {
709 let text = decl(&Decl::Let(LetDecl {
710 doc: Doc::none(),
711 name: Ident::new("zd_enc_Pose"),
712 params: alloc::vec![
713 FnParam {
714 name: Ident::new("w"),
715 type_: Type::named("ZdWire.Writer"),
716 },
717 FnParam {
718 name: Ident::new("v"),
719 type_: Type::named("Pose"),
720 },
721 ],
722 type_: Some(Type::Primitive(Primitive::Unit)),
723 body: alloc::vec![Stmt::Return(Expr::Literal(Literal::Bool(true)))],
724 }));
725 assert_eq!(
726 text,
727 "let zd_enc_Pose (w: ZdWire.Writer) (v: Pose) : unit = true\n"
728 );
729 }
730
731 #[test]
732 fn the_module_line_always_carries_rec() {
733 let text = file(&File {
734 doc: Doc::none(),
735 module: Ident::new("ZdTypes"),
736 decls: Vec::new(),
737 });
738 assert!(text.starts_with("module rec ZdTypes\n"), "{text}");
739 }
740
741 #[test]
742 fn a_loop_break_renders_as_a_try_with_and_a_raise() {
743 let text = decl(&Decl::Let(LetDecl {
744 doc: Doc::none(),
745 name: Ident::new("f"),
746 params: Vec::new(),
747 type_: Some(Type::Primitive(Primitive::Unit)),
748 body: alloc::vec![
749 Stmt::Until {
750 condition: Expr::Literal(Literal::Bool(true)),
751 body: alloc::vec![Stmt::Leave],
752 },
753 Stmt::Return(Expr::Literal(Literal::Bool(true))),
754 ],
755 }));
756 assert!(text.contains("try\n"), "{text}");
757 assert!(text.contains("while true do\n"), "{text}");
758 assert!(text.contains("raise (ZdWire.LoopBreak())\n"), "{text}");
759 assert!(text.contains(":? ZdWire.LoopBreak -> ()\n"), "{text}");
760 }
761
762 #[test]
763 fn a_counted_loop_writes_fs_for_syntax() {
764 let text = decl(&Decl::Let(LetDecl {
765 doc: Doc::none(),
766 name: Ident::new("f"),
767 params: Vec::new(),
768 type_: Some(Type::Primitive(Primitive::Unit)),
769 body: alloc::vec![
770 Stmt::CountUp {
771 counter: Ident::new("i"),
772 type_: Box::new(Type::Primitive(Primitive::UInt32)),
773 bound: Box::new(Expr::Name(Ident::new("n"))),
774 body: alloc::vec![Stmt::Expr(Expr::Name(Ident::new("i")))],
775 },
776 Stmt::Return(Expr::Literal(Literal::Bool(true))),
777 ],
778 }));
779 assert!(text.contains("for i = 0u to (n) - 1u do\n"), "{text}");
780 }
781
782 #[test]
783 fn a_doc_comment_sits_above_the_field_it_belongs_to() {
784 let mut documented = field("type_", Type::Primitive(Primitive::Int32), Expr::int(0));
785 documented.doc = Doc::line("IDL: `type`.");
786 let text = decl(&class_decl(alloc::vec![documented]));
787 assert_eq!(
788 text,
789 "type Pose() =\n /// IDL: `type`.\n member val type_ : int32 = 0 with get, set\n"
790 );
791 }
792}