1use crate::writer::{Writer, verbatim_lines};
28use alloc::string::{String, ToString};
29use alloc::vec::Vec;
30use zerodds_lang_cst::common::{Doc, Ident, Literal};
31use zerodds_lang_cst::lua::{Expr, File, Function, Key, Place, Stat, Table};
32
33#[must_use]
35pub fn file(file: &File) -> String {
36 let mut w = Writer::new(" ");
37 plain(&mut w, &file.doc);
38 if !file.doc.is_empty() {
39 w.blank_line();
40 }
41 for statement in &file.body {
42 if opens_block(statement) {
48 w.blank_line();
49 }
50 render_stat(&mut w, statement);
51 }
52 w.blank_line();
53 w.push("return ");
55 render_expr(&mut w, &file.returns);
56 w.newline();
57 for text in &file.end_text {
58 verbatim_lines(&mut w, text);
59 }
60 w.finish()
61}
62
63#[must_use]
65pub fn stat(node: &Stat) -> String {
66 let mut w = Writer::new(" ");
67 render_stat(&mut w, node);
68 w.finish()
69}
70
71#[must_use]
77pub fn expr(node: &Expr) -> String {
78 let mut w = Writer::new(" ");
79 render_expr(&mut w, node);
80 let mut out = w.finish();
81 if out.ends_with('\n') {
82 out.pop();
83 }
84 out
85}
86
87#[must_use]
89pub fn ident(name: &Ident) -> String {
90 name.as_str().to_string()
91}
92
93fn opens_block(node: &Stat) -> bool {
95 match node {
96 Stat::Local { doc, .. } | Stat::Assign { doc, .. } => !doc.is_empty(),
97 Stat::Function(function) => !function.doc.is_empty(),
98 Stat::Comment(doc) | Stat::Annotation(doc) => !doc.is_empty(),
99 Stat::Verbatim(_) => true,
100 Stat::Return(_)
101 | Stat::Perform(_)
102 | Stat::If { .. }
103 | Stat::While { .. }
104 | Stat::NumericFor { .. }
105 | Stat::Break => false,
106 }
107}
108
109fn render_stat(w: &mut Writer, node: &Stat) {
116 match node {
117 Stat::Local { doc, name, value } => {
118 annotation(w, doc);
119 w.push("local ");
120 w.push(name.as_str());
121 if let Some(value) = value {
122 w.push(" = ");
123 render_expr(w, value);
124 }
125 w.newline();
126 }
127 Stat::Assign { doc, target, value } => {
128 annotation(w, doc);
129 render_place(w, target);
130 w.push(" = ");
131 render_expr(w, value);
132 w.newline();
133 }
134 Stat::Function(function) => render_function(w, function),
135 Stat::Return(value) => {
136 w.push("return");
137 if let Some(value) = value {
138 w.push(" ");
139 render_expr(w, value);
140 }
141 w.newline();
142 }
143 Stat::Comment(doc) => plain(w, doc),
144 Stat::Annotation(doc) => annotation(w, doc),
145 Stat::Verbatim(text) => verbatim_lines(w, text),
146 Stat::Perform(value) => {
147 render_expr(w, value);
148 w.newline();
149 }
150 Stat::If {
151 condition,
152 then,
153 otherwise,
154 } => {
155 render_if(w, condition, then, otherwise.as_deref(), "if ");
156 w.line("end");
157 }
158 Stat::While { condition, body } => {
159 w.push("while ");
160 render_expr(w, condition);
161 w.push(" do");
162 w.newline();
163 w.indent();
164 for statement in body {
165 render_stat(w, statement);
166 }
167 w.dedent();
168 w.line("end");
169 }
170 Stat::NumericFor {
171 counter,
172 from,
173 to,
174 body,
175 } => {
176 w.push("for ");
177 w.push(counter.as_str());
178 w.push(" = ");
179 render_expr(w, from);
180 w.push(", ");
181 render_expr(w, to);
182 w.push(" do");
183 w.newline();
184 w.indent();
185 for statement in body {
186 render_stat(w, statement);
187 }
188 w.dedent();
189 w.line("end");
190 }
191 Stat::Break => w.line("break"),
192 }
193}
194
195fn render_if(
217 w: &mut Writer,
218 condition: &Expr,
219 then: &[Stat],
220 otherwise: Option<&[Stat]>,
221 keyword: &str,
222) {
223 let mut condition = condition;
224 let mut then = then;
225 let mut otherwise = otherwise;
226 let mut keyword = keyword;
227 loop {
228 w.push(keyword);
229 render_expr(w, condition);
230 w.push(" then");
231 w.newline();
232 w.indent();
233 for statement in then {
234 render_stat(w, statement);
235 }
236 w.dedent();
237 match otherwise {
238 None => return,
239 Some(
240 [
241 Stat::If {
242 condition: next_condition,
243 then: next_then,
244 otherwise: next_otherwise,
245 },
246 ],
247 ) => {
248 condition = next_condition;
249 then = next_then;
250 otherwise = next_otherwise.as_deref();
251 keyword = "elseif ";
252 }
253 Some(rest) => {
254 w.line("else");
255 w.indent();
256 for statement in rest {
257 render_stat(w, statement);
258 }
259 w.dedent();
260 return;
261 }
262 }
263 }
264}
265
266fn render_function(w: &mut Writer, node: &Function) {
273 annotation(w, &node.doc);
274 w.push("function ");
275 render_place(w, &node.target);
276 w.push("(");
277 let params: Vec<String> = node
278 .params
279 .iter()
280 .map(|param| param.as_str().to_string())
281 .collect();
282 w.push(¶ms.join(", "));
283 w.push(")");
284 w.newline();
285 w.indent();
286 for statement in &node.body {
287 render_stat(w, statement);
288 }
289 w.dedent();
290 w.line("end");
291}
292
293fn render_place(w: &mut Writer, node: &Place) {
300 match node {
301 Place::Name(name) => w.push(name.as_str()),
302 Place::Field { base, name } => {
303 render_place(w, base);
304 w.push(".");
305 w.push(name.as_str());
306 }
307 Place::Index { base, key } => {
308 render_place(w, base);
309 w.push("[");
310 render_expr(w, key);
311 w.push("]");
312 }
313 Place::Paren(inner) => {
314 w.push("(");
315 render_expr(w, inner);
316 w.push(")");
317 }
318 }
319}
320
321fn render_expr(w: &mut Writer, node: &Expr) {
328 match node {
329 Expr::Table(table) if !one_line(table) => render_table(w, table),
330 Expr::Table(_) | Expr::Nil | Expr::Literal(_) | Expr::Place(_) => w.push(&expr_text(node)),
331 Expr::Call { func, args } => {
332 render_place(w, func);
333 w.push("(");
334 for (index, arg) in args.iter().enumerate() {
335 if index > 0 {
336 w.push(", ");
337 }
338 render_expr(w, arg);
339 }
340 w.push(")");
341 }
342 Expr::Binary { op, lhs, rhs } => {
343 render_expr(w, lhs);
344 w.push(" ");
345 w.push(op);
346 w.push(" ");
347 render_expr(w, rhs);
348 }
349 Expr::Unary { op, value } => {
350 w.push(op);
351 if op.chars().all(|c| c.is_ascii_alphabetic()) {
352 w.push(" ");
353 }
354 render_expr(w, value);
355 }
356 Expr::Paren(inner) => {
357 w.push("(");
358 render_expr(w, inner);
359 w.push(")");
360 }
361 }
362}
363
364fn render_table(w: &mut Writer, table: &Table) {
377 w.push("{");
378 w.newline();
379 w.indent();
380 for entry in &table.entries {
381 annotation(w, &entry.doc);
382 render_key(w, &entry.key);
383 render_expr(w, &entry.value);
384 w.push(",");
385 w.newline();
386 }
387 w.dedent();
388 w.push("}");
389}
390
391fn render_key(w: &mut Writer, key: &Key) {
392 match key {
393 Key::Positional => {}
394 Key::Name(name) => {
395 w.push(name.as_str());
396 w.push(" = ");
397 }
398 Key::Expr(expr) => {
399 w.push("[");
400 render_expr(w, expr);
401 w.push("] = ");
402 }
403 }
404}
405
406fn one_line(table: &Table) -> bool {
414 table
415 .entries
416 .iter()
417 .all(|entry| entry.doc.is_empty() && !matches!(entry.value, Expr::Table(_)))
418}
419
420fn expr_text(node: &Expr) -> String {
433 match node {
434 Expr::Nil => "nil".to_string(),
435 Expr::Literal(literal) => literal_text(literal),
436 Expr::Place(place) => place_text(place),
437 Expr::Table(table) => {
438 if table.is_empty() {
439 return "{}".to_string();
440 }
441 let mut out = String::from("{ ");
442 for (index, entry) in table.entries.iter().enumerate() {
443 if index > 0 {
444 out.push_str(", ");
445 }
446 out.push_str(&key_text(&entry.key));
447 out.push_str(&expr_text(&entry.value));
448 }
449 out.push_str(" }");
450 out
451 }
452 Expr::Call { func, args } => {
453 let mut out = place_text(func);
454 out.push('(');
455 let parts: Vec<String> = args.iter().map(expr_text).collect();
456 out.push_str(&parts.join(", "));
457 out.push(')');
458 out
459 }
460 Expr::Binary { op, lhs, rhs } => {
461 let mut out = expr_text(lhs);
462 out.push(' ');
463 out.push_str(op);
464 out.push(' ');
465 out.push_str(&expr_text(rhs));
466 out
467 }
468 Expr::Unary { op, value } => {
469 let mut out = String::from(*op);
470 if op.chars().all(|c| c.is_ascii_alphabetic()) {
471 out.push(' ');
472 }
473 out.push_str(&expr_text(value));
474 out
475 }
476 Expr::Paren(inner) => {
477 let mut out = String::from("(");
478 out.push_str(&expr_text(inner));
479 out.push(')');
480 out
481 }
482 }
483}
484
485fn key_text(key: &Key) -> String {
492 match key {
493 Key::Positional => String::new(),
494 Key::Name(name) => {
495 let mut out = name.as_str().to_string();
496 out.push_str(" = ");
497 out
498 }
499 Key::Expr(expr) => {
500 let mut out = String::from("[");
501 out.push_str(&expr_text(expr));
502 out.push_str("] = ");
503 out
504 }
505 }
506}
507
508fn place_text(node: &Place) -> String {
515 match node {
516 Place::Name(name) => name.as_str().to_string(),
517 Place::Field { base, name } => {
518 let mut out = place_text(base);
519 out.push('.');
520 out.push_str(name.as_str());
521 out
522 }
523 Place::Index { base, key } => {
524 let mut out = place_text(base);
525 out.push('[');
526 out.push_str(&expr_text(key));
527 out.push(']');
528 out
529 }
530 Place::Paren(inner) => {
531 let mut out = String::from("(");
532 out.push_str(&expr_text(inner));
533 out.push(')');
534 out
535 }
536 }
537}
538
539fn literal_text(literal: &Literal) -> String {
540 match literal {
541 Literal::Int(text) | Literal::Float(text) => text.clone(),
542 Literal::Bool(value) => {
543 if *value {
544 "true".to_string()
545 } else {
546 "false".to_string()
547 }
548 }
549 Literal::Char(ch) => {
553 let mut out = String::from("\"");
554 out.push_str(&escape_string(&alloc::string::String::from(*ch)));
555 out.push('"');
556 out
557 }
558 Literal::Str(text) => {
559 let mut out = String::from("\"");
560 out.push_str(&escape_string(text));
561 out.push('"');
562 out
563 }
564 Literal::Bytes(bytes) => {
568 let mut out = String::from("\"");
569 for byte in bytes {
570 out.push_str(&alloc::format!("\\{byte:03}"));
571 }
572 out.push('"');
573 out
574 }
575 Literal::Null => "nil".to_string(),
576 }
577}
578
579#[must_use]
593pub fn escape_string(text: &str) -> String {
594 let mut out = String::new();
595 for ch in text.chars() {
596 match ch {
597 '"' => out.push_str("\\\""),
598 '\\' => out.push_str("\\\\"),
599 '\n' => out.push_str("\\n"),
600 '\r' => out.push_str("\\r"),
601 '\t' => out.push_str("\\t"),
602 other if (other as u32) < 0x20 || other as u32 == 0x7f => {
603 out.push_str(&alloc::format!("\\{:03}", other as u32));
604 }
605 other => out.push(other),
606 }
607 }
608 out
609}
610
611fn plain(w: &mut Writer, doc: &Doc) {
613 marked(w, "--", doc);
614}
615
616fn annotation(w: &mut Writer, doc: &Doc) {
619 marked(w, "---", doc);
620}
621
622fn marked(w: &mut Writer, marker: &str, doc: &Doc) {
629 for line in doc.as_lines() {
630 if line.is_empty() {
631 w.line(marker);
632 continue;
633 }
634 let mut text = String::from(marker);
635 if !line.starts_with('@') {
636 text.push(' ');
637 }
638 text.push_str(line);
639 w.line(&text);
640 }
641}
642
643#[cfg(test)]
644mod tests {
645 #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
646 use super::*;
647 use alloc::vec;
648 use zerodds_lang_cst::lua::{self, Entry, table_key};
649
650 fn name(text: &str) -> Place {
651 Place::name(text)
652 }
653
654 #[test]
655 fn a_file_ends_with_the_return_its_module_needs() {
656 let rendered = file(&File {
657 end_text: Vec::new(),
658 doc: Doc::line("A header."),
659 body: vec![Stat::Local {
660 doc: Doc::none(),
661 name: Ident::new("M"),
662 value: Some(Expr::Table(Table::default())),
663 }],
664 returns: Expr::place(name("M")),
665 });
666 assert_eq!(rendered, "-- A header.\n\nlocal M = {}\n\nreturn M\n");
667 }
668
669 #[test]
670 fn a_note_takes_two_dashes_and_an_annotation_three() {
671 assert_eq!(stat(&Stat::Comment(Doc::line("a note"))), "-- a note\n");
675 assert_eq!(
676 stat(&Stat::Annotation(Doc::line("@class Pose"))),
677 "---@class Pose\n"
678 );
679 }
680
681 #[test]
682 fn a_luacats_tag_is_written_against_the_dashes_and_prose_is_not() {
683 assert_eq!(
687 stat(&Stat::Annotation(Doc::lines(&[
688 "@field x integer",
689 "a note"
690 ]))),
691 "---@field x integer\n--- a note\n"
692 );
693 }
694
695 #[test]
696 fn a_commented_statement_starts_a_block_and_an_uncommented_one_does_not() {
697 let rendered = file(&File {
699 end_text: Vec::new(),
700 doc: Doc::none(),
701 body: vec![
702 Stat::Assign {
703 doc: Doc::line("the descriptor"),
704 target: name("M").field("Pose"),
705 value: Expr::int(1),
706 },
707 Stat::Assign {
708 doc: Doc::none(),
709 target: name("M").field("registry"),
710 value: Expr::int(2),
711 },
712 Stat::Assign {
713 doc: Doc::line("the next block"),
714 target: name("M").field("Other"),
715 value: Expr::int(3),
716 },
717 ],
718 returns: Expr::place(name("M")),
719 });
720 assert_eq!(
721 rendered,
722 "--- the descriptor\nM.Pose = 1\nM.registry = 2\n\n\
723 --- the next block\nM.Other = 3\n\nreturn M\n"
724 );
725 }
726
727 #[test]
728 fn an_empty_comment_line_carries_no_trailing_space() {
729 let rendered = stat(&Stat::Comment(Doc::lines(&["first", "", "third"])));
730 assert_eq!(rendered, "-- first\n--\n-- third\n");
731 }
732
733 #[test]
734 fn a_table_of_scalars_is_written_on_one_line() {
735 let table = Table::of(vec![
736 Entry::named("name", Expr::string("x")),
737 Entry::named("bits", Expr::int(32)),
738 ]);
739 assert_eq!(expr(&Expr::Table(table)), "{ name = \"x\", bits = 32 }");
740 }
741
742 #[test]
743 fn a_table_holding_a_table_is_written_one_entry_per_line() {
744 let table = Table::of(vec![Entry::named(
745 "members",
746 Expr::Table(Table::of(vec![Entry::positional(Expr::int(1))])),
747 )]);
748 assert_eq!(expr(&Expr::Table(table)), "{\n members = { 1 },\n}");
751 }
752
753 #[test]
754 fn a_nested_table_indents_against_the_statement_that_holds_it() {
755 let rendered = stat(&Stat::Assign {
758 doc: Doc::none(),
759 target: name("M").field("Pose"),
760 value: Expr::Table(Table::of(vec![Entry::named(
761 "members",
762 Expr::Table(Table::of(vec![Entry::positional(Expr::Table(Table::of(
763 vec![Entry::named("name", Expr::string("x"))],
764 )))])),
765 )])),
766 });
767 assert_eq!(
768 rendered,
769 "M.Pose = {\n members = {\n { name = \"x\" },\n },\n}\n"
770 );
771 }
772
773 #[test]
774 fn an_entry_with_a_comment_forces_the_nested_form() {
775 let table = Table::of(vec![Entry {
779 doc: Doc::line("why"),
780 key: table_key("a"),
781 value: Expr::int(1),
782 }]);
783 assert_eq!(expr(&Expr::Table(table)), "{\n --- why\n a = 1,\n}");
784 }
785
786 #[test]
787 fn a_keyword_key_is_written_in_the_bracket_form() {
788 let table = Table::of(vec![Entry::named("end", Expr::int(1))]);
789 assert_eq!(expr(&Expr::Table(table)), "{ [\"end\"] = 1 }");
790 }
791
792 #[test]
793 fn a_keyword_field_of_a_place_is_written_in_the_bracket_form() {
794 let place = name("M").field("common").field("end");
795 assert_eq!(expr(&Expr::place(place)), "M.common[\"end\"]");
796 }
797
798 #[test]
799 fn a_function_renders_with_its_body_indented() {
800 let rendered = stat(&Stat::Function(Function {
801 doc: Doc::line("@return Pose"),
802 target: name("M").field("Pose").field("new"),
803 params: Vec::new(),
804 body: vec![Stat::Return(Some(Expr::Table(Table::of(vec![
805 Entry::named("x", Expr::int(0)),
806 ]))))],
807 }));
808 assert_eq!(
809 rendered,
810 "---@return Pose\nfunction M.Pose.new()\n return { x = 0 }\nend\n"
811 );
812 }
813
814 #[test]
815 fn a_call_renders_its_arguments_in_order() {
816 let call = Expr::Call {
817 func: alloc::boxed::Box::new(name("assert")),
818 args: vec![
819 Expr::place(name("M").field("Point")),
820 Expr::string("not declared"),
821 ],
822 };
823 assert_eq!(expr(&call), "assert(M.Point, \"not declared\")");
824 }
825
826 #[test]
827 fn a_binary_expression_writes_the_operator_it_was_given() {
828 let shift = Expr::Binary {
829 op: "<<",
830 lhs: alloc::boxed::Box::new(Expr::int(1)),
831 rhs: alloc::boxed::Box::new(Expr::int(4)),
832 };
833 assert_eq!(expr(&shift), "1 << 4");
834 }
835
836 #[test]
837 fn nil_and_the_booleans_are_written_as_lua_spells_them() {
838 assert_eq!(expr(&Expr::Nil), "nil");
839 assert_eq!(expr(&Expr::Literal(Literal::Null)), "nil");
840 assert_eq!(expr(&Expr::bool(true)), "true");
841 assert_eq!(expr(&Expr::bool(false)), "false");
842 }
843
844 #[test]
845 fn a_float_keeps_the_form_that_makes_math_type_answer_float() {
846 assert_eq!(
847 expr(&Expr::Literal(Literal::Float(lua::float_text("0")))),
848 "0.0"
849 );
850 }
851
852 #[test]
853 fn a_control_character_escape_is_three_digits_wide() {
854 assert_eq!(escape_string("\u{0}1"), "\\0001");
857 assert_eq!(escape_string("a\"b"), "a\\\"b");
858 assert_eq!(escape_string("a\\b"), "a\\\\b");
859 assert_eq!(escape_string("a\nb"), "a\\nb");
860 assert_eq!(escape_string("plain"), "plain");
861 }
862
863 #[test]
864 fn a_non_ascii_character_goes_through_as_its_own_octets() {
865 assert_eq!(escape_string("Größe"), "Größe");
868 }
869
870 #[test]
871 fn a_char_literal_becomes_a_one_character_string() {
872 assert_eq!(expr(&Expr::Literal(Literal::Char('a'))), "\"a\"");
873 }
874
875 #[test]
876 fn a_byte_string_is_written_as_decimal_escapes() {
877 assert_eq!(
878 expr(&Expr::Literal(Literal::Bytes(vec![0, 1, 255]))),
879 "\"\\000\\001\\255\""
880 );
881 }
882
883 #[test]
884 fn an_index_place_renders_its_key_expression() {
885 let place = name("M").field("_descriptors");
886 let rendered = expr(&Expr::place(Place::Index {
887 base: alloc::boxed::Box::new(place),
888 key: alloc::boxed::Box::new(Expr::string("::Point")),
889 }));
890 assert_eq!(rendered, "M._descriptors[\"::Point\"]");
891 }
892
893 #[test]
894 fn a_bare_return_renders_without_a_value() {
895 assert_eq!(stat(&Stat::Return(None)), "return\n");
896 }
897
898 #[test]
899 fn a_local_without_a_value_renders_as_a_bare_declaration() {
900 assert_eq!(
901 stat(&Stat::Local {
902 doc: Doc::none(),
903 name: Ident::new("x"),
904 value: None,
905 }),
906 "local x\n"
907 );
908 }
909
910 #[test]
911 fn an_identifier_renders_exactly_as_it_was_given() {
912 assert_eq!(ident(&Ident::new("Pose")), "Pose");
913 }
914
915 #[test]
916 fn a_call_statement_renders_as_the_call() {
917 let rendered = stat(&Stat::Perform(Expr::Call {
920 func: alloc::boxed::Box::new(name("zdw").field("write_u32")),
921 args: vec![Expr::place(name("w")), Expr::int(7)],
922 }));
923 assert_eq!(rendered, "zdw.write_u32(w, 7)\n");
924 }
925
926 #[test]
927 fn an_if_renders_its_two_arms_and_ends_once() {
928 let rendered = stat(&Stat::If {
929 condition: Expr::Binary {
930 op: "==",
931 lhs: alloc::boxed::Box::new(Expr::place(name("x"))),
932 rhs: alloc::boxed::Box::new(Expr::int(1)),
933 },
934 then: vec![Stat::Return(Some(Expr::int(1)))],
935 otherwise: Some(vec![Stat::Return(Some(Expr::int(2)))]),
936 });
937 assert_eq!(
938 rendered,
939 "if x == 1 then\n return 1\nelse\n return 2\nend\n"
940 );
941 }
942
943 #[test]
944 fn an_else_holding_one_if_flattens_to_elseif() {
945 let inner = Stat::If {
948 condition: Expr::Binary {
949 op: "==",
950 lhs: alloc::boxed::Box::new(Expr::place(name("x"))),
951 rhs: alloc::boxed::Box::new(Expr::int(2)),
952 },
953 then: vec![Stat::Break],
954 otherwise: Some(vec![Stat::Return(None)]),
955 };
956 let rendered = stat(&Stat::If {
957 condition: Expr::Binary {
958 op: "==",
959 lhs: alloc::boxed::Box::new(Expr::place(name("x"))),
960 rhs: alloc::boxed::Box::new(Expr::int(1)),
961 },
962 then: vec![Stat::Break],
963 otherwise: Some(vec![inner]),
964 });
965 assert_eq!(
966 rendered,
967 "if x == 1 then\n break\nelseif x == 2 then\n break\nelse\n return\nend\n"
968 );
969 }
970
971 #[test]
972 fn a_while_and_a_numeric_for_render_their_bodies_indented() {
973 let rendered = stat(&Stat::While {
974 condition: Expr::bool(true),
975 body: vec![Stat::Break],
976 });
977 assert_eq!(rendered, "while true do\n break\nend\n");
978 let rendered = stat(&Stat::NumericFor {
979 counter: Ident::new("zd_i"),
980 from: Expr::int(0),
981 to: Expr::Binary {
982 op: "-",
983 lhs: alloc::boxed::Box::new(Expr::place(name("zd_n"))),
984 rhs: alloc::boxed::Box::new(Expr::int(1)),
985 },
986 body: vec![Stat::Break],
987 });
988 assert_eq!(rendered, "for zd_i = 0, zd_n - 1 do\n break\nend\n");
989 }
990
991 #[test]
992 fn the_unary_operators_space_the_word_and_not_the_symbols() {
993 let value = || alloc::boxed::Box::new(Expr::place(name("v")));
995 assert_eq!(
996 expr(&Expr::Unary {
997 op: "not",
998 value: value()
999 }),
1000 "not v"
1001 );
1002 assert_eq!(
1003 expr(&Expr::Unary {
1004 op: "#",
1005 value: value()
1006 }),
1007 "#v"
1008 );
1009 assert_eq!(
1010 expr(&Expr::Unary {
1011 op: "~",
1012 value: value()
1013 }),
1014 "~v"
1015 );
1016 }
1017
1018 #[test]
1019 fn a_deliberate_parenthesis_survives_as_expression_and_as_place_base() {
1020 assert_eq!(
1021 expr(&Expr::Paren(alloc::boxed::Box::new(Expr::int(1)))),
1022 "(1)"
1023 );
1024 let place = Place::Index {
1025 base: alloc::boxed::Box::new(Place::Paren(alloc::boxed::Box::new(Expr::place(name(
1026 "f",
1027 ))))),
1028 key: alloc::boxed::Box::new(Expr::int(1)),
1029 };
1030 assert_eq!(expr(&Expr::place(place)), "(f)[1]");
1031 }
1032}