Skip to main content

logicaffeine_language/
printer.rs

1//! English rendering of imperative expressions — the shared surface behind
2//! power-assert failure messages, property counterexamples, and mutation
3//! survivor reports.
4//!
5//! [`render_expr`] turns an [`Expr`] back into source-like English (`a plus b`,
6//! `the total of xs`, `x is at least 10`). [`subexpressions`] collects the
7//! parts worth showing a runtime value for — named things and computations,
8//! never literals (already visible in the rendered text) — so a failed
9//! `Expect` can annotate each with its actual value.
10
11use crate::ast::stmt::{BinaryOpKind, Expr, Literal};
12use crate::intern::Interner;
13
14/// The English word(s) a binary operator reads as, in infix position.
15fn op_word(op: BinaryOpKind) -> &'static str {
16    use BinaryOpKind::*;
17    match op {
18        Add => "plus",
19        Subtract => "minus",
20        Multiply => "times",
21        Divide | ExactDivide => "divided by",
22        FloorDivide => "floor-divided by",
23        Pow => "to the power of",
24        Modulo => "modulo",
25        Eq => "is",
26        NotEq => "is not",
27        Lt => "is less than",
28        Gt => "is greater than",
29        LtEq => "is at most",
30        GtEq => "is at least",
31        And => "and",
32        Or => "or",
33        Concat => "combined with",
34        SeqConcat => "followed by",
35        ApproxEq => "is approximately",
36        BitXor => "xor",
37        BitAnd => "bitand",
38        BitOr => "bitor",
39        Shl => "shifted left by",
40        Shr => "shifted right by",
41    }
42}
43
44/// Render a literal as it reads in source.
45fn render_literal(lit: &Literal, interner: &Interner) -> String {
46    match lit {
47        Literal::Number(n) => n.to_string(),
48        Literal::Float(f) => f.to_string(),
49        Literal::Text(s) => format!("{:?}", interner.resolve(*s)),
50        Literal::Boolean(b) => b.to_string(),
51        Literal::Nothing => "nothing".to_string(),
52        Literal::Char(c) => format!("'{c}'"),
53        Literal::Duration(ns) => format!("{ns}ns"),
54        _ => "…".to_string(),
55    }
56}
57
58/// Render an expression as source-like English. Exotic shapes fall back to an
59/// ellipsis rather than panicking — a failure message must never crash.
60pub fn render_expr(expr: &Expr<'_>, interner: &Interner) -> String {
61    match expr {
62        Expr::Literal(lit) => render_literal(lit, interner),
63        Expr::Identifier(sym) => interner.resolve(*sym).to_string(),
64        Expr::BinaryOp { op, left, right } => {
65            format!(
66                "{} {} {}",
67                render_expr(left, interner),
68                op_word(*op),
69                render_expr(right, interner)
70            )
71        }
72        Expr::Not { operand } => format!("not {}", render_expr(operand, interner)),
73        Expr::Call { function, args } => {
74            let rendered: Vec<String> = args.iter().map(|a| render_expr(a, interner)).collect();
75            format!("{}({})", interner.resolve(*function), rendered.join(", "))
76        }
77        Expr::Index { collection, index } => {
78            format!(
79                "item {} of {}",
80                render_expr(index, interner),
81                render_expr(collection, interner)
82            )
83        }
84        Expr::Length { collection } => {
85            format!("the length of {}", render_expr(collection, interner))
86        }
87        Expr::Contains { collection, value } => {
88            format!(
89                "{} contains {}",
90                render_expr(collection, interner),
91                render_expr(value, interner)
92            )
93        }
94        Expr::FieldAccess { object, field } => {
95            format!(
96                "the {} of {}",
97                interner.resolve(*field),
98                render_expr(object, interner)
99            )
100        }
101        Expr::List(items) => {
102            let rendered: Vec<String> = items.iter().map(|i| render_expr(i, interner)).collect();
103            format!("[{}]", rendered.join(", "))
104        }
105        _ => "…".to_string(),
106    }
107}
108
109/// The subexpressions worth showing a runtime value for, in a stable order:
110/// the whole expression's interesting operands and their operands, depth-first,
111/// EXCLUDING bare literals (already visible in the rendered text). Used to
112/// build a failed expectation's power-assert annotations.
113pub fn subexpressions<'a, 'e>(expr: &'e Expr<'a>, out: &mut Vec<&'e Expr<'a>>) {
114    match expr {
115        Expr::Literal(_) => {}
116        Expr::Identifier(_) => out.push(expr),
117        Expr::BinaryOp { left, right, .. } => {
118            subexpressions(left, out);
119            subexpressions(right, out);
120        }
121        Expr::Not { operand } => subexpressions(operand, out),
122        Expr::Call { args, .. } => {
123            out.push(expr);
124            for a in args {
125                subexpressions(a, out);
126            }
127        }
128        Expr::Index { collection, index } => {
129            out.push(expr);
130            subexpressions(collection, out);
131            subexpressions(index, out);
132        }
133        Expr::Length { collection } => {
134            out.push(expr);
135            subexpressions(collection, out);
136        }
137        Expr::FieldAccess { object, .. } => {
138            out.push(expr);
139            subexpressions(object, out);
140        }
141        Expr::Contains { collection, value } => {
142            subexpressions(collection, out);
143            subexpressions(value, out);
144        }
145        Expr::List(items) => {
146            for i in items {
147                subexpressions(i, out);
148            }
149        }
150        _ => {}
151    }
152}