1use crate::writer::{Writer, comment_lines, verbatim_lines};
25use alloc::format;
26use alloc::string::{String, ToString};
27use alloc::vec::Vec;
28use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
29use zerodds_lang_cst::elixir::{
30 Access, Attribute, CodecFunction, Expr, Field, File, Function, Item, Module, Param, Signature,
31 Statement, TopLevel, Type, TypeAlias,
32};
33
34#[must_use]
36pub fn file(file: &File) -> String {
37 let mut w = Writer::new(" ");
38 if !file.doc.is_empty() {
39 comment_lines(&mut w, "# ", file.doc.as_lines());
40 }
41 for entry in &file.modules {
42 w.blank_line();
43 render_top_level(&mut w, entry);
44 }
45 w.finish()
46}
47
48#[must_use]
50pub fn top_level(node: &TopLevel) -> String {
51 let mut w = Writer::new(" ");
52 render_top_level(&mut w, node);
53 w.finish()
54}
55
56fn render_top_level(w: &mut Writer, node: &TopLevel) {
57 match node {
58 TopLevel::Module(module) => render_module(w, module),
59 TopLevel::Verbatim(text) => verbatim_lines(w, text),
60 }
61}
62
63#[must_use]
65pub fn module(node: &Module) -> String {
66 let mut w = Writer::new(" ");
67 render_module(&mut w, node);
68 w.finish()
69}
70
71#[must_use]
73pub fn item(node: &Item) -> String {
74 let mut w = Writer::new(" ");
75 render_item(&mut w, node);
76 w.finish()
77}
78
79#[must_use]
81pub fn type_(node: &Type) -> String {
82 type_text(node)
83}
84
85#[must_use]
87pub fn expr(node: &Expr) -> String {
88 expr_text(node)
89}
90
91#[must_use]
93pub fn ident(name: &Ident) -> String {
94 name.as_str().to_string()
95}
96
97fn render_module(w: &mut Writer, node: &Module) {
98 let mut head = String::from("defmodule ");
99 head.push_str(&path_text(&node.name));
100 head.push_str(" do");
101 w.line(&head);
102 w.indent();
103 render_moduledoc(w, &node.doc);
104 let mut previous: Option<&Item> = None;
105 for (index, item) in node.body.iter().enumerate() {
106 let separated = match previous {
107 None => !node.doc.is_empty(),
108 Some(previous) => !belong_together(previous, item),
109 };
110 if separated && (index > 0 || !node.doc.is_empty()) {
111 w.blank_line();
112 }
113 render_item(w, item);
114 previous = Some(item);
115 }
116 w.dedent();
117 w.line("end");
118}
119
120fn render_moduledoc(w: &mut Writer, doc: &Doc) {
128 if doc.is_empty() {
129 return;
130 }
131 w.line("@moduledoc \"\"\"");
132 for line in doc.as_lines() {
133 w.line(&escape_heredoc(line));
134 }
135 w.line("\"\"\"");
136}
137
138fn belong_together(previous: &Item, next: &Item) -> bool {
150 match (previous, next) {
151 (Item::Function(before), Item::Function(after)) => {
152 before.name == after.name || (before.params.is_empty() && after.params.is_empty())
153 }
154 (Item::Spec(before), Item::Function(after)) => before.name == after.name,
155 (Item::Callback(_), Item::Callback(_)) => true,
156 _ => false,
157 }
158}
159
160fn render_item(w: &mut Writer, node: &Item) {
161 match node {
162 Item::Attribute(attribute) => render_attribute(w, attribute),
163 Item::Defstruct(fields) => render_defstruct(w, fields),
164 Item::TypeAlias(alias) => render_type_alias(w, alias),
165 Item::Spec(signature) => render_signature(w, "@spec ", signature),
166 Item::Callback(signature) => render_signature(w, "@callback ", signature),
167 Item::Function(function) => render_function(w, function),
168 Item::Verbatim(text) => verbatim_lines(w, text),
169 }
170}
171
172fn render_attribute(w: &mut Writer, node: &Attribute) {
173 doc_comment(w, &node.doc);
174 let mut head = String::from("@");
175 head.push_str(node.name.as_str());
176 head.push(' ');
177 render_value(w, &head, &node.value);
178}
179
180fn render_defstruct(w: &mut Writer, fields: &[Field]) {
186 if fields.is_empty() {
187 w.line("defstruct []");
188 return;
189 }
190 let pairs: Vec<(Ident, Expr)> = fields
191 .iter()
192 .map(|field| (field.name.clone(), field.default.clone()))
193 .collect();
194 render_value(w, "defstruct ", &Expr::Keyword(pairs));
195}
196
197fn render_type_alias(w: &mut Writer, node: &TypeAlias) {
198 doc_comment(w, &node.doc);
199 let mut head = String::from("@type ");
200 head.push_str(node.name.as_str());
201 head.push_str(" :: ");
202 render_type_value(w, &head, &node.type_);
203}
204
205fn render_signature(w: &mut Writer, marker: &str, node: &Signature) {
206 doc_comment(w, &node.doc);
207 let mut line = String::from(marker);
208 line.push_str(node.name.as_str());
209 line.push('(');
210 let params: Vec<String> = node.params.iter().map(param_text).collect();
211 line.push_str(¶ms.join(", "));
212 line.push_str(") :: ");
213 line.push_str(&type_text(&node.result));
214 w.line(&line);
215}
216
217fn param_text(param: &Param) -> String {
218 let mut out = String::new();
219 if let Some(name) = ¶m.name {
220 out.push_str(name.as_str());
221 out.push_str(" :: ");
222 }
223 out.push_str(&type_text(¶m.type_));
224 out
225}
226
227fn render_function(w: &mut Writer, node: &Function) {
233 doc_comment(w, &node.doc);
234 let mut line = String::from("def ");
235 line.push_str(node.name.as_str());
236 if !node.params.is_empty() {
237 line.push('(');
238 let params: Vec<String> = node.params.iter().map(expr_text).collect();
239 line.push_str(¶ms.join(", "));
240 line.push(')');
241 }
242 line.push_str(", do: ");
243 line.push_str(&expr_text(&node.body));
244 w.line(&line);
245}
246
247const LINE_LENGTH: usize = 98;
254
255fn fits(w: &Writer, head: &str, text: &str) -> bool {
257 w.depth()
258 .saturating_mul(2)
259 .saturating_add(head.len())
260 .saturating_add(text.len())
261 <= LINE_LENGTH
262}
263
264fn render_value(w: &mut Writer, head: &str, value: &Expr) {
266 if fits(w, head, &expr_text(value)) {
267 let mut line = String::from(head);
268 line.push_str(&expr_text(value));
269 w.line(&line);
270 return;
271 }
272 match value {
273 Expr::List(items) if items.len() > 1 => {
274 let mut line = String::from(head);
275 line.push('[');
276 w.line(&line);
277 w.indent();
278 for (index, item) in items.iter().enumerate() {
279 let mut text = expr_text(item);
280 if index + 1 < items.len() {
281 text.push(',');
282 }
283 w.line(&text);
284 }
285 w.dedent();
286 w.line("]");
287 }
288 Expr::Keyword(pairs) if pairs.len() > 1 => {
289 let mut line = String::from(head);
290 line.push('[');
291 w.line(&line);
292 w.indent();
293 for (index, (key, item)) in pairs.iter().enumerate() {
294 let mut text = keyword_text(key, item);
295 if index + 1 < pairs.len() {
296 text.push(',');
297 }
298 w.line(&text);
299 }
300 w.dedent();
301 w.line("]");
302 }
303 other => {
304 let mut line = String::from(head);
305 line.push_str(&expr_text(other));
306 w.line(&line);
307 }
308 }
309}
310
311fn render_type_value(w: &mut Writer, head: &str, type_: &Type) {
313 if fits(w, head, &type_text(type_)) {
314 let mut line = String::from(head);
315 line.push_str(&type_text(type_));
316 w.line(&line);
317 return;
318 }
319 match type_ {
320 Type::Struct(fields) if fields.len() > 1 => {
321 let mut line = String::from(head);
322 line.push_str("%__MODULE__{");
323 w.line(&line);
324 w.indent();
325 for (index, (key, field)) in fields.iter().enumerate() {
326 let mut text = String::from(key.as_str());
327 text.push_str(": ");
328 text.push_str(&type_text(field));
329 if index + 1 < fields.len() {
330 text.push(',');
331 }
332 w.line(&text);
333 }
334 w.dedent();
335 w.line("}");
336 }
337 other => {
338 let mut line = String::from(head);
339 line.push_str(&type_text(other));
340 w.line(&line);
341 }
342 }
343}
344
345fn type_text(node: &Type) -> String {
349 match node {
350 Type::Basic(basic) => basic.as_str().to_string(),
351 Type::Atom(name) => atom_text(name),
352 Type::Local(name) => {
353 let mut out = String::from(name.as_str());
354 out.push_str("()");
355 out
356 }
357 Type::Remote { module, name } => {
358 let mut out = path_text(module);
359 out.push('.');
360 out.push_str(name.as_str());
361 out.push_str("()");
362 out
363 }
364 Type::List(element) => {
365 let mut out = String::from("list(");
366 out.push_str(&type_text(element));
367 out.push(')');
368 out
369 }
370 Type::Tuple(items) => {
371 let parts: Vec<String> = items.iter().map(type_text).collect();
372 let mut out = String::from("{");
373 out.push_str(&parts.join(", "));
374 out.push('}');
375 out
376 }
377 Type::Struct(fields) => {
378 let parts: Vec<String> = fields
379 .iter()
380 .map(|(key, field)| {
381 let mut out = String::from(key.as_str());
382 out.push_str(": ");
383 out.push_str(&type_text(field));
384 out
385 })
386 .collect();
387 let mut out = String::from("%__MODULE__{");
388 out.push_str(&parts.join(", "));
389 out.push('}');
390 out
391 }
392 Type::Union(arms) => {
393 let parts: Vec<String> = arms.iter().map(type_text).collect();
394 parts.join(" | ")
395 }
396 }
397}
398
399fn expr_text(node: &Expr) -> String {
406 match node {
407 Expr::Literal(literal) => literal_text(literal),
408 Expr::Atom(name) => atom_text(name),
409 Expr::Var(name) => name.as_str().to_string(),
410 Expr::Attr(name) => {
411 let mut out = String::from("@");
412 out.push_str(name.as_str());
413 out
414 }
415 Expr::Module(path) => path_text(path),
416 Expr::List(items) => {
417 let parts: Vec<String> = items.iter().map(expr_text).collect();
418 let mut out = String::from("[");
419 out.push_str(&parts.join(", "));
420 out.push(']');
421 out
422 }
423 Expr::Tuple(items) => {
424 let parts: Vec<String> = items.iter().map(expr_text).collect();
425 let mut out = String::from("{");
426 out.push_str(&parts.join(", "));
427 out.push('}');
428 out
429 }
430 Expr::Keyword(pairs) => {
431 let parts: Vec<String> = pairs
432 .iter()
433 .map(|(key, value)| keyword_text(key, value))
434 .collect();
435 let mut out = String::from("[");
436 out.push_str(&parts.join(", "));
437 out.push(']');
438 out
439 }
440 Expr::Call { func, args } => {
441 let parts: Vec<String> = args.iter().map(expr_text).collect();
442 let mut out = path_text(func);
443 out.push('(');
444 out.push_str(&parts.join(", "));
445 out.push(')');
446 out
447 }
448 Expr::Binary { op, lhs, rhs } => {
449 format!("{} {op} {}", expr_text(lhs), expr_text(rhs))
450 }
451 Expr::Unary { op, value } => {
452 let mut out = String::from(*op);
453 out.push_str(&expr_text(value));
454 out
455 }
456 Expr::Paren(value) => format!("({})", expr_text(value)),
457 Expr::Field { value, name } => format!("{}.{}", expr_text(value), name.as_str()),
458 Expr::Index { value, index } => {
463 format!("Enum.at({}, {})", expr_text(value), expr_text(index))
464 }
465 Expr::IfElse {
466 condition,
467 then,
468 otherwise,
469 } => format!(
470 "(if {}, do: {}, else: {})",
471 expr_text(condition),
472 expr_text(then),
473 expr_text(otherwise)
474 ),
475 Expr::StructLiteral { module, fields } => {
476 let parts: Vec<String> = fields
477 .iter()
478 .map(|(key, value)| keyword_text(key, value))
479 .collect();
480 format!("%{}{{{}}}", path_text(module), parts.join(", "))
481 }
482 }
483}
484
485fn keyword_text(key: &Ident, value: &Expr) -> String {
492 let mut out = String::new();
493 out.push_str(key.as_str());
494 out.push_str(": ");
495 out.push_str(&expr_text(value));
496 out
497}
498
499fn literal_text(literal: &Literal) -> String {
500 match literal {
501 Literal::Int(text) => text.clone(),
502 Literal::Float(text) => float_text(text),
503 Literal::Bool(value) => {
504 if *value {
505 "true".to_string()
506 } else {
507 "false".to_string()
508 }
509 }
510 Literal::Char(ch) => alloc::format!("{}", u32::from(*ch)),
516 Literal::Str(text) => {
517 let mut out = String::from("\"");
518 out.push_str(&escape_elixir(text));
519 out.push('"');
520 out
521 }
522 Literal::Bytes(bytes) => {
526 let parts: Vec<String> = bytes.iter().map(|b| alloc::format!("{b}")).collect();
527 let mut out = String::from("<<");
528 out.push_str(&parts.join(", "));
529 out.push_str(">>");
530 out
531 }
532 Literal::Null => "nil".to_string(),
533 }
534}
535
536fn float_text(text: &str) -> String {
544 let (mantissa, exponent) = match text.find(['e', 'E']) {
545 Some(at) => (&text[..at], Some(&text[at + 1..])),
546 None => (text, None),
547 };
548 let mut out = String::new();
549 if mantissa.starts_with('.') {
550 out.push('0');
551 } else if let Some(rest) = mantissa.strip_prefix('-').filter(|r| r.starts_with('.')) {
552 out.push_str("-0");
553 out.push_str(rest);
554 return with_exponent(out, exponent);
555 }
556 out.push_str(mantissa);
557 if out.ends_with('.') {
558 out.push('0');
559 }
560 if !out.contains('.') {
561 out.push_str(".0");
562 }
563 with_exponent(out, exponent)
564}
565
566fn with_exponent(mut text: String, exponent: Option<&str>) -> String {
567 if let Some(exponent) = exponent {
568 text.push('e');
569 text.push_str(exponent);
570 }
571 text
572}
573
574fn atom_text(name: &str) -> String {
582 let plain = !name.is_empty()
583 && name
584 .chars()
585 .next()
586 .is_some_and(|first| first.is_ascii_lowercase() || first == '_')
587 && name
588 .chars()
589 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '?' || ch == '!');
590 let mut out = String::from(":");
591 if plain {
592 out.push_str(name);
593 } else {
594 out.push('"');
595 out.push_str(&escape_elixir(name));
596 out.push('"');
597 }
598 out
599}
600
601fn path_text(path: &Path) -> String {
602 path.segments()
603 .iter()
604 .map(|segment| segment.as_str())
605 .collect::<Vec<&str>>()
606 .join(".")
607}
608
609fn escape_elixir(text: &str) -> String {
618 let mut out = String::new();
619 for ch in text.chars() {
620 match ch {
621 '"' => out.push_str("\\\""),
622 '\\' => out.push_str("\\\\"),
623 '\n' => out.push_str("\\n"),
624 '\r' => out.push_str("\\r"),
625 '\t' => out.push_str("\\t"),
626 '\0' => out.push_str("\\0"),
627 '#' => out.push_str("\\#"),
628 other => out.push(other),
629 }
630 }
631 out
632}
633
634fn escape_heredoc(line: &str) -> String {
641 line.replace('#', "\\#")
642}
643
644fn doc_comment(w: &mut Writer, doc: &Doc) {
645 if doc.is_empty() {
646 return;
647 }
648 comment_lines(w, "# ", doc.as_lines());
649}
650
651#[must_use]
657pub fn string_literal(text: &str) -> String {
658 let mut out = String::from("\"");
659 out.push_str(&escape_elixir(text));
660 out.push('"');
661 out
662}
663
664struct Lowering {
691 loops_named: usize,
693 loop_accs: Vec<Vec<String>>,
696 outer_reads: Vec<String>,
704}
705
706#[must_use]
708pub fn codec_function(node: &CodecFunction) -> String {
709 let mut w = Writer::new(" ");
710 doc_comment(&mut w, &node.doc);
711 let mut head = String::from("def ");
712 head.push_str(node.name.as_str());
713 if !node.params.is_empty() {
714 head.push('(');
715 let params: Vec<&str> = node.params.iter().map(Ident::as_str).collect();
716 head.push_str(¶ms.join(", "));
717 head.push(')');
718 }
719 head.push_str(" do");
720 w.line(&head);
721 w.indent();
722 let needs_try = contains_return(&node.body);
723 if needs_try {
724 w.line("try do");
725 w.indent();
726 }
727 let mut bound: Vec<String> = node
728 .params
729 .iter()
730 .map(|param| param.as_str().to_string())
731 .collect();
732 let mut result_reads = Vec::new();
733 expr_reads(&node.result, &mut result_reads);
734 let mut lowering = Lowering {
735 loops_named: 0,
736 loop_accs: Vec::new(),
737 outer_reads: result_reads,
738 };
739 let body = flatten(&node.body);
740 render_statements(&mut w, &body, &mut bound, &mut lowering);
741 w.line(&expr_text(&node.result));
742 if needs_try {
743 w.dedent();
744 w.line("catch");
745 w.indent();
746 w.line(":throw, {:zd_return, zd_ret} -> zd_ret");
747 w.dedent();
748 w.line("end");
749 }
750 w.dedent();
751 w.line("end");
752 w.finish()
753}
754
755fn assigned(statements: &[Statement], out: &mut Vec<String>) {
768 let push = |name: &Ident, out: &mut Vec<String>| {
769 let text = name.as_str();
770 if text != "_" && !out.iter().any(|seen| seen == text) {
771 out.push(text.to_string());
772 }
773 };
774 for statement in statements {
775 match statement {
776 Statement::Bind { name, .. } => push(name, out),
777 Statement::BindTuple { names, .. } => {
778 for name in names {
779 push(name, out);
780 }
781 }
782 Statement::Update { base, .. } => push(base, out),
783 Statement::If {
784 then, otherwise, ..
785 } => {
786 assigned(then, out);
787 assigned(otherwise, out);
788 }
789 Statement::For { body, .. } | Statement::While { body, .. } => assigned(body, out),
791 Statement::Seq(inner) => assigned(inner, out),
792 Statement::Break | Statement::Return(_) | Statement::Comment(_) => {}
793 }
794 }
795}
796
797fn threaded(statements: &[Statement], bound: &[String]) -> Vec<String> {
800 let mut inside = Vec::new();
801 assigned(statements, &mut inside);
802 bound
803 .iter()
804 .filter(|name| inside.iter().any(|assigned| assigned == *name))
805 .cloned()
806 .collect()
807}
808
809fn breaks_at_this_level(statements: &[Statement]) -> bool {
822 statements.iter().any(|statement| match statement {
823 Statement::Break => true,
824 Statement::If {
825 then, otherwise, ..
826 } => breaks_at_this_level(then) || breaks_at_this_level(otherwise),
827 Statement::Seq(inner) => breaks_at_this_level(inner),
828 Statement::For { .. } | Statement::While { .. } => false,
830 _ => false,
831 })
832}
833
834fn contains_return(statements: &[Statement]) -> bool {
847 statements.iter().any(|statement| match statement {
848 Statement::Return(_) => true,
849 Statement::If {
850 then, otherwise, ..
851 } => contains_return(then) || contains_return(otherwise),
852 Statement::For { body, .. } | Statement::While { body, .. } => contains_return(body),
853 Statement::Seq(inner) => contains_return(inner),
854 _ => false,
855 })
856}
857
858fn flatten(statements: &[Statement]) -> Vec<Statement> {
873 let mut out = Vec::new();
874 for statement in statements {
875 match statement {
876 Statement::Seq(inner) => out.extend(flatten(inner)),
877 Statement::If {
878 condition,
879 then,
880 otherwise,
881 } => out.push(Statement::If {
882 condition: condition.clone(),
883 then: flatten(then),
884 otherwise: flatten(otherwise),
885 }),
886 Statement::For {
887 counter,
888 bound,
889 body,
890 } => out.push(Statement::For {
891 counter: counter.clone(),
892 bound: bound.clone(),
893 body: flatten(body),
894 }),
895 Statement::While { condition, body } => out.push(Statement::While {
896 condition: condition.clone(),
897 body: flatten(body),
898 }),
899 other => out.push(other.clone()),
900 }
901 }
902 out
903}
904
905fn expr_reads(expr: &Expr, out: &mut Vec<String>) {
914 match expr {
915 Expr::Var(name) => {
916 if !out.iter().any(|seen| seen == name.as_str()) {
917 out.push(name.as_str().to_string());
918 }
919 }
920 Expr::Call { args, .. } => {
921 for argument in args {
922 expr_reads(argument, out);
923 }
924 }
925 Expr::Binary { lhs, rhs, .. } => {
926 expr_reads(lhs, out);
927 expr_reads(rhs, out);
928 }
929 Expr::Unary { value, .. } | Expr::Paren(value) => expr_reads(value, out),
930 Expr::Field { value, .. } => expr_reads(value, out),
931 Expr::Index { value, index } => {
932 expr_reads(value, out);
933 expr_reads(index, out);
934 }
935 Expr::IfElse {
936 condition,
937 then,
938 otherwise,
939 } => {
940 expr_reads(condition, out);
941 expr_reads(then, out);
942 expr_reads(otherwise, out);
943 }
944 Expr::List(items) | Expr::Tuple(items) => {
945 for item in items {
946 expr_reads(item, out);
947 }
948 }
949 Expr::Keyword(pairs) => {
950 for (_, value) in pairs {
951 expr_reads(value, out);
952 }
953 }
954 Expr::StructLiteral { fields, .. } => {
955 for (_, value) in fields {
956 expr_reads(value, out);
957 }
958 }
959 Expr::Literal(_) | Expr::Atom(_) | Expr::Attr(_) | Expr::Module(_) => {}
960 }
961}
962
963fn expr_mentions(expr: &Expr, name: &str) -> bool {
964 let mut reads = Vec::new();
965 expr_reads(expr, &mut reads);
966 reads.iter().any(|read| read == name)
967}
968
969fn statements_read(statements: &[Statement], name: &str) -> bool {
986 statements.iter().any(|statement| match statement {
987 Statement::Bind { value, .. } | Statement::BindTuple { value, .. } => {
988 expr_mentions(value, name)
989 }
990 Statement::Update { base, path, value } => {
991 base.as_str() == name
992 || expr_mentions(value, name)
993 || path.iter().any(|step| match step {
994 Access::At(index) => expr_mentions(index, name),
995 Access::Key(_) => false,
996 })
997 }
998 Statement::If {
999 condition,
1000 then,
1001 otherwise,
1002 } => {
1003 let mut inside = Vec::new();
1004 assigned(then, &mut inside);
1005 assigned(otherwise, &mut inside);
1006 expr_mentions(condition, name)
1007 || inside.iter().any(|bound| bound == name)
1008 || statements_read(then, name)
1009 || statements_read(otherwise, name)
1010 }
1011 Statement::For { bound, body, .. } => {
1012 let mut inside = Vec::new();
1013 assigned(body, &mut inside);
1014 expr_mentions(bound, name)
1015 || inside.iter().any(|assigned| assigned == name)
1016 || statements_read(body, name)
1017 }
1018 Statement::While { condition, body } => {
1019 let mut inside = Vec::new();
1020 assigned(body, &mut inside);
1021 expr_mentions(condition, name)
1022 || inside.iter().any(|assigned| assigned == name)
1023 || statements_read(body, name)
1024 }
1025 Statement::Return(value) => expr_mentions(value, name),
1026 Statement::Seq(inner) => statements_read(inner, name),
1027 Statement::Break | Statement::Comment(_) => false,
1028 })
1029}
1030
1031fn binding_pattern(names: &[String], lowering: &Lowering, tail: &[Statement]) -> String {
1036 let parts: Vec<String> = names
1037 .iter()
1038 .map(|name| {
1039 if statements_read(tail, name) || lowering.outer_reads.iter().any(|read| read == name) {
1040 name.clone()
1041 } else {
1042 format!("_{name}")
1043 }
1044 })
1045 .collect();
1046 format!("{{{}}}", parts.join(", "))
1047}
1048
1049fn push_outer_reads(lowering: &mut Lowering, names: &[String]) -> usize {
1052 let mut pushed = 0;
1053 for name in names {
1054 if !lowering.outer_reads.iter().any(|read| read == name) {
1055 lowering.outer_reads.push(name.clone());
1056 pushed += 1;
1057 }
1058 }
1059 pushed
1060}
1061
1062fn pop_outer_reads(lowering: &mut Lowering, pushed: usize) {
1063 for _ in 0..pushed {
1064 lowering.outer_reads.pop();
1065 }
1066}
1067
1068fn acc_tuple(names: &[String]) -> String {
1070 let mut out = String::from("{");
1071 out.push_str(
1072 &names
1073 .iter()
1074 .map(String::as_str)
1075 .collect::<Vec<&str>>()
1076 .join(", "),
1077 );
1078 out.push('}');
1079 out
1080}
1081
1082fn bind_name(bound: &mut Vec<String>, name: &Ident) {
1083 let text = name.as_str();
1084 if text != "_" && !bound.iter().any(|seen| seen == text) {
1085 bound.push(text.to_string());
1086 }
1087}
1088
1089#[allow(clippy::too_many_lines)]
1100fn render_statements(
1101 w: &mut Writer,
1102 statements: &[Statement],
1103 bound: &mut Vec<String>,
1104 lowering: &mut Lowering,
1105) {
1106 for (index, statement) in statements.iter().enumerate() {
1107 let tail = &statements[index + 1..];
1108 match statement {
1109 Statement::Seq(inner) => render_statements(w, inner, bound, lowering),
1112 Statement::Comment(doc) => doc_comment(w, doc),
1113 Statement::Bind { name, value } => {
1114 w.line(&format!("{} = {}", name.as_str(), expr_text(value)));
1115 bind_name(bound, name);
1116 }
1117 Statement::BindTuple { names, value } => {
1118 let pattern: Vec<&str> = names.iter().map(Ident::as_str).collect();
1119 w.line(&format!(
1120 "{{{}}} = {}",
1121 pattern.join(", "),
1122 expr_text(value)
1123 ));
1124 for name in names {
1125 bind_name(bound, name);
1126 }
1127 }
1128 Statement::Update { base, path, value } => {
1129 render_update(w, base, path, value);
1130 bind_name(bound, base);
1131 }
1132 Statement::If {
1133 condition,
1134 then,
1135 otherwise,
1136 } => render_if(w, condition, then, otherwise, bound, lowering, tail),
1137 Statement::For {
1138 counter,
1139 bound: limit,
1140 body,
1141 } => render_for(w, counter, limit, body, bound, lowering, tail),
1142 Statement::While { condition, body } => {
1143 render_while(w, condition, body, bound, lowering, tail);
1144 }
1145 Statement::Break => {
1146 let acc = lowering
1147 .loop_accs
1148 .last()
1149 .map_or_else(|| String::from("{}"), |names| acc_tuple(names));
1150 w.line(&format!("throw({{:zd_break, {acc}}})"));
1151 }
1152 Statement::Return(value) => {
1153 w.line(&format!("throw({{:zd_return, {}}})", expr_text(value)));
1154 }
1155 }
1156 }
1157}
1158
1159fn render_update(w: &mut Writer, base: &Ident, path: &[Access], value: &Expr) {
1161 if let [Access::Key(field)] = path {
1162 w.line(&format!(
1163 "{base} = %{{{base} | {field}: {value}}}",
1164 base = base.as_str(),
1165 field = field.as_str(),
1166 value = expr_text(value)
1167 ));
1168 return;
1169 }
1170 let steps: Vec<String> = path
1171 .iter()
1172 .map(|step| match step {
1173 Access::Key(field) => format!("Access.key!(:{})", field.as_str()),
1177 Access::At(index) => format!("Access.at({})", expr_text(index)),
1178 })
1179 .collect();
1180 w.line(&format!(
1181 "{base} = put_in({base}, [{path}], {value})",
1182 base = base.as_str(),
1183 path = steps.join(", "),
1184 value = expr_text(value)
1185 ));
1186}
1187
1188#[allow(clippy::too_many_arguments)]
1199fn render_if(
1200 w: &mut Writer,
1201 condition: &Expr,
1202 then: &[Statement],
1203 otherwise: &[Statement],
1204 bound: &[String],
1205 lowering: &mut Lowering,
1206 tail: &[Statement],
1207) {
1208 let mut both: Vec<Statement> = then.to_vec();
1209 both.extend(otherwise.iter().cloned());
1210 let names = threaded(&both, bound);
1211 if names.is_empty() {
1212 if then.is_empty() && otherwise.is_empty() {
1213 return;
1214 }
1215 w.line(&format!("if {} do", expr_text(condition)));
1216 w.indent();
1217 if then.is_empty() {
1218 w.line("nil");
1220 } else {
1221 let mut branch_bound = bound.to_vec();
1222 render_statements(w, then, &mut branch_bound, lowering);
1223 }
1224 w.dedent();
1225 if !otherwise.is_empty() {
1226 w.line("else");
1227 w.indent();
1228 let mut branch_bound = bound.to_vec();
1229 render_statements(w, otherwise, &mut branch_bound, lowering);
1230 w.dedent();
1231 }
1232 w.line("end");
1233 return;
1234 }
1235 let acc = acc_tuple(&names);
1236 w.line(&format!("{} =", binding_pattern(&names, lowering, tail)));
1237 w.indent();
1238 w.line(&format!("if {} do", expr_text(condition)));
1239 w.indent();
1240 let pushed = push_outer_reads(lowering, &names);
1241 let mut branch_bound = bound.to_vec();
1242 render_statements(w, then, &mut branch_bound, lowering);
1243 w.line(&acc);
1244 w.dedent();
1245 w.line("else");
1246 w.indent();
1247 let mut branch_bound = bound.to_vec();
1248 render_statements(w, otherwise, &mut branch_bound, lowering);
1249 w.line(&acc);
1250 w.dedent();
1251 w.line("end");
1252 w.dedent();
1253 pop_outer_reads(lowering, pushed);
1254}
1255
1256#[allow(clippy::too_many_arguments)]
1267fn render_for(
1268 w: &mut Writer,
1269 counter: &Ident,
1270 limit: &Expr,
1271 body: &[Statement],
1272 bound: &[String],
1273 lowering: &mut Lowering,
1274 tail: &[Statement],
1275) {
1276 let names = threaded(body, bound);
1277 let breaks = breaks_at_this_level(body);
1278 let range = format!("0..({} - 1)//1", expr_text(limit));
1279 if names.is_empty() && !breaks {
1280 w.line(&format!("Enum.each({range}, fn {} ->", counter.as_str()));
1281 w.indent();
1282 if body.is_empty() {
1283 w.line("nil");
1284 } else {
1285 let mut body_bound = bound.to_vec();
1286 body_bound.push(counter.as_str().to_string());
1287 lowering.loop_accs.push(Vec::new());
1288 render_statements(w, body, &mut body_bound, lowering);
1289 lowering.loop_accs.pop();
1290 }
1291 w.dedent();
1292 w.line("end)");
1293 return;
1294 }
1295 let acc = acc_tuple(&names);
1296 if !names.is_empty() {
1297 w.line(&format!("{} =", binding_pattern(&names, lowering, tail)));
1298 w.indent();
1299 }
1300 if breaks {
1301 w.line("try do");
1302 w.indent();
1303 }
1304 w.line(&format!(
1305 "Enum.reduce({range}, {acc}, fn {}, {acc} ->",
1306 counter.as_str()
1307 ));
1308 w.indent();
1309 let pushed = push_outer_reads(lowering, &names);
1310 let mut body_bound = bound.to_vec();
1311 body_bound.push(counter.as_str().to_string());
1312 lowering.loop_accs.push(names.clone());
1313 render_statements(w, body, &mut body_bound, lowering);
1314 lowering.loop_accs.pop();
1315 pop_outer_reads(lowering, pushed);
1316 w.line(&acc);
1317 w.dedent();
1318 w.line("end)");
1319 if breaks {
1320 w.dedent();
1321 w.line("catch");
1322 w.indent();
1323 w.line(":throw, {:zd_break, zd_brk} -> zd_brk");
1324 w.dedent();
1325 w.line("end");
1326 }
1327 if !names.is_empty() {
1328 w.dedent();
1329 }
1330}
1331
1332#[allow(clippy::too_many_arguments)]
1343fn render_while(
1344 w: &mut Writer,
1345 condition: &Expr,
1346 body: &[Statement],
1347 bound: &[String],
1348 lowering: &mut Lowering,
1349 tail: &[Statement],
1350) {
1351 let names = threaded(body, bound);
1352 let breaks = breaks_at_this_level(body);
1353 let acc = acc_tuple(&names);
1354 let loop_name = format!("zd_loop{}", lowering.loops_named);
1355 lowering.loops_named += 1;
1356 w.line(&format!("{loop_name} = fn {loop_name}, {acc} ->"));
1357 w.indent();
1358 w.line(&format!("if {} do", expr_text(condition)));
1359 w.indent();
1360 let pushed = push_outer_reads(lowering, &names);
1361 let mut body_bound = bound.to_vec();
1362 lowering.loop_accs.push(names.clone());
1363 render_statements(w, body, &mut body_bound, lowering);
1364 lowering.loop_accs.pop();
1365 pop_outer_reads(lowering, pushed);
1366 w.line(&format!("{loop_name}.({loop_name}, {acc})"));
1367 w.dedent();
1368 w.line("else");
1369 w.indent();
1370 w.line(&acc);
1371 w.dedent();
1372 w.line("end");
1373 w.dedent();
1374 w.line("end");
1375 let call = format!("{loop_name}.({loop_name}, {acc})");
1376 let pattern = binding_pattern(&names, lowering, tail);
1377 if breaks {
1378 if !names.is_empty() {
1379 w.line(&format!("{pattern} ="));
1380 w.indent();
1381 }
1382 w.line("try do");
1383 w.indent();
1384 w.line(&call);
1385 w.dedent();
1386 w.line("catch");
1387 w.indent();
1388 w.line(":throw, {:zd_break, zd_brk} -> zd_brk");
1389 w.dedent();
1390 w.line("end");
1391 if !names.is_empty() {
1392 w.dedent();
1393 }
1394 } else if names.is_empty() {
1395 w.line(&call);
1396 } else {
1397 w.line(&format!("{pattern} = {call}"));
1398 }
1399}
1400
1401#[cfg(test)]
1402mod tests {
1403 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
1404 use super::*;
1405 use alloc::vec;
1406 use zerodds_lang_cst::elixir::{Basic, Field};
1407
1408 fn module_of(body: Vec<Item>) -> Module {
1409 Module {
1410 doc: Doc::none(),
1411 name: Path::single("Probe"),
1412 body,
1413 }
1414 }
1415
1416 #[test]
1417 fn a_module_is_a_do_end_block_indented_by_two_spaces() {
1418 let text = module(&module_of(vec![Item::Defstruct(vec![Field {
1419 name: Ident::new("x"),
1420 default: Expr::int(0),
1421 }])]));
1422 assert_eq!(text, "defmodule Probe do\n defstruct [x: 0]\nend\n");
1423 }
1424
1425 #[test]
1426 fn a_module_name_joins_its_segments_with_dots() {
1427 let text = module(&Module {
1428 doc: Doc::none(),
1429 name: Path::new(&["Common", "Util", "Location"]),
1430 body: Vec::new(),
1431 });
1432 assert!(
1433 text.starts_with("defmodule Common.Util.Location do"),
1434 "{text}"
1435 );
1436 }
1437
1438 #[test]
1439 fn a_moduledoc_is_a_heredoc_at_the_top_of_the_body() {
1440 let text = module(&Module {
1441 doc: Doc::lines(&["first", "second"]),
1442 name: Path::single("Probe"),
1443 body: Vec::new(),
1444 });
1445 assert_eq!(
1446 text,
1447 "defmodule Probe do\n @moduledoc \"\"\"\n first\n second\n \"\"\"\nend\n"
1448 );
1449 }
1450
1451 #[test]
1452 fn a_struct_with_no_fields_is_defstruct_with_an_empty_list() {
1453 let text = item(&Item::Defstruct(Vec::new()));
1454 assert_eq!(text, "defstruct []\n");
1455 }
1456
1457 #[test]
1458 fn a_struct_of_several_short_fields_stays_on_one_line() {
1459 let text = item(&Item::Defstruct(vec![
1460 Field {
1461 name: Ident::new("x"),
1462 default: Expr::int(0),
1463 },
1464 Field {
1465 name: Ident::new("pose"),
1466 default: Expr::nil(),
1467 },
1468 ]));
1469 assert_eq!(text, "defstruct [x: 0, pose: nil]\n");
1470 }
1471
1472 #[test]
1473 fn a_struct_too_wide_for_a_line_takes_one_field_each() {
1474 let text = item(&Item::Defstruct(vec![
1475 Field {
1476 name: Ident::new("a_rather_long_member_name"),
1477 default: Expr::string("a default long enough to push this past the column"),
1478 },
1479 Field {
1480 name: Ident::new("another_long_member_name"),
1481 default: Expr::nil(),
1482 },
1483 ]));
1484 assert_eq!(
1485 text,
1486 "defstruct [\n a_rather_long_member_name: \"a default long enough to push \
1487 this past the column\",\n another_long_member_name: nil\n]\n"
1488 );
1489 }
1490
1491 #[test]
1492 fn a_struct_type_too_wide_for_a_line_takes_one_field_each() {
1493 let text = item(&Item::TypeAlias(TypeAlias {
1494 doc: Doc::none(),
1495 name: Ident::new("t"),
1496 type_: Type::Struct(vec![
1497 (
1498 Ident::new("a_rather_long_member_name"),
1499 Type::Basic(Basic::Integer),
1500 ),
1501 (
1502 Ident::new("another_long_member_name"),
1503 Type::Basic(Basic::Float),
1504 ),
1505 (
1506 Ident::new("a_third_long_member_name"),
1507 Type::Basic(Basic::Binary),
1508 ),
1509 ]),
1510 }));
1511 assert_eq!(
1512 text,
1513 "@type t :: %__MODULE__{\n a_rather_long_member_name: integer(),\n \
1514 another_long_member_name: float(),\n a_third_long_member_name: binary()\n}\n"
1515 );
1516 }
1517
1518 #[test]
1519 fn a_struct_type_that_fits_stays_on_its_line() {
1520 let text = item(&Item::TypeAlias(TypeAlias {
1521 doc: Doc::none(),
1522 name: Ident::new("t"),
1523 type_: Type::Struct(vec![
1524 (Ident::new("x"), Type::Basic(Basic::Integer)),
1525 (Ident::new("y"), Type::Basic(Basic::Float)),
1526 ]),
1527 }));
1528 assert_eq!(text, "@type t :: %__MODULE__{x: integer(), y: float()}\n");
1529 }
1530
1531 #[test]
1532 fn every_type_form_writes_its_own_spelling() {
1533 assert_eq!(type_(&Type::Basic(Basic::Integer)), "integer()");
1534 assert_eq!(type_(&Type::Basic(Basic::Nil)), "nil");
1535 assert_eq!(type_(&Type::Atom("idle".to_string())), ":idle");
1536 assert_eq!(type_(&Type::Local(Ident::new("t"))), "t()");
1537 assert_eq!(
1538 type_(&Type::Remote {
1539 module: Path::new(&["Common", "Pose"]),
1540 name: Ident::new("t"),
1541 }),
1542 "Common.Pose.t()"
1543 );
1544 assert_eq!(
1545 type_(&Type::list(Type::Basic(Basic::Integer))),
1546 "list(integer())"
1547 );
1548 assert_eq!(
1549 type_(&Type::Tuple(vec![
1550 Type::Basic(Basic::Integer),
1551 Type::Basic(Basic::Binary)
1552 ])),
1553 "{integer(), binary()}"
1554 );
1555 assert_eq!(
1556 type_(&Type::Basic(Basic::Integer).or_nil()),
1557 "integer() | nil"
1558 );
1559 }
1560
1561 #[test]
1562 fn an_attribute_of_a_long_list_takes_one_line_per_entry() {
1563 let text = item(&Item::Attribute(Attribute {
1564 doc: Doc::none(),
1565 name: Ident::new("zd_members"),
1566 value: Expr::List(vec![
1567 Expr::Tuple(vec![
1568 Expr::Atom("x".to_string()),
1569 Expr::Keyword(vec![
1570 (Ident::new("idl"), Expr::string("x")),
1571 (Ident::new("kind"), Expr::Atom("int32".to_string())),
1572 (Ident::new("bits"), Expr::int(32)),
1573 ]),
1574 ]),
1575 Expr::Tuple(vec![
1576 Expr::Atom("y".to_string()),
1577 Expr::Keyword(vec![
1578 (Ident::new("idl"), Expr::string("y")),
1579 (Ident::new("kind"), Expr::Atom("float64".to_string())),
1580 (Ident::new("bits"), Expr::int(64)),
1581 (Ident::new("signed"), Expr::Literal(Literal::Bool(true))),
1582 ]),
1583 ]),
1584 ]),
1585 }));
1586 assert_eq!(
1587 text,
1588 "@zd_members [\n {:x, [idl: \"x\", kind: :int32, bits: 32]},\n \
1589 {:y, [idl: \"y\", kind: :float64, bits: 64, signed: true]}\n]\n"
1590 );
1591 }
1592
1593 #[test]
1594 fn a_function_writes_its_body_on_the_same_line() {
1595 let text = item(&Item::Function(Function {
1596 doc: Doc::none(),
1597 name: Ident::new("limit"),
1598 params: Vec::new(),
1599 body: Expr::Attr(Ident::new("limit")),
1600 }));
1601 assert_eq!(text, "def limit, do: @limit\n");
1602 }
1603
1604 #[test]
1605 fn a_function_with_patterns_writes_them_in_parentheses() {
1606 let text = item(&Item::Function(Function {
1607 doc: Doc::none(),
1608 name: Ident::new("from_value"),
1609 params: vec![Expr::int(0)],
1610 body: Expr::Tuple(vec![
1611 Expr::Atom("ok".to_string()),
1612 Expr::Atom("idle".to_string()),
1613 ]),
1614 }));
1615 assert_eq!(text, "def from_value(0), do: {:ok, :idle}\n");
1616 }
1617
1618 #[test]
1619 fn a_spec_and_a_callback_differ_only_in_their_marker() {
1620 let signature = Signature {
1621 doc: Doc::none(),
1622 name: Ident::new("status"),
1623 params: vec![Param {
1624 name: Some(Ident::new("target")),
1625 type_: Type::Basic(Basic::Binary),
1626 }],
1627 result: Type::Basic(Basic::Integer),
1628 };
1629 assert_eq!(
1630 item(&Item::Spec(signature.clone())),
1631 "@spec status(target :: binary()) :: integer()\n"
1632 );
1633 assert_eq!(
1634 item(&Item::Callback(signature)),
1635 "@callback status(target :: binary()) :: integer()\n"
1636 );
1637 }
1638
1639 #[test]
1640 fn a_float_literal_gets_the_point_elixir_requires() {
1641 assert_eq!(float_text("1e10"), "1.0e10");
1645 assert_eq!(float_text("1"), "1.0");
1646 assert_eq!(float_text("1.5"), "1.5");
1647 assert_eq!(float_text("1.5e-3"), "1.5e-3");
1648 assert_eq!(float_text(".5"), "0.5");
1649 assert_eq!(float_text("-.5"), "-0.5");
1650 assert_eq!(float_text("2."), "2.0");
1651 }
1652
1653 #[test]
1654 fn a_string_literal_escapes_the_interpolation_marker() {
1655 assert_eq!(expr(&Expr::string("a#{b}c")), "\"a\\#{b}c\"".to_string());
1658 assert_eq!(expr(&Expr::string("plain")), "\"plain\"");
1659 assert_eq!(expr(&Expr::string("a\"b")), "\"a\\\"b\"");
1660 }
1661
1662 #[test]
1663 fn an_atom_that_is_not_an_identifier_is_quoted() {
1664 assert_eq!(expr(&Expr::Atom("idle".to_string())), ":idle");
1665 assert_eq!(expr(&Expr::Atom("has space".to_string())), ":\"has space\"");
1666 assert_eq!(expr(&Expr::Atom("Upper".to_string())), ":\"Upper\"");
1667 }
1668
1669 #[test]
1670 fn a_module_name_in_a_value_is_written_as_the_alias() {
1671 assert_eq!(
1674 expr(&Expr::Module(Path::new(&["Common", "Pose"]))),
1675 "Common.Pose"
1676 );
1677 }
1678
1679 #[test]
1680 fn a_byte_string_is_a_binary_and_not_a_charlist() {
1681 assert_eq!(
1682 expr(&Expr::Literal(Literal::Bytes(vec![1, 255]))),
1683 "<<1, 255>>"
1684 );
1685 }
1686
1687 #[test]
1688 fn a_char_literal_is_written_as_its_code_unit() {
1689 assert_eq!(expr(&Expr::Literal(Literal::Char('A'))), "65");
1690 }
1691
1692 #[test]
1693 fn a_file_writes_its_comment_then_its_modules() {
1694 let text = file(&File {
1695 doc: Doc::line("Generated."),
1696 modules: vec![TopLevel::Module(module_of(Vec::new()))],
1697 });
1698 assert_eq!(text, "# Generated.\n\ndefmodule Probe do\nend\n");
1699 }
1700}