blots_core/
formatter.rs

1use crate::ast::{BinaryOp, DoStatement, Expr, RecordEntry, RecordKey, SpannedExpr};
2use crate::ast_to_source::{expr_to_source};
3use crate::values::LambdaArg;
4
5const DEFAULT_MAX_COLUMNS: usize = 80;
6const INDENT_SIZE: usize = 2;
7
8/// Format a Blots expression with intelligent line breaking
9pub fn format_expr(expr: &SpannedExpr, max_columns: Option<usize>) -> String {
10    let max_cols = max_columns.unwrap_or(DEFAULT_MAX_COLUMNS);
11    format_expr_impl(expr, max_cols, 0)
12}
13
14/// Internal formatting implementation with indentation tracking
15fn format_expr_impl(expr: &SpannedExpr, max_cols: usize, indent: usize) -> String {
16    // Special handling for lambdas to ensure correct argument formatting
17    if let Expr::Lambda { args, body } = &expr.node {
18        return format_lambda(args, body, max_cols, indent);
19    }
20
21    // First, try single-line formatting using our custom formatter
22    let single_line = format_single_line(expr);
23    let current_line_length = indent + single_line.len();
24
25    // If it fits on one line, use it
26    if current_line_length <= max_cols {
27        return single_line;
28    }
29
30    // Otherwise, apply smart multi-line formatting based on expression type
31    format_multiline(expr, max_cols, indent)
32}
33
34/// Format an expression on a single line (respecting our formatting rules)
35fn format_single_line(expr: &SpannedExpr) -> String {
36    match &expr.node {
37        Expr::Lambda { args, body } => {
38            let args_str: Vec<String> = args.iter().map(lambda_arg_to_str).collect();
39            let args_part = if args.len() == 1 && matches!(args[0], LambdaArg::Required(_)) {
40                args_str[0].clone()
41            } else {
42                format!("({})", args_str.join(", "))
43            };
44            format!("{} => {}", args_part, format_single_line(body))
45        }
46        Expr::Call { func, args } => {
47            let func_str = match &func.node {
48                Expr::Lambda { .. } => format!("({})", format_single_line(func)),
49                _ => format_single_line(func),
50            };
51            let args_str: Vec<String> = args.iter().map(format_single_line).collect();
52            format!("{}({})", func_str, args_str.join(", "))
53        }
54        Expr::List(items) => {
55            let items_str: Vec<String> = items.iter().map(format_single_line).collect();
56            format!("[{}]", items_str.join(", "))
57        }
58        Expr::Record(entries) => {
59            let entries_str: Vec<String> = entries.iter().map(|e| format_record_entry_single_line(e)).collect();
60            format!("{{{}}}", entries_str.join(", "))
61        }
62        // For everything else, use the existing expr_to_source
63        _ => expr_to_source(expr),
64    }
65}
66
67/// Format a record entry on a single line
68fn format_record_entry_single_line(entry: &RecordEntry) -> String {
69    match &entry.key {
70        RecordKey::Static(key) => format!("{}: {}", key, format_single_line(&entry.value)),
71        RecordKey::Dynamic(key_expr) => {
72            format!("[{}]: {}", format_single_line(key_expr), format_single_line(&entry.value))
73        }
74        RecordKey::Shorthand(name) => name.clone(),
75        RecordKey::Spread(expr) => format_single_line(expr),
76    }
77}
78
79/// Format an expression across multiple lines
80fn format_multiline(expr: &SpannedExpr, max_cols: usize, indent: usize) -> String {
81    match &expr.node {
82        Expr::List(items) => format_list_multiline(items, max_cols, indent),
83        Expr::Record(entries) => format_record_multiline(entries, max_cols, indent),
84        Expr::Conditional { condition, then_expr, else_expr } => {
85            format_conditional_multiline(condition, then_expr, else_expr, max_cols, indent)
86        }
87        Expr::Call { func, args } => format_call_multiline(func, args, max_cols, indent),
88        Expr::BinaryOp { op, left, right } => {
89            format_binary_op_multiline(op, left, right, max_cols, indent)
90        }
91        Expr::DoBlock { statements, return_expr } => {
92            format_do_block_multiline(statements, return_expr, indent)
93        }
94        // For other expression types, fall back to single-line
95        _ => expr_to_source(expr),
96    }
97}
98
99/// Format a list with line breaks
100fn format_list_multiline(items: &[SpannedExpr], max_cols: usize, indent: usize) -> String {
101    if items.is_empty() {
102        return "[]".to_string();
103    }
104
105    let inner_indent = indent + INDENT_SIZE;
106    let indent_str = make_indent(inner_indent);
107
108    let mut result = "[".to_string();
109
110    for item in items.iter() {
111        result.push_str("\n");
112        result.push_str(&indent_str);
113        result.push_str(&format_expr_impl(item, max_cols, inner_indent));
114
115        // Add comma after each item (including last for multi-line)
116        result.push(',');
117    }
118
119    result.push_str("\n");
120    result.push_str(&make_indent(indent));
121    result.push(']');
122
123    result
124}
125
126/// Format a record with line breaks
127fn format_record_multiline(entries: &[RecordEntry], max_cols: usize, indent: usize) -> String {
128    if entries.is_empty() {
129        return "{}".to_string();
130    }
131
132    let inner_indent = indent + INDENT_SIZE;
133    let indent_str = make_indent(inner_indent);
134
135    let mut result = "{".to_string();
136
137    for entry in entries {
138        result.push_str("\n");
139        result.push_str(&indent_str);
140        result.push_str(&format_record_entry(entry, max_cols, inner_indent));
141
142        // Add comma after each entry (including last for multi-line)
143        result.push(',');
144    }
145
146    result.push_str("\n");
147    result.push_str(&make_indent(indent));
148    result.push('}');
149
150    result
151}
152
153/// Format a single record entry
154fn format_record_entry(entry: &RecordEntry, max_cols: usize, indent: usize) -> String {
155    match &entry.key {
156        RecordKey::Static(key) => {
157            format!("{}: {}", key, format_expr_impl(&entry.value, max_cols, indent))
158        }
159        RecordKey::Dynamic(key_expr) => {
160            format!(
161                "[{}]: {}",
162                format_expr_impl(key_expr, max_cols, indent),
163                format_expr_impl(&entry.value, max_cols, indent)
164            )
165        }
166        RecordKey::Shorthand(name) => name.clone(),
167        RecordKey::Spread(expr) => format_expr_impl(expr, max_cols, indent),
168    }
169}
170
171/// Format a lambda (handles both single-line and multi-line)
172fn format_lambda(args: &[LambdaArg], body: &SpannedExpr, max_cols: usize, indent: usize) -> String {
173    let args_str: Vec<String> = args.iter().map(lambda_arg_to_str).collect();
174
175    // For single required arguments, omit parentheses
176    let args_part = if args.len() == 1 && matches!(args[0], LambdaArg::Required(_)) {
177        format!("{} =>", args_str[0])
178    } else {
179        format!("({}) =>", args_str.join(", "))
180    };
181
182    // Try single-line first
183    let single_line_body = format_expr_impl(body, max_cols, indent);
184    let single_line = format!("{} {}", args_part, single_line_body);
185
186    if indent + single_line.len() <= max_cols {
187        return single_line;
188    }
189
190    // Otherwise, put body on next line with increased indentation
191    let body_indent = indent + INDENT_SIZE;
192    format!(
193        "{}\n{}{}",
194        args_part,
195        make_indent(body_indent),
196        format_expr_impl(body, max_cols, body_indent)
197    )
198}
199
200/// Format a conditional with line breaks
201fn format_conditional_multiline(
202    condition: &SpannedExpr,
203    then_expr: &SpannedExpr,
204    else_expr: &SpannedExpr,
205    max_cols: usize,
206    indent: usize,
207) -> String {
208    let cond_str = format_expr_impl(condition, max_cols, indent);
209
210    // Try to fit "if <condition> then" on one line
211    let if_then_prefix = format!("if {} then", cond_str);
212
213    if indent + if_then_prefix.len() <= max_cols {
214        // Put then/else clauses on new lines
215        let inner_indent = indent + INDENT_SIZE;
216        format!(
217            "{}\n{}{}\nelse\n{}{}",
218            if_then_prefix,
219            make_indent(inner_indent),
220            format_expr_impl(then_expr, max_cols, inner_indent),
221            make_indent(inner_indent),
222            format_expr_impl(else_expr, max_cols, inner_indent)
223        )
224    } else {
225        // Everything on separate lines
226        let inner_indent = indent + INDENT_SIZE;
227        format!(
228            "if\n{}{}\nthen\n{}{}\nelse\n{}{}",
229            make_indent(inner_indent),
230            format_expr_impl(condition, max_cols, inner_indent),
231            make_indent(inner_indent),
232            format_expr_impl(then_expr, max_cols, inner_indent),
233            make_indent(inner_indent),
234            format_expr_impl(else_expr, max_cols, inner_indent)
235        )
236    }
237}
238
239/// Format a function call with line breaks
240fn format_call_multiline(func: &SpannedExpr, args: &[SpannedExpr], max_cols: usize, indent: usize) -> String {
241    let func_str = match &func.node {
242        Expr::Lambda { .. } => format!("({})", format_expr_impl(func, max_cols, indent)),
243        _ => format_expr_impl(func, max_cols, indent),
244    };
245
246    if args.is_empty() {
247        return format!("{}()", func_str);
248    }
249
250    // Try formatting args on separate lines
251    let inner_indent = indent + INDENT_SIZE;
252    let indent_str = make_indent(inner_indent);
253
254    let mut result = format!("{}(", func_str);
255
256    for (i, arg) in args.iter().enumerate() {
257        result.push_str("\n");
258        result.push_str(&indent_str);
259        result.push_str(&format_expr_impl(arg, max_cols, inner_indent));
260
261        if i < args.len() - 1 {
262            result.push(',');
263        } else {
264            // Trailing comma on last arg for multi-line
265            result.push(',');
266        }
267    }
268
269    result.push_str("\n");
270    result.push_str(&make_indent(indent));
271    result.push(')');
272
273    result
274}
275
276/// Format a binary operation with line breaks
277fn format_binary_op_multiline(
278    op: &BinaryOp,
279    left: &SpannedExpr,
280    right: &SpannedExpr,
281    max_cols: usize,
282    indent: usize,
283) -> String {
284    let op_str = binary_op_str(op);
285    let left_str = format_expr_impl(left, max_cols, indent);
286
287    // Break before the operator
288    let right_indent = indent + INDENT_SIZE;
289    format!(
290        "{}\n{}{} {}",
291        left_str,
292        make_indent(right_indent),
293        op_str,
294        format_expr_impl(right, max_cols, right_indent)
295    )
296}
297
298/// Format a do block (always multi-line)
299fn format_do_block_multiline(statements: &[DoStatement], return_expr: &SpannedExpr, indent: usize) -> String {
300    let inner_indent = indent + INDENT_SIZE;
301    let indent_str = make_indent(inner_indent);
302
303    let mut result = "do {".to_string();
304
305    for stmt in statements {
306        match stmt {
307            DoStatement::Expression(e) => {
308                result.push_str("\n");
309                result.push_str(&indent_str);
310                // Do blocks use unlimited line length for now
311                result.push_str(&format_expr_impl(e, usize::MAX, inner_indent));
312            }
313            DoStatement::Comment(c) => {
314                result.push_str("\n");
315                result.push_str(&indent_str);
316                result.push_str(c);
317            }
318        }
319    }
320
321    result.push_str("\n");
322    result.push_str(&indent_str);
323    result.push_str("return ");
324    result.push_str(&format_expr_impl(return_expr, usize::MAX, inner_indent));
325    result.push_str("\n");
326    result.push_str(&make_indent(indent));
327    result.push('}');
328
329    result
330}
331
332/// Convert lambda argument to string
333fn lambda_arg_to_str(arg: &LambdaArg) -> String {
334    match arg {
335        LambdaArg::Required(name) => name.clone(),
336        LambdaArg::Optional(name) => format!("{}?", name),
337        LambdaArg::Rest(name) => format!("...{}", name),
338    }
339}
340
341/// Convert binary operator to string
342fn binary_op_str(op: &BinaryOp) -> &'static str {
343    match op {
344        BinaryOp::Add => "+",
345        BinaryOp::Subtract => "-",
346        BinaryOp::Multiply => "*",
347        BinaryOp::Divide => "/",
348        BinaryOp::Modulo => "%",
349        BinaryOp::Power => "^",
350        BinaryOp::Equal => "==",
351        BinaryOp::NotEqual => "!=",
352        BinaryOp::Less => "<",
353        BinaryOp::LessEq => "<=",
354        BinaryOp::Greater => ">",
355        BinaryOp::GreaterEq => ">=",
356        BinaryOp::DotEqual => ".==",
357        BinaryOp::DotNotEqual => ".!=",
358        BinaryOp::DotLess => ".<",
359        BinaryOp::DotLessEq => ".<=",
360        BinaryOp::DotGreater => ".>",
361        BinaryOp::DotGreaterEq => ".>=",
362        BinaryOp::And => "&&",
363        BinaryOp::NaturalAnd => "and",
364        BinaryOp::Or => "||",
365        BinaryOp::NaturalOr => "or",
366        BinaryOp::Via => "via",
367        BinaryOp::Into => "into",
368        BinaryOp::Where => "where",
369        BinaryOp::Coalesce => "??",
370    }
371}
372
373/// Generate indentation string
374fn make_indent(indent: usize) -> String {
375    " ".repeat(indent)
376}
377
378#[cfg(test)]
379mod tests {
380    use super::*;
381    use crate::parser::get_pairs;
382    use crate::expressions::pairs_to_expr;
383
384    fn parse_test_expr(source: &str) -> SpannedExpr {
385        use crate::parser::Rule;
386
387        let pairs = get_pairs(source).unwrap();
388
389        // Extract the actual expression from the statement wrapper
390        for pair in pairs {
391            if pair.as_rule() == Rule::statement {
392                if let Some(inner_pair) = pair.into_inner().next() {
393                    return pairs_to_expr(inner_pair.into_inner()).unwrap();
394                }
395            }
396        }
397
398        panic!("No statement found in parsed input");
399    }
400
401    #[test]
402    fn test_format_short_list() {
403        let expr = parse_test_expr("[1, 2, 3]");
404        let formatted = format_expr(&expr, Some(80));
405        assert_eq!(formatted, "[1, 2, 3]");
406    }
407
408    #[test]
409    fn test_format_long_list() {
410        let expr = parse_test_expr("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]");
411        let formatted = format_expr(&expr, Some(40));
412        assert!(formatted.contains("\n"));
413        assert!(formatted.contains("[\n"));
414    }
415
416    #[test]
417    fn test_format_short_record() {
418        let expr = parse_test_expr("{x: 1, y: 2}");
419        let formatted = format_expr(&expr, Some(80));
420        assert_eq!(formatted, "{x: 1, y: 2}");
421    }
422
423    #[test]
424    fn test_format_long_record() {
425        let expr = parse_test_expr("{name: \"Alice\", age: 30, email: \"alice@example.com\", address: \"123 Main St\"}");
426        let formatted = format_expr(&expr, Some(40));
427        assert!(formatted.contains("\n"));
428        assert!(formatted.contains("{\n"));
429    }
430
431    #[test]
432    fn test_format_conditional() {
433        let expr = parse_test_expr("if very_long_condition_variable > 100 then \"yes\" else \"no\"");
434        let formatted = format_expr(&expr, Some(30));
435        assert!(formatted.contains("\n"));
436    }
437
438    #[test]
439    fn test_format_binary_op() {
440        let expr = parse_test_expr("very_long_variable_name + another_very_long_variable_name");
441        let formatted = format_expr(&expr, Some(30));
442        assert!(formatted.contains("\n"));
443    }
444
445    #[test]
446    fn test_format_lambda() {
447        let expr = parse_test_expr("(x, y) => x + y");
448        let formatted = format_expr(&expr, Some(80));
449        assert_eq!(formatted, "(x, y) => x + y");
450    }
451
452    #[test]
453    fn test_format_nested_list() {
454        let expr = parse_test_expr("[[1, 2, 3], [4, 5, 6], [7, 8, 9]]");
455        let formatted = format_expr(&expr, Some(20));
456        assert!(formatted.contains("\n"));
457    }
458
459    #[test]
460    fn test_format_function_call() {
461        let expr = parse_test_expr("map([1, 2, 3], x => x * 2)");
462        let formatted = format_expr(&expr, Some(80));
463        assert_eq!(formatted, "map([1, 2, 3], x => x * 2)");
464    }
465
466    #[test]
467    fn test_format_do_block() {
468        let expr = parse_test_expr("do { x = 1\n  return x }");
469        let formatted = format_expr(&expr, Some(80));
470        assert!(formatted.contains("do {"));
471        assert!(formatted.contains("return"));
472    }
473}