Skip to main content

meow_meow_script/
unparser.rs

1// MMS AST → source text.
2//
3// Pure function over the AST defined in `ast.rs`. No I/O, no eval.
4// Used by the component serialization pipeline (subtree → CE AST → text)
5// and by the REPL `dump` command. Round-trip property:
6//
7//     parse(src) == parse(unparse(parse(src)))
8//
9// Exact textual round-trip is NOT a goal — comments and whitespace are not
10// preserved by the parser, so they can't be reproduced. Structural equality
11// of the AST after a re-parse is the invariant.
12
13use crate::ast::{
14    AssignmentStatement, BinOpKind, BlockStatement, CallExpression, ComponentExpression,
15    ConstructorCall, ElseBranch, Expression, Ident, IfStatement, ImportItem, ReturnStatement,
16    Statement, TableFieldValue, UnaryOpKind,
17};
18use crate::token::Unit;
19
20const INDENT_STEP: usize = 4;
21
22pub fn unparse_program(stmts: &[Statement]) -> String {
23    let mut out = String::new();
24    let mut p = Printer {
25        out: &mut out,
26        indent: 0,
27    };
28    for (i, s) in stmts.iter().enumerate() {
29        if i > 0 {
30            p.out.push('\n');
31        }
32        p.write_indent();
33        p.statement(s);
34        p.out.push('\n');
35    }
36    out
37}
38
39pub fn unparse_expression(e: &Expression) -> String {
40    let mut out = String::new();
41    let mut p = Printer {
42        out: &mut out,
43        indent: 0,
44    };
45    p.expression(e);
46    out
47}
48
49pub fn unparse_component(ce: &ComponentExpression) -> String {
50    let mut out = String::new();
51    let mut p = Printer {
52        out: &mut out,
53        indent: 0,
54    };
55    p.component(ce);
56    out
57}
58
59struct Printer<'a> {
60    out: &'a mut String,
61    indent: usize,
62}
63
64impl<'a> Printer<'a> {
65    fn write_indent(&mut self) {
66        for _ in 0..self.indent {
67            self.out.push(' ');
68        }
69    }
70
71    // ---- statements -----------------------------------------------------
72
73    fn statement(&mut self, s: &Statement) {
74        match s {
75            Statement::Assignment(a) => self.assignment(a),
76            Statement::Reassign { target, value } => {
77                self.expression(target);
78                self.out.push_str(" = ");
79                self.expression(value);
80            }
81            Statement::Return(ReturnStatement { value }) => {
82                self.out.push_str("return");
83                if let Some(v) = value {
84                    self.out.push(' ');
85                    self.expression(v);
86                }
87            }
88            Statement::If(i) => self.if_stmt(i),
89            Statement::Block(b) => self.block(b),
90            Statement::Expression(e) => self.expression(e),
91            Statement::ForIn {
92                binding,
93                iterable,
94                body,
95            } => {
96                self.out.push_str("for ");
97                self.out.push_str(&binding.0);
98                self.out.push_str(" in ");
99                self.expression(iterable);
100                self.out.push(' ');
101                self.block(body);
102            }
103            Statement::While { condition, body } => {
104                self.out.push_str("while ");
105                self.expression(condition);
106                self.out.push(' ');
107                self.block(body);
108            }
109            Statement::Break => self.out.push_str("break"),
110            Statement::Continue => self.out.push_str("continue"),
111            Statement::Import { items, path } => {
112                self.out.push_str("import { ");
113                for (i, item) in items.iter().enumerate() {
114                    if i > 0 {
115                        self.out.push_str(", ");
116                    }
117                    self.import_item(item);
118                }
119                self.out.push_str(" } from ");
120                self.string_literal(path);
121            }
122        }
123    }
124
125    fn assignment(&mut self, a: &AssignmentStatement) {
126        if a.exported {
127            self.out.push_str("export ");
128        }
129        // `fn name(...) { ... }` is the natural form when assigning a Function.
130        if let Expression::Function { params, body } = &a.value {
131            self.out.push_str("fn ");
132            self.out.push_str(&a.name.0);
133            self.params_and_body(params, body);
134            return;
135        }
136        self.out.push_str("let ");
137        self.out.push_str(&a.name.0);
138        self.out.push_str(" = ");
139        self.expression(&a.value);
140    }
141
142    fn if_stmt(&mut self, i: &IfStatement) {
143        self.out.push_str("if ");
144        self.expression(&i.condition);
145        self.out.push(' ');
146        self.block(&i.then_branch);
147        if let Some(e) = &i.else_branch {
148            self.out.push_str(" else ");
149            self.else_branch(e);
150        }
151    }
152
153    fn else_branch(&mut self, e: &ElseBranch) {
154        match e {
155            ElseBranch::Block(block) => self.block(block),
156            ElseBranch::If(next_if) => self.if_stmt(next_if),
157        }
158    }
159
160    fn import_item(&mut self, item: &ImportItem) {
161        match item {
162            ImportItem::Named(n) => self.out.push_str(&n.0),
163            ImportItem::NamedAlias { name, alias } => {
164                self.out.push_str(&name.0);
165                self.out.push_str(" as ");
166                self.out.push_str(&alias.0);
167            }
168            ImportItem::PositionalAlias { index, alias } => {
169                self.out.push_str(&index.to_string());
170                self.out.push_str(" as ");
171                self.out.push_str(&alias.0);
172            }
173        }
174    }
175
176    fn block(&mut self, b: &BlockStatement) {
177        if b.statements.is_empty() {
178            self.out.push_str("{}");
179            return;
180        }
181        self.out.push('{');
182        self.out.push('\n');
183        self.indent += INDENT_STEP;
184        for s in &b.statements {
185            self.write_indent();
186            self.statement(s);
187            self.out.push('\n');
188        }
189        self.indent -= INDENT_STEP;
190        self.write_indent();
191        self.out.push('}');
192    }
193
194    fn params_and_body(&mut self, params: &[Ident], body: &BlockStatement) {
195        self.out.push('(');
196        for (i, p) in params.iter().enumerate() {
197            if i > 0 {
198                self.out.push_str(", ");
199            }
200            self.out.push_str(&p.0);
201        }
202        self.out.push_str(") ");
203        self.block(body);
204    }
205
206    // ---- expressions ----------------------------------------------------
207
208    fn expression(&mut self, e: &Expression) {
209        match e {
210            Expression::String(s) => self.string_literal(s),
211            Expression::Number(n) => self.number(*n),
212            Expression::Dimension(n, unit) => {
213                self.number(*n);
214                self.out.push_str(unit_suffix(*unit));
215            }
216            Expression::Bool(b) => self.out.push_str(if *b { "true" } else { "false" }),
217            Expression::Null => self.out.push_str("null"),
218            Expression::Identifier(Ident(name)) => self.out.push_str(name),
219            Expression::Array(items) => {
220                self.out.push('[');
221                for (i, item) in items.iter().enumerate() {
222                    if i > 0 {
223                        self.out.push_str(", ");
224                    }
225                    self.expression(item);
226                }
227                self.out.push(']');
228            }
229            Expression::Table(fields) => self.table(fields),
230            Expression::Index { base, index } => {
231                self.atom(base);
232                self.out.push('[');
233                self.expression(index);
234                self.out.push(']');
235            }
236            Expression::Call(c) => self.call(c),
237            Expression::Component(c) => self.component(c),
238            Expression::BinaryOp { op, lhs, rhs } => self.binop(op, lhs, rhs),
239            Expression::UnaryOp { op, operand } => {
240                let sym = match op {
241                    UnaryOpKind::Neg => "-",
242                    UnaryOpKind::Not => "!",
243                };
244                self.out.push_str(sym);
245                self.atom(operand);
246            }
247            Expression::Function { params, body } => {
248                self.out.push_str("fn");
249                self.params_and_body(params, body);
250            }
251        }
252    }
253
254    /// Print an expression as an atom: parenthesize if it could parse as
255    /// multiple tokens at the top level (binops, unary, function literal).
256    fn atom(&mut self, e: &Expression) {
257        match e {
258            Expression::BinaryOp { .. }
259            | Expression::UnaryOp { .. }
260            | Expression::Index { .. }
261            | Expression::Function { .. } => {
262                self.out.push('(');
263                self.expression(e);
264                self.out.push(')');
265            }
266            _ => self.expression(e),
267        }
268    }
269
270    fn table(&mut self, fields: &[TableFieldValue]) {
271        if fields.is_empty() {
272            self.out.push_str("{}");
273            return;
274        }
275        self.out.push('{');
276        self.out.push('\n');
277        self.indent += INDENT_STEP;
278        for field in fields {
279            self.write_indent();
280            self.out.push_str(&field.name.0);
281            self.out.push_str(" = ");
282            self.expression(&field.value);
283            self.out.push('\n');
284        }
285        self.indent -= INDENT_STEP;
286        self.write_indent();
287        self.out.push('}');
288    }
289
290    fn call(&mut self, c: &CallExpression) {
291        // Method-call form: callee is `lhs . method_ident`.
292        if let Expression::BinaryOp {
293            op: BinOpKind::Dot,
294            lhs,
295            rhs,
296        } = &*c.callee
297        {
298            if let Expression::Identifier(Ident(method)) = &**rhs {
299                self.atom(lhs);
300                self.out.push('.');
301                self.out.push_str(method);
302                self.args(&c.args);
303                return;
304            }
305        }
306        self.atom(&c.callee);
307        self.args(&c.args);
308    }
309
310    fn args(&mut self, args: &[Expression]) {
311        self.out.push('(');
312        for (i, a) in args.iter().enumerate() {
313            if i > 0 {
314                self.out.push_str(", ");
315            }
316            self.expression(a);
317        }
318        self.out.push(')');
319    }
320
321    fn binop(&mut self, op: &BinOpKind, lhs: &Expression, rhs: &Expression) {
322        if matches!(op, BinOpKind::Dot) {
323            self.atom(lhs);
324            self.out.push('.');
325            self.atom(rhs);
326            return;
327        }
328        let sym = binop_sym(op);
329        self.atom(lhs);
330        self.out.push(' ');
331        self.out.push_str(sym);
332        self.out.push(' ');
333        self.atom(rhs);
334    }
335
336    fn component(&mut self, ce: &ComponentExpression) {
337        self.out.push_str(&ce.component_type.0);
338        for ctor in &ce.constructors {
339            self.constructor(ctor);
340        }
341        // Empty body: omit braces when there's at least one constructor
342        // (e.g. `R.cube()`). With no constructors and no body, still emit
343        // `Name {}` so the re-parse keeps it as a ComponentExpression rather
344        // than a bare Identifier.
345        if ce.body.statements.is_empty() {
346            if ce.constructors.is_empty() {
347                self.out.push_str(" {}");
348            }
349            return;
350        }
351        self.out.push(' ');
352        self.block(&ce.body);
353    }
354
355    fn constructor(&mut self, c: &ConstructorCall) {
356        self.out.push('.');
357        self.out.push_str(&c.method.0);
358        self.args(&c.args);
359    }
360
361    // ---- literals -------------------------------------------------------
362
363    fn number(&mut self, n: f64) {
364        if !n.is_finite() {
365            // NaN / Inf can't round-trip through the tokenizer. Emit a
366            // best-effort form; tests should never produce these.
367            self.out.push_str(&format!("{n}"));
368            return;
369        }
370        let s = format!("{n}");
371        // Force `.0` so the literal stays unambiguously a float and lines
372        // up with the style used in examples (`1.0`, `0.85`).
373        if !s.contains('.') && !s.contains('e') && !s.contains('E') {
374            self.out.push_str(&s);
375            self.out.push_str(".0");
376        } else {
377            self.out.push_str(&s);
378        }
379    }
380
381    fn string_literal(&mut self, s: &str) {
382        self.out.push('"');
383        for ch in s.chars() {
384            match ch {
385                '\\' => self.out.push_str("\\\\"),
386                '"' => self.out.push_str("\\\""),
387                '\n' => self.out.push_str("\\n"),
388                '\r' => self.out.push_str("\\r"),
389                '\t' => self.out.push_str("\\t"),
390                c => self.out.push(c),
391            }
392        }
393        self.out.push('"');
394    }
395}
396
397fn binop_sym(op: &BinOpKind) -> &'static str {
398    match op {
399        BinOpKind::Add => "+",
400        BinOpKind::Sub => "-",
401        BinOpKind::Mul => "*",
402        BinOpKind::Div => "/",
403        BinOpKind::Rem => "%",
404        BinOpKind::Eq => "==",
405        BinOpKind::NotEq => "!=",
406        BinOpKind::Lt => "<",
407        BinOpKind::Gt => ">",
408        BinOpKind::LtEq => "<=",
409        BinOpKind::GtEq => ">=",
410        BinOpKind::And => "&&",
411        BinOpKind::Or => "||",
412        BinOpKind::Pipe => "|>",
413        BinOpKind::Query => "->",
414        BinOpKind::Dot => ".",
415    }
416}
417
418fn unit_suffix(u: Unit) -> &'static str {
419    match u {
420        Unit::Percent => "%",
421        Unit::GlyphUnits => "gu",
422        Unit::WorldUnits => "wu",
423        Unit::Degrees => "deg",
424        Unit::Radians => "rad",
425    }
426}
427
428// =====================================================================
429// Tests
430// =====================================================================
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use crate::parser::MeowMeowParser;
436    use crate::tokenizer::MeowMeowTokenizer;
437
438    fn parse(src: &str) -> Vec<Statement> {
439        let tokens = MeowMeowTokenizer::new(src)
440            .tokenize()
441            .expect("tokenize failed");
442        MeowMeowParser::new(tokens)
443            .parse_program()
444            .expect("parse failed")
445    }
446
447    fn round_trip(src: &str) {
448        let ast1 = parse(src);
449        let text = unparse_program(&ast1);
450        let ast2 = match std::panic::catch_unwind(|| parse(&text)) {
451            Ok(a) => a,
452            Err(_) => panic!("re-parse panicked. Unparsed text was:\n{text}"),
453        };
454        assert_eq!(
455            ast1, ast2,
456            "AST changed after round trip.\n--- original ---\n{src}\n--- unparsed ---\n{text}\n"
457        );
458    }
459
460    #[test]
461    fn round_trip_cat_mms() {
462        let src = include_str!("../tests/fixtures/cat.mms");
463        round_trip(src);
464    }
465
466    #[test]
467    fn round_trip_minimal_component() {
468        round_trip("T {}");
469    }
470
471    #[test]
472    fn round_trip_builder_chain() {
473        round_trip("T.position(0.0, 1.0, -2.5).scale(0.5, 0.5, 0.5) { R.cube() }");
474    }
475
476    #[test]
477    fn round_trip_let_and_arith() {
478        round_trip("let x = 1.0 + 2.0 * 3.0\nlet y = -x");
479    }
480
481    #[test]
482    fn round_trip_function() {
483        round_trip("fn double(n) { return n * 2.0 }\nlet four = double(2.0)");
484    }
485
486    #[test]
487    fn round_trip_for_loop() {
488        round_trip("let sum = 0.0\nfor i in [1.0, 2.0, 3.0] { sum = sum + i }");
489    }
490
491    #[test]
492    fn round_trip_if_else() {
493        round_trip("if true { let a = 1.0 } else { let b = 2.0 }");
494    }
495
496    #[test]
497    fn round_trip_import() {
498        round_trip("import { foo, bar as baz, 0 as cat } from \"cat.mms\"");
499    }
500
501    #[test]
502    fn round_trip_dimensions() {
503        round_trip("let a = 50%\nlet b = 20gu\nlet c = 30deg\nlet d = 0.5rad");
504    }
505
506    #[test]
507    fn round_trip_strings_with_escapes() {
508        round_trip("let s = \"hello \\\"world\\\"\\nnext line\"");
509    }
510
511    #[test]
512    fn negative_number_idempotent() {
513        // `-0.22` may parse as UnaryOp(Neg, 0.22). Round-trip should be stable.
514        round_trip("T.position(-0.22, 1.38, 0.52) {}");
515    }
516}