Skip to main content

panproto_expr_parser/
pretty.rs

1//! Pretty printer for panproto expressions.
2//!
3//! Converts `panproto_expr::Expr` back into Haskell-style surface syntax.
4//! The output is designed to round-trip through the parser:
5//! `parse(tokenize(pretty_print(&e))) == e` for well-formed expressions.
6//!
7//! Parentheses are minimized using precedence awareness, and operators
8//! are printed in infix notation where the parser supports it.
9
10use std::fmt::Write;
11use std::sync::Arc;
12
13use panproto_expr::{BuiltinOp, Expr, Literal, Pattern};
14
15/// Precedence levels (higher binds tighter).
16///
17/// These mirror the Pratt parser precedences in `parser.rs`.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
19enum Prec {
20    /// Top level: no parens needed.
21    Top = 0,
22    /// Pipe operator (`&`).
23    Pipe = 1,
24    /// Logical or (`||`).
25    Or = 3,
26    /// Logical and (`&&`).
27    And = 4,
28    /// Comparison (`==`, `/=`, `<`, `<=`, `>`, `>=`).
29    Cmp = 5,
30    /// Concatenation (`++`).
31    Concat = 6,
32    /// Addition and subtraction (`+`, `-`).
33    AddSub = 7,
34    /// Multiplication, division, modulo (`*`, `/`, `%`, `mod`, `div`).
35    MulDiv = 8,
36    /// Unary prefix (`-`, `not`).
37    Unary = 9,
38    /// Function application.
39    App = 10,
40    /// Postfix (`.field`, `->edge`), atoms.
41    Atom = 11,
42}
43
44/// Associativity of a binary operator.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46enum Assoc {
47    Left,
48    Right,
49}
50
51/// Pretty print an expression to a string.
52///
53/// The output uses Haskell-style surface syntax with minimal parentheses.
54///
55/// # Examples
56///
57/// ```
58/// use std::sync::Arc;
59/// use panproto_expr::{Expr, Literal, BuiltinOp};
60/// use panproto_expr_parser::pretty_print;
61///
62/// let e = Expr::Builtin(BuiltinOp::Add, vec![
63///     Expr::Var(Arc::from("x")),
64///     Expr::Lit(Literal::Int(1)),
65/// ]);
66/// assert_eq!(pretty_print(&e), "x + 1");
67/// ```
68#[must_use]
69pub fn pretty_print(expr: &Expr) -> String {
70    let mut buf = String::new();
71    write_expr(&mut buf, expr, Prec::Top);
72    buf
73}
74
75/// Write an expression at the given precedence context.
76///
77/// If the expression's own precedence is lower than `ctx`, wraps in parens.
78fn write_expr(buf: &mut String, expr: &Expr, ctx: Prec) {
79    match expr {
80        Expr::Var(name) => buf.push_str(name),
81
82        Expr::Lit(lit) => write_literal(buf, lit),
83
84        Expr::Lam(param, body) => {
85            let needs_parens = ctx > Prec::Top;
86            if needs_parens {
87                buf.push('(');
88            }
89            write_lambda_chain(buf, param, body);
90            if needs_parens {
91                buf.push(')');
92            }
93        }
94
95        Expr::App(func, arg) => {
96            write_app(buf, expr, ctx);
97            let _ = (func, arg); // used inside write_app
98        }
99
100        Expr::Record(fields) => {
101            write_record_expr(buf, fields);
102        }
103
104        Expr::List(items) => {
105            buf.push('[');
106            for (i, item) in items.iter().enumerate() {
107                if i > 0 {
108                    buf.push_str(", ");
109                }
110                write_expr(buf, item, Prec::Top);
111            }
112            buf.push(']');
113        }
114
115        Expr::Field(inner, name) => {
116            write_expr(buf, inner, Prec::Atom);
117            buf.push('.');
118            buf.push_str(name);
119        }
120
121        Expr::Index(inner, idx) => {
122            write_expr(buf, inner, Prec::Atom);
123            buf.push('[');
124            write_expr(buf, idx, Prec::Top);
125            buf.push(']');
126        }
127
128        Expr::Match { scrutinee, arms } => {
129            write_match(buf, scrutinee, arms, ctx);
130        }
131
132        Expr::Let { name, value, body } => {
133            write_let(buf, name, value, body, ctx);
134        }
135
136        Expr::Builtin(op, args) => {
137            write_builtin(buf, *op, args, ctx);
138        }
139    }
140}
141
142/// Write a chain of nested lambdas as `\x y z -> body`.
143fn write_lambda_chain(buf: &mut String, first_param: &Arc<str>, first_body: &Expr) {
144    buf.push('\\');
145    buf.push_str(first_param);
146    let mut body = first_body;
147    while let Expr::Lam(param, inner) = body {
148        buf.push(' ');
149        buf.push_str(param);
150        body = inner;
151    }
152    buf.push_str(" -> ");
153    write_expr(buf, body, Prec::Top);
154}
155
156/// Write function application, collecting curried args: `f x y z`.
157fn write_app(buf: &mut String, expr: &Expr, ctx: Prec) {
158    let needs_parens = ctx > Prec::App;
159    if needs_parens {
160        buf.push('(');
161    }
162
163    // Collect the application spine.
164    let mut spine: Vec<&Expr> = Vec::new();
165    let mut head = expr;
166    while let Expr::App(func, arg) = head {
167        spine.push(arg);
168        head = func;
169    }
170    spine.reverse();
171
172    write_expr(buf, head, Prec::App);
173    for arg in &spine {
174        buf.push(' ');
175        write_expr(buf, arg, Prec::Atom);
176    }
177
178    if needs_parens {
179        buf.push(')');
180    }
181}
182
183/// Write a record expression with punning where appropriate.
184fn write_record_expr(buf: &mut String, fields: &[(Arc<str>, Expr)]) {
185    buf.push_str("{ ");
186    for (i, (name, val)) in fields.iter().enumerate() {
187        if i > 0 {
188            buf.push_str(", ");
189        }
190        // Record punning: `{ x }` when field name equals variable name.
191        if let Expr::Var(v) = val {
192            if v == name {
193                buf.push_str(name);
194                continue;
195            }
196        }
197        buf.push_str(name);
198        buf.push_str(" = ");
199        write_expr(buf, val, Prec::Top);
200    }
201    buf.push_str(" }");
202}
203
204/// Write a match expression.
205///
206/// Detects `if/then/else` patterns (two arms with True and Wildcard)
207/// and emits those in the shorter form.
208fn write_match(buf: &mut String, scrutinee: &Expr, arms: &[(Pattern, Expr)], ctx: Prec) {
209    // Detect if/then/else: Match with [Lit(Bool(true)) -> then, Wildcard -> else]
210    if arms.len() == 2 {
211        if let (Pattern::Lit(Literal::Bool(true)), then_branch) = &arms[0] {
212            if let (Pattern::Wildcard, else_branch) = &arms[1] {
213                let needs_parens = ctx > Prec::Top;
214                if needs_parens {
215                    buf.push('(');
216                }
217                buf.push_str("if ");
218                write_expr(buf, scrutinee, Prec::Top);
219                buf.push_str(" then ");
220                write_expr(buf, then_branch, Prec::Top);
221                buf.push_str(" else ");
222                write_expr(buf, else_branch, Prec::Top);
223                if needs_parens {
224                    buf.push(')');
225                }
226                return;
227            }
228        }
229    }
230
231    let needs_parens = ctx > Prec::Top;
232    if needs_parens {
233        buf.push('(');
234    }
235    buf.push_str("case ");
236    write_expr(buf, scrutinee, Prec::Top);
237    buf.push_str(" of\n");
238    for (i, (pat, body)) in arms.iter().enumerate() {
239        if i > 0 {
240            buf.push('\n');
241        }
242        buf.push_str("  ");
243        write_pattern(buf, pat);
244        buf.push_str(" -> ");
245        write_expr(buf, body, Prec::Top);
246    }
247    if needs_parens {
248        buf.push(')');
249    }
250}
251
252/// Write a let binding, collapsing nested lets into a layout block.
253fn write_let(buf: &mut String, name: &Arc<str>, value: &Expr, body: &Expr, ctx: Prec) {
254    let needs_parens = ctx > Prec::Top;
255    if needs_parens {
256        buf.push('(');
257    }
258
259    // Collect chained lets.
260    let mut bindings: Vec<(&Arc<str>, &Expr)> = vec![(name, value)];
261    let mut final_body = body;
262    while let Expr::Let {
263        name: n,
264        value: v,
265        body: b,
266    } = final_body
267    {
268        bindings.push((n, v));
269        final_body = b;
270    }
271
272    if bindings.len() == 1 {
273        buf.push_str("let ");
274        buf.push_str(name);
275        buf.push_str(" = ");
276        write_expr(buf, value, Prec::Top);
277        buf.push_str(" in ");
278    } else {
279        buf.push_str("let\n");
280        for (n, v) in &bindings {
281            buf.push_str("  ");
282            buf.push_str(n);
283            buf.push_str(" = ");
284            write_expr(buf, v, Prec::Top);
285            buf.push('\n');
286        }
287        buf.push_str("in ");
288    }
289    write_expr(buf, final_body, Prec::Top);
290
291    if needs_parens {
292        buf.push(')');
293    }
294}
295
296/// Write a builtin operation, using infix/prefix syntax where possible.
297fn write_builtin(buf: &mut String, op: BuiltinOp, args: &[Expr], ctx: Prec) {
298    // Try infix binary operators.
299    if let Some((sym, prec, assoc)) = infix_info(op) {
300        if args.len() == 2 {
301            let needs_parens = ctx > prec;
302            if needs_parens {
303                buf.push('(');
304            }
305            // For left-associative operators, the left child is fine at the
306            // same precedence but the right child needs to be tighter (to
307            // avoid ambiguity). Vice versa for right-associative.
308            let (left_ctx, right_ctx) = match assoc {
309                Assoc::Left => (prec, next_prec(prec)),
310                Assoc::Right => (next_prec(prec), prec),
311            };
312            write_expr(buf, &args[0], left_ctx);
313            buf.push(' ');
314            buf.push_str(sym);
315            buf.push(' ');
316            write_expr(buf, &args[1], right_ctx);
317            if needs_parens {
318                buf.push(')');
319            }
320            return;
321        }
322    }
323
324    // Edge traversal: `expr -> edge`
325    if op == BuiltinOp::Edge && args.len() == 2 {
326        if let Expr::Lit(Literal::Str(edge_name)) = &args[1] {
327            let needs_parens = ctx > Prec::Atom;
328            if needs_parens {
329                buf.push('(');
330            }
331            write_expr(buf, &args[0], Prec::Atom);
332            buf.push_str(" -> ");
333            buf.push_str(edge_name);
334            if needs_parens {
335                buf.push(')');
336            }
337            return;
338        }
339    }
340
341    // Unary prefix: negation and logical not.
342    if op == BuiltinOp::Neg && args.len() == 1 {
343        let needs_parens = ctx > Prec::Unary;
344        if needs_parens {
345            buf.push('(');
346        }
347        buf.push('-');
348        write_expr(buf, &args[0], Prec::Atom);
349        if needs_parens {
350            buf.push(')');
351        }
352        return;
353    }
354
355    if op == BuiltinOp::Not && args.len() == 1 {
356        let needs_parens = ctx > Prec::Unary;
357        if needs_parens {
358            buf.push('(');
359        }
360        buf.push_str("not ");
361        write_expr(buf, &args[0], Prec::Atom);
362        if needs_parens {
363            buf.push(')');
364        }
365        return;
366    }
367
368    // Fallback: function call syntax `name arg1 arg2 ...`
369    let needs_parens = ctx > Prec::App && !args.is_empty();
370    if needs_parens {
371        buf.push('(');
372    }
373    buf.push_str(builtin_name(op));
374    // Higher-order list builtins are stored list-first but written
375    // function-first, inverting the parser's lowering so that printing a
376    // parsed expression reproduces the source form.
377    let surface: Vec<&Expr> = surface_arg_order(op, args);
378    for arg in surface {
379        buf.push(' ');
380        write_expr(buf, arg, Prec::Atom);
381    }
382    if needs_parens {
383        buf.push(')');
384    }
385}
386
387/// Reorder a builtin's stored arguments into the order the surface
388/// syntax writes them.
389///
390/// The inverse of the parser's `lower_list_builtin_args`: `map`,
391/// `filter`, and `flat_map` are stored `[xs, f]` and written `f xs`;
392/// `fold` is stored `[xs, z, f]` and written `f z xs`. Every other
393/// builtin uses one order at both layers.
394fn surface_arg_order(op: BuiltinOp, args: &[Expr]) -> Vec<&Expr> {
395    match (op, args.len()) {
396        (BuiltinOp::Map | BuiltinOp::Filter | BuiltinOp::FlatMap, 2) => {
397            vec![&args[1], &args[0]]
398        }
399        (BuiltinOp::Fold, 3) => vec![&args[2], &args[1], &args[0]],
400        _ => args.iter().collect(),
401    }
402}
403
404/// Map a builtin op to its infix operator symbol, precedence, and associativity.
405///
406/// Returns `None` for builtins that should use function call syntax.
407const fn infix_info(op: BuiltinOp) -> Option<(&'static str, Prec, Assoc)> {
408    match op {
409        BuiltinOp::Or => Some(("||", Prec::Or, Assoc::Left)),
410        BuiltinOp::And => Some(("&&", Prec::And, Assoc::Left)),
411        BuiltinOp::Eq => Some(("==", Prec::Cmp, Assoc::Right)),
412        BuiltinOp::Neq => Some(("/=", Prec::Cmp, Assoc::Right)),
413        BuiltinOp::Lt => Some(("<", Prec::Cmp, Assoc::Right)),
414        BuiltinOp::Lte => Some(("<=", Prec::Cmp, Assoc::Right)),
415        BuiltinOp::Gt => Some((">", Prec::Cmp, Assoc::Right)),
416        BuiltinOp::Gte => Some((">=", Prec::Cmp, Assoc::Right)),
417        BuiltinOp::Concat => Some(("++", Prec::Concat, Assoc::Right)),
418        BuiltinOp::Add => Some(("+", Prec::AddSub, Assoc::Left)),
419        BuiltinOp::Sub => Some(("-", Prec::AddSub, Assoc::Left)),
420        BuiltinOp::Mul => Some(("*", Prec::MulDiv, Assoc::Left)),
421        BuiltinOp::Div => Some(("/", Prec::MulDiv, Assoc::Left)),
422        BuiltinOp::Mod => Some(("%", Prec::MulDiv, Assoc::Left)),
423        _ => None,
424    }
425}
426
427/// Get the next higher precedence level.
428const fn next_prec(p: Prec) -> Prec {
429    match p {
430        Prec::Top => Prec::Pipe,
431        Prec::Pipe => Prec::Or,
432        Prec::Or => Prec::And,
433        Prec::And => Prec::Cmp,
434        Prec::Cmp => Prec::Concat,
435        Prec::Concat => Prec::AddSub,
436        Prec::AddSub => Prec::MulDiv,
437        Prec::MulDiv => Prec::Unary,
438        Prec::Unary => Prec::App,
439        Prec::App | Prec::Atom => Prec::Atom,
440    }
441}
442
443/// Map a builtin op to its canonical function name for call syntax.
444const fn builtin_name(op: BuiltinOp) -> &'static str {
445    match op {
446        BuiltinOp::Add => "add",
447        BuiltinOp::Sub => "sub",
448        BuiltinOp::Mul => "mul",
449        BuiltinOp::Div => "div",
450        BuiltinOp::Mod => "mod",
451        BuiltinOp::Neg => "neg",
452        BuiltinOp::Abs => "abs",
453        BuiltinOp::Floor => "floor",
454        BuiltinOp::Ceil => "ceil",
455        BuiltinOp::Round => "round",
456        BuiltinOp::Eq => "eq",
457        BuiltinOp::Neq => "neq",
458        BuiltinOp::Lt => "lt",
459        BuiltinOp::Lte => "lte",
460        BuiltinOp::Gt => "gt",
461        BuiltinOp::Gte => "gte",
462        BuiltinOp::And => "and",
463        BuiltinOp::Or => "or",
464        BuiltinOp::Not => "not",
465        BuiltinOp::Concat => "concat",
466        BuiltinOp::Len => "len",
467        BuiltinOp::Slice => "slice",
468        BuiltinOp::Upper => "upper",
469        BuiltinOp::Lower => "lower",
470        BuiltinOp::Trim => "trim",
471        BuiltinOp::Split => "split",
472        BuiltinOp::Join => "join",
473        BuiltinOp::Replace => "replace",
474        BuiltinOp::Contains => "contains",
475        BuiltinOp::Map => "map",
476        BuiltinOp::Filter => "filter",
477        BuiltinOp::Fold => "fold",
478        BuiltinOp::Append => "append",
479        BuiltinOp::Head => "head",
480        BuiltinOp::Tail => "tail",
481        BuiltinOp::Reverse => "reverse",
482        BuiltinOp::FlatMap => "flat_map",
483        BuiltinOp::Length => "length",
484        BuiltinOp::Range => "range",
485        BuiltinOp::MergeRecords => "merge",
486        BuiltinOp::Keys => "keys",
487        BuiltinOp::Values => "values",
488        BuiltinOp::HasField => "has_field",
489        BuiltinOp::DefaultVal => "default",
490        BuiltinOp::Clamp => "clamp",
491        BuiltinOp::TruncateStr => "truncate_str",
492        BuiltinOp::IntToFloat => "int_to_float",
493        BuiltinOp::FloatToInt => "float_to_int",
494        BuiltinOp::IntToStr => "int_to_str",
495        BuiltinOp::FloatToStr => "float_to_str",
496        BuiltinOp::StrToInt => "str_to_int",
497        BuiltinOp::StrToFloat => "str_to_float",
498        BuiltinOp::TypeOf => "type_of",
499        BuiltinOp::IsNull => "is_null",
500        BuiltinOp::IsList => "is_list",
501        BuiltinOp::Edge => "edge",
502        BuiltinOp::Children => "children",
503        BuiltinOp::HasEdge => "has_edge",
504        BuiltinOp::EdgeCount => "edge_count",
505        BuiltinOp::Anchor => "anchor",
506    }
507}
508
509/// Write a literal value.
510fn write_literal(buf: &mut String, lit: &Literal) {
511    match lit {
512        Literal::Bool(true) => buf.push_str("True"),
513        Literal::Bool(false) => buf.push_str("False"),
514        Literal::Int(n) => {
515            let _ = write!(buf, "{n}");
516        }
517        Literal::Float(f) => {
518            // Ensure there is always a decimal point so the parser
519            // recognizes this as a float, not an int.
520            let s = format!("{f}");
521            if s.contains('.') {
522                buf.push_str(&s);
523            } else {
524                let _ = write!(buf, "{f}.0");
525            }
526        }
527        Literal::Str(s) => {
528            buf.push('"');
529            // Escape backslashes and double quotes.
530            for ch in s.chars() {
531                match ch {
532                    '\\' => buf.push_str("\\\\"),
533                    '"' => buf.push_str("\\\""),
534                    '\n' => buf.push_str("\\n"),
535                    '\r' => buf.push_str("\\r"),
536                    '\t' => buf.push_str("\\t"),
537                    c => buf.push(c),
538                }
539            }
540            buf.push('"');
541        }
542        Literal::Bytes(bytes) => {
543            // No native bytes syntax; emit as a list of ints.
544            buf.push('[');
545            for (i, b) in bytes.iter().enumerate() {
546                if i > 0 {
547                    buf.push_str(", ");
548                }
549                let _ = write!(buf, "{b}");
550            }
551            buf.push(']');
552        }
553        Literal::Null => buf.push_str("Nothing"),
554        Literal::Record(fields) => {
555            buf.push_str("{ ");
556            for (i, (name, val)) in fields.iter().enumerate() {
557                if i > 0 {
558                    buf.push_str(", ");
559                }
560                buf.push_str(name);
561                buf.push_str(" = ");
562                write_literal(buf, val);
563            }
564            buf.push_str(" }");
565        }
566        Literal::List(items) => {
567            buf.push('[');
568            for (i, item) in items.iter().enumerate() {
569                if i > 0 {
570                    buf.push_str(", ");
571                }
572                write_literal(buf, item);
573            }
574            buf.push(']');
575        }
576        Literal::Closure { param, body, .. } => {
577            // Print as a lambda; the captured env is lost but the
578            // expression form is preserved for round-tripping.
579            buf.push('\\');
580            buf.push_str(param);
581            buf.push_str(" -> ");
582            write_expr(buf, body, Prec::Top);
583        }
584    }
585}
586
587/// Write a pattern.
588fn write_pattern(buf: &mut String, pat: &Pattern) {
589    match pat {
590        Pattern::Wildcard => buf.push('_'),
591        Pattern::Var(name) => buf.push_str(name),
592        Pattern::Lit(lit) => write_literal(buf, lit),
593        Pattern::Record(fields) => {
594            buf.push_str("{ ");
595            for (i, (name, p)) in fields.iter().enumerate() {
596                if i > 0 {
597                    buf.push_str(", ");
598                }
599                // Record pattern punning: `{ x }` when field pattern is Var(x).
600                if let Pattern::Var(v) = p {
601                    if v == name {
602                        buf.push_str(name);
603                        continue;
604                    }
605                }
606                buf.push_str(name);
607                buf.push_str(" = ");
608                write_pattern(buf, p);
609            }
610            buf.push_str(" }");
611        }
612        Pattern::List(pats) => {
613            buf.push('[');
614            for (i, p) in pats.iter().enumerate() {
615                if i > 0 {
616                    buf.push_str(", ");
617                }
618                write_pattern(buf, p);
619            }
620            buf.push(']');
621        }
622        Pattern::Constructor(name, args) => {
623            buf.push_str(name);
624            for arg in args {
625                buf.push(' ');
626                // Wrap constructor args in parens if they are themselves
627                // constructors with args (to avoid ambiguity).
628                let needs_parens = matches!(arg, Pattern::Constructor(_, a) if !a.is_empty());
629                if needs_parens {
630                    buf.push('(');
631                }
632                write_pattern(buf, arg);
633                if needs_parens {
634                    buf.push(')');
635                }
636            }
637        }
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644    use crate::{parse, tokenize};
645
646    /// Parse a string, pretty print it, re-parse, and verify equality.
647    fn round_trip(input: &str) {
648        let tokens1 = tokenize(input).unwrap_or_else(|e| panic!("first lex failed: {e}"));
649        let expr1 = parse(&tokens1).unwrap_or_else(|e| panic!("first parse failed: {e:?}"));
650        let printed = pretty_print(&expr1);
651        let tokens2 = tokenize(&printed).unwrap_or_else(|e| {
652            panic!("re-lex failed for {printed:?}: {e}");
653        });
654        let expr2 = parse(&tokens2).unwrap_or_else(|e| {
655            panic!("re-parse failed for {printed:?}: {e:?}");
656        });
657        assert_eq!(
658            expr1, expr2,
659            "round trip failed.\n  input:   {input:?}\n  printed: {printed:?}"
660        );
661    }
662
663    /// Pretty print an expression built programmatically and check output.
664    fn prints_as(expr: &Expr, expected: &str) {
665        let actual = pretty_print(expr);
666        assert_eq!(actual, expected, "pretty_print mismatch");
667    }
668
669    // ── Literals ──────────────────────────────────────────────────
670
671    #[test]
672    fn lit_int() {
673        prints_as(&Expr::Lit(Literal::Int(42)), "42");
674    }
675
676    #[test]
677    fn lit_negative_int() {
678        prints_as(&Expr::Lit(Literal::Int(-5)), "-5");
679    }
680
681    #[test]
682    fn lit_float() {
683        prints_as(&Expr::Lit(Literal::Float(3.125)), "3.125");
684    }
685
686    #[test]
687    fn lit_string() {
688        prints_as(&Expr::Lit(Literal::Str("hello".into())), r#""hello""#);
689    }
690
691    #[test]
692    fn lit_string_escapes() {
693        prints_as(
694            &Expr::Lit(Literal::Str("say \"hi\"".into())),
695            r#""say \"hi\"""#,
696        );
697    }
698
699    #[test]
700    fn lit_bool() {
701        prints_as(&Expr::Lit(Literal::Bool(true)), "True");
702        prints_as(&Expr::Lit(Literal::Bool(false)), "False");
703    }
704
705    #[test]
706    fn lit_null() {
707        prints_as(&Expr::Lit(Literal::Null), "Nothing");
708    }
709
710    #[test]
711    fn lit_bytes() {
712        prints_as(&Expr::Lit(Literal::Bytes(vec![1, 2, 3])), "[1, 2, 3]");
713    }
714
715    // ── Variables ─────────────────────────────────────────────────
716
717    #[test]
718    fn variable() {
719        prints_as(&Expr::Var(Arc::from("x")), "x");
720    }
721
722    // ── Lambda ────────────────────────────────────────────────────
723
724    #[test]
725    fn lambda_simple() {
726        prints_as(
727            &Expr::Lam(Arc::from("x"), Box::new(Expr::Var(Arc::from("x")))),
728            "\\x -> x",
729        );
730    }
731
732    #[test]
733    fn lambda_multi_param() {
734        prints_as(
735            &Expr::Lam(
736                Arc::from("x"),
737                Box::new(Expr::Lam(
738                    Arc::from("y"),
739                    Box::new(Expr::Builtin(
740                        BuiltinOp::Add,
741                        vec![Expr::Var(Arc::from("x")), Expr::Var(Arc::from("y"))],
742                    )),
743                )),
744            ),
745            "\\x y -> x + y",
746        );
747    }
748
749    #[test]
750    fn lambda_round_trip() {
751        round_trip("\\x -> x + 1");
752        round_trip("\\x y -> x + y");
753    }
754
755    // ── Application ───────────────────────────────────────────────
756
757    #[test]
758    fn app_simple() {
759        prints_as(
760            &Expr::App(
761                Box::new(Expr::Var(Arc::from("f"))),
762                Box::new(Expr::Var(Arc::from("x"))),
763            ),
764            "f x",
765        );
766    }
767
768    #[test]
769    fn app_chain() {
770        prints_as(
771            &Expr::App(
772                Box::new(Expr::App(
773                    Box::new(Expr::Var(Arc::from("f"))),
774                    Box::new(Expr::Var(Arc::from("x"))),
775                )),
776                Box::new(Expr::Var(Arc::from("y"))),
777            ),
778            "f x y",
779        );
780    }
781
782    #[test]
783    fn app_complex_arg() {
784        // f (g x) should parenthesize the argument
785        prints_as(
786            &Expr::App(
787                Box::new(Expr::Var(Arc::from("f"))),
788                Box::new(Expr::App(
789                    Box::new(Expr::Var(Arc::from("g"))),
790                    Box::new(Expr::Var(Arc::from("x"))),
791                )),
792            ),
793            "f (g x)",
794        );
795    }
796
797    // ── Record ────────────────────────────────────────────────────
798
799    #[test]
800    fn record_simple() {
801        prints_as(
802            &Expr::Record(vec![
803                (Arc::from("x"), Expr::Lit(Literal::Int(1))),
804                (Arc::from("y"), Expr::Lit(Literal::Int(2))),
805            ]),
806            "{ x = 1, y = 2 }",
807        );
808    }
809
810    #[test]
811    fn record_punning() {
812        prints_as(
813            &Expr::Record(vec![
814                (Arc::from("x"), Expr::Var(Arc::from("x"))),
815                (Arc::from("y"), Expr::Var(Arc::from("y"))),
816            ]),
817            "{ x, y }",
818        );
819    }
820
821    #[test]
822    fn record_mixed_punning() {
823        prints_as(
824            &Expr::Record(vec![
825                (Arc::from("x"), Expr::Var(Arc::from("x"))),
826                (Arc::from("y"), Expr::Lit(Literal::Int(42))),
827            ]),
828            "{ x, y = 42 }",
829        );
830    }
831
832    #[test]
833    fn record_round_trip() {
834        round_trip("{ name = x, age = 30 }");
835        round_trip("{ x, y }");
836    }
837
838    // ── List ──────────────────────────────────────────────────────
839
840    #[test]
841    fn list_simple() {
842        prints_as(
843            &Expr::List(vec![
844                Expr::Lit(Literal::Int(1)),
845                Expr::Lit(Literal::Int(2)),
846                Expr::Lit(Literal::Int(3)),
847            ]),
848            "[1, 2, 3]",
849        );
850    }
851
852    #[test]
853    fn list_empty() {
854        prints_as(&Expr::List(vec![]), "[]");
855    }
856
857    #[test]
858    fn list_round_trip() {
859        round_trip("[1, 2, 3]");
860        round_trip("[]");
861    }
862
863    // ── Field access ──────────────────────────────────────────────
864
865    #[test]
866    fn field_access() {
867        prints_as(
868            &Expr::Field(Box::new(Expr::Var(Arc::from("x"))), Arc::from("name")),
869            "x.name",
870        );
871    }
872
873    #[test]
874    fn field_chain() {
875        prints_as(
876            &Expr::Field(
877                Box::new(Expr::Field(
878                    Box::new(Expr::Var(Arc::from("x"))),
879                    Arc::from("a"),
880                )),
881                Arc::from("b"),
882            ),
883            "x.a.b",
884        );
885    }
886
887    #[test]
888    fn field_round_trip() {
889        round_trip("x.name");
890        round_trip("x.a.b");
891    }
892
893    // ── Edge traversal ────────────────────────────────────────────
894
895    #[test]
896    fn edge_traversal() {
897        prints_as(
898            &Expr::Builtin(
899                BuiltinOp::Edge,
900                vec![
901                    Expr::Var(Arc::from("doc")),
902                    Expr::Lit(Literal::Str("layers".into())),
903                ],
904            ),
905            "doc -> layers",
906        );
907    }
908
909    #[test]
910    fn edge_chain() {
911        prints_as(
912            &Expr::Builtin(
913                BuiltinOp::Edge,
914                vec![
915                    Expr::Builtin(
916                        BuiltinOp::Edge,
917                        vec![
918                            Expr::Var(Arc::from("doc")),
919                            Expr::Lit(Literal::Str("layers".into())),
920                        ],
921                    ),
922                    Expr::Lit(Literal::Str("annotations".into())),
923                ],
924            ),
925            "doc -> layers -> annotations",
926        );
927    }
928
929    #[test]
930    fn edge_round_trip() {
931        round_trip("doc -> layers");
932        round_trip("doc -> layers -> annotations");
933    }
934
935    // ── Infix operators ───────────────────────────────────────────
936
937    #[test]
938    fn infix_add() {
939        prints_as(
940            &Expr::Builtin(
941                BuiltinOp::Add,
942                vec![Expr::Var(Arc::from("x")), Expr::Lit(Literal::Int(1))],
943            ),
944            "x + 1",
945        );
946    }
947
948    #[test]
949    fn infix_precedence_no_parens() {
950        // 1 + 2 * 3 should not need parens because * binds tighter.
951        prints_as(
952            &Expr::Builtin(
953                BuiltinOp::Add,
954                vec![
955                    Expr::Lit(Literal::Int(1)),
956                    Expr::Builtin(
957                        BuiltinOp::Mul,
958                        vec![Expr::Lit(Literal::Int(2)), Expr::Lit(Literal::Int(3))],
959                    ),
960                ],
961            ),
962            "1 + 2 * 3",
963        );
964    }
965
966    #[test]
967    fn infix_precedence_needs_parens() {
968        // (1 + 2) * 3 needs parens because + is lower than *.
969        prints_as(
970            &Expr::Builtin(
971                BuiltinOp::Mul,
972                vec![
973                    Expr::Builtin(
974                        BuiltinOp::Add,
975                        vec![Expr::Lit(Literal::Int(1)), Expr::Lit(Literal::Int(2))],
976                    ),
977                    Expr::Lit(Literal::Int(3)),
978                ],
979            ),
980            "(1 + 2) * 3",
981        );
982    }
983
984    #[test]
985    fn infix_left_assoc_no_parens() {
986        // 1 + 2 + 3 is left-associative, so (1+2)+3 needs no parens.
987        prints_as(
988            &Expr::Builtin(
989                BuiltinOp::Add,
990                vec![
991                    Expr::Builtin(
992                        BuiltinOp::Add,
993                        vec![Expr::Lit(Literal::Int(1)), Expr::Lit(Literal::Int(2))],
994                    ),
995                    Expr::Lit(Literal::Int(3)),
996                ],
997            ),
998            "1 + 2 + 3",
999        );
1000    }
1001
1002    #[test]
1003    fn infix_right_assoc_needs_parens() {
1004        // For left-assoc +, 1 + (2 + 3) needs parens on the right.
1005        prints_as(
1006            &Expr::Builtin(
1007                BuiltinOp::Add,
1008                vec![
1009                    Expr::Lit(Literal::Int(1)),
1010                    Expr::Builtin(
1011                        BuiltinOp::Add,
1012                        vec![Expr::Lit(Literal::Int(2)), Expr::Lit(Literal::Int(3))],
1013                    ),
1014                ],
1015            ),
1016            "1 + (2 + 3)",
1017        );
1018    }
1019
1020    #[test]
1021    fn infix_concat_right_assoc() {
1022        // ++ is right-associative, so a ++ (b ++ c) needs no parens.
1023        prints_as(
1024            &Expr::Builtin(
1025                BuiltinOp::Concat,
1026                vec![
1027                    Expr::Var(Arc::from("a")),
1028                    Expr::Builtin(
1029                        BuiltinOp::Concat,
1030                        vec![Expr::Var(Arc::from("b")), Expr::Var(Arc::from("c"))],
1031                    ),
1032                ],
1033            ),
1034            "a ++ b ++ c",
1035        );
1036    }
1037
1038    #[test]
1039    fn infix_comparison() {
1040        prints_as(
1041            &Expr::Builtin(
1042                BuiltinOp::Eq,
1043                vec![Expr::Var(Arc::from("x")), Expr::Lit(Literal::Int(1))],
1044            ),
1045            "x == 1",
1046        );
1047        prints_as(
1048            &Expr::Builtin(
1049                BuiltinOp::Neq,
1050                vec![Expr::Var(Arc::from("x")), Expr::Lit(Literal::Int(1))],
1051            ),
1052            "x /= 1",
1053        );
1054        prints_as(
1055            &Expr::Builtin(
1056                BuiltinOp::Lt,
1057                vec![Expr::Var(Arc::from("x")), Expr::Var(Arc::from("y"))],
1058            ),
1059            "x < y",
1060        );
1061    }
1062
1063    #[test]
1064    fn infix_logical() {
1065        prints_as(
1066            &Expr::Builtin(
1067                BuiltinOp::And,
1068                vec![Expr::Var(Arc::from("a")), Expr::Var(Arc::from("b"))],
1069            ),
1070            "a && b",
1071        );
1072        prints_as(
1073            &Expr::Builtin(
1074                BuiltinOp::Or,
1075                vec![Expr::Var(Arc::from("a")), Expr::Var(Arc::from("b"))],
1076            ),
1077            "a || b",
1078        );
1079    }
1080
1081    #[test]
1082    fn infix_round_trips() {
1083        round_trip("1 + 2");
1084        round_trip("1 + 2 * 3");
1085        round_trip("(1 + 2) * 3");
1086        round_trip("a && b || c");
1087        round_trip("x == 1");
1088        round_trip("x /= 1");
1089    }
1090
1091    // ── Prefix operators ──────────────────────────────────────────
1092
1093    #[test]
1094    fn prefix_neg() {
1095        prints_as(
1096            &Expr::Builtin(BuiltinOp::Neg, vec![Expr::Var(Arc::from("x"))]),
1097            "-x",
1098        );
1099    }
1100
1101    #[test]
1102    fn prefix_not() {
1103        prints_as(
1104            &Expr::Builtin(BuiltinOp::Not, vec![Expr::Lit(Literal::Bool(true))]),
1105            "not True",
1106        );
1107    }
1108
1109    #[test]
1110    fn prefix_round_trip() {
1111        round_trip("-x");
1112        round_trip("not True");
1113    }
1114
1115    // ── Builtin function call syntax ──────────────────────────────
1116
1117    #[test]
1118    fn builtin_function_call() {
1119        // Stored list-first, written function-first.
1120        prints_as(
1121            &Expr::Builtin(
1122                BuiltinOp::Map,
1123                vec![Expr::Var(Arc::from("xs")), Expr::Var(Arc::from("f"))],
1124            ),
1125            "map f xs",
1126        );
1127    }
1128
1129    #[test]
1130    fn fold_prints_function_first() {
1131        prints_as(
1132            &Expr::Builtin(
1133                BuiltinOp::Fold,
1134                vec![
1135                    Expr::Var(Arc::from("xs")),
1136                    Expr::Var(Arc::from("z")),
1137                    Expr::Var(Arc::from("f")),
1138                ],
1139            ),
1140            "fold f z xs",
1141        );
1142    }
1143
1144    #[test]
1145    fn builtin_unary() {
1146        prints_as(
1147            &Expr::Builtin(BuiltinOp::Head, vec![Expr::Var(Arc::from("xs"))]),
1148            "head xs",
1149        );
1150    }
1151
1152    #[test]
1153    fn builtin_round_trip() {
1154        round_trip("map f xs");
1155        round_trip("head xs");
1156        round_trip("filter f xs");
1157        round_trip("fold f z xs");
1158        round_trip("flat_map f xs");
1159    }
1160
1161    // ── Let ───────────────────────────────────────────────────────
1162
1163    #[test]
1164    fn let_simple() {
1165        prints_as(
1166            &Expr::Let {
1167                name: Arc::from("x"),
1168                value: Box::new(Expr::Lit(Literal::Int(1))),
1169                body: Box::new(Expr::Builtin(
1170                    BuiltinOp::Add,
1171                    vec![Expr::Var(Arc::from("x")), Expr::Lit(Literal::Int(1))],
1172                )),
1173            },
1174            "let x = 1 in x + 1",
1175        );
1176    }
1177
1178    #[test]
1179    fn let_round_trip() {
1180        round_trip("let x = 1 in x + 1");
1181    }
1182
1183    // ── If/then/else ──────────────────────────────────────────────
1184
1185    #[test]
1186    fn if_then_else() {
1187        let expr = Expr::Match {
1188            scrutinee: Box::new(Expr::Lit(Literal::Bool(true))),
1189            arms: vec![
1190                (
1191                    Pattern::Lit(Literal::Bool(true)),
1192                    Expr::Lit(Literal::Int(1)),
1193                ),
1194                (Pattern::Wildcard, Expr::Lit(Literal::Int(0))),
1195            ],
1196        };
1197        prints_as(&expr, "if True then 1 else 0");
1198    }
1199
1200    #[test]
1201    fn if_round_trip() {
1202        round_trip("if True then 1 else 0");
1203    }
1204
1205    // ── Case/of ───────────────────────────────────────────────────
1206
1207    #[test]
1208    fn case_of() {
1209        let expr = Expr::Match {
1210            scrutinee: Box::new(Expr::Var(Arc::from("x"))),
1211            arms: vec![
1212                (
1213                    Pattern::Lit(Literal::Bool(true)),
1214                    Expr::Lit(Literal::Int(1)),
1215                ),
1216                (
1217                    Pattern::Lit(Literal::Bool(false)),
1218                    Expr::Lit(Literal::Int(0)),
1219                ),
1220            ],
1221        };
1222        prints_as(&expr, "case x of\n  True -> 1\n  False -> 0");
1223    }
1224
1225    #[test]
1226    fn case_round_trip() {
1227        round_trip("case x of\n  True -> 1\n  False -> 0");
1228    }
1229
1230    // ── Nested expressions ────────────────────────────────────────
1231
1232    #[test]
1233    fn nested_let_in_lambda() {
1234        round_trip("\\x -> let y = x + 1 in y * 2");
1235    }
1236
1237    #[test]
1238    fn nested_if_in_let() {
1239        round_trip("let x = if True then 1 else 0 in x + 1");
1240    }
1241
1242    #[test]
1243    fn lambda_as_arg() {
1244        // f (\x -> x) should parenthesize the lambda argument
1245        prints_as(
1246            &Expr::App(
1247                Box::new(Expr::Var(Arc::from("f"))),
1248                Box::new(Expr::Lam(
1249                    Arc::from("x"),
1250                    Box::new(Expr::Var(Arc::from("x"))),
1251                )),
1252            ),
1253            "f (\\x -> x)",
1254        );
1255    }
1256
1257    #[test]
1258    fn complex_expression_round_trip() {
1259        round_trip("\\f xs -> map (\\x -> f x + 1) xs");
1260    }
1261
1262    // ── Pattern printing ──────────────────────────────────────────
1263
1264    #[test]
1265    fn pattern_wildcard() {
1266        let mut buf = String::new();
1267        write_pattern(&mut buf, &Pattern::Wildcard);
1268        assert_eq!(buf, "_");
1269    }
1270
1271    #[test]
1272    fn pattern_var() {
1273        let mut buf = String::new();
1274        write_pattern(&mut buf, &Pattern::Var(Arc::from("x")));
1275        assert_eq!(buf, "x");
1276    }
1277
1278    #[test]
1279    fn pattern_lit() {
1280        let mut buf = String::new();
1281        write_pattern(&mut buf, &Pattern::Lit(Literal::Int(42)));
1282        assert_eq!(buf, "42");
1283    }
1284
1285    #[test]
1286    fn pattern_list() {
1287        let mut buf = String::new();
1288        write_pattern(
1289            &mut buf,
1290            &Pattern::List(vec![
1291                Pattern::Var(Arc::from("x")),
1292                Pattern::Var(Arc::from("y")),
1293            ]),
1294        );
1295        assert_eq!(buf, "[x, y]");
1296    }
1297
1298    #[test]
1299    fn pattern_record_punning() {
1300        let mut buf = String::new();
1301        write_pattern(
1302            &mut buf,
1303            &Pattern::Record(vec![
1304                (Arc::from("x"), Pattern::Var(Arc::from("x"))),
1305                (Arc::from("y"), Pattern::Var(Arc::from("y"))),
1306            ]),
1307        );
1308        assert_eq!(buf, "{ x, y }");
1309    }
1310
1311    #[test]
1312    fn pattern_constructor() {
1313        let mut buf = String::new();
1314        write_pattern(
1315            &mut buf,
1316            &Pattern::Constructor(Arc::from("Just"), vec![Pattern::Var(Arc::from("x"))]),
1317        );
1318        assert_eq!(buf, "Just x");
1319    }
1320
1321    // ── Index ─────────────────────────────────────────────────────
1322
1323    #[test]
1324    fn index_access() {
1325        prints_as(
1326            &Expr::Index(
1327                Box::new(Expr::Var(Arc::from("xs"))),
1328                Box::new(Expr::Lit(Literal::Int(0))),
1329            ),
1330            "xs[0]",
1331        );
1332    }
1333
1334    // ── Literal record and list ───────────────────────────────────
1335
1336    #[test]
1337    fn literal_record() {
1338        prints_as(
1339            &Expr::Lit(Literal::Record(vec![
1340                (Arc::from("x"), Literal::Int(1)),
1341                (Arc::from("y"), Literal::Int(2)),
1342            ])),
1343            "{ x = 1, y = 2 }",
1344        );
1345    }
1346
1347    #[test]
1348    fn literal_list() {
1349        prints_as(
1350            &Expr::Lit(Literal::List(vec![Literal::Int(1), Literal::Int(2)])),
1351            "[1, 2]",
1352        );
1353    }
1354
1355    // ── Mixed precedence round trips ──────────────────────────────
1356
1357    #[test]
1358    fn precedence_logical_and_comparison() {
1359        round_trip("x == 1 && y == 2");
1360    }
1361
1362    #[test]
1363    fn precedence_arithmetic_in_comparison() {
1364        round_trip("x + 1 == y * 2");
1365    }
1366
1367    #[test]
1368    fn concat_round_trip() {
1369        round_trip(r#""hello" ++ " world""#);
1370    }
1371}