blots_core/
ast_to_source.rs

1use crate::ast::{
2    BinaryOp, DoStatement, Expr, PostfixOp, RecordEntry, RecordKey, SpannedExpr, UnaryOp,
3};
4use crate::precedence::{Assoc, operator_info};
5use crate::values::{LambdaArg, SerializableValue};
6use indexmap::IndexMap;
7
8/// Reserved words that cannot be used as unquoted identifiers
9const RESERVED_WORDS: &[&str] = &[
10    "if", "then", "else", "true", "false", "null", "and", "or", "not", "do", "return", "output",
11];
12
13/// Check if a string is a valid unquoted identifier in Blots.
14/// Valid identifiers:
15/// - Must start with ASCII letter or underscore
16/// - Can contain ASCII letters, digits, or underscores
17/// - Cannot be a reserved word
18pub fn is_valid_identifier(s: &str) -> bool {
19    if s.is_empty() {
20        return false;
21    }
22
23    // Check if it's a reserved word
24    if RESERVED_WORDS.contains(&s) {
25        return false;
26    }
27
28    let mut chars = s.chars();
29
30    // First character must be ASCII letter or underscore
31    match chars.next() {
32        Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
33        _ => return false,
34    }
35
36    // Rest must be ASCII letters, digits, or underscores
37    chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
38}
39
40/// Format a record key, adding quotes if necessary
41pub fn format_record_key(key: &str) -> String {
42    if is_valid_identifier(key) {
43        key.to_string()
44    } else {
45        format!("\"{}\"", key.replace('\\', "\\\\").replace('"', "\\\""))
46    }
47}
48
49pub fn expr_to_source(spanned_expr: &SpannedExpr) -> String {
50    match &spanned_expr.node {
51        Expr::Number(n) => {
52            if n.fract() == 0.0 && n.abs() < 1e15 {
53                format!("{:.0}", n)
54            } else {
55                n.to_string()
56            }
57        }
58        Expr::String(s) => format!("\"{}\"", s.replace("\\", "\\\\").replace("\"", "\\\"")),
59        Expr::Bool(b) => b.to_string(),
60        Expr::Null => "null".to_string(),
61        Expr::Identifier(name) => name.clone(),
62        Expr::InputReference(field) => format!("#{}", field),
63        Expr::BuiltIn(built_in) => built_in.name().to_string(),
64        Expr::List(items) => {
65            let items_str: Vec<String> = items.iter().map(expr_to_source).collect();
66            format!("[{}]", items_str.join(", "))
67        }
68        Expr::Record(entries) => {
69            let entries_str: Vec<String> = entries.iter().map(record_entry_to_source).collect();
70            format!("{{{}}}", entries_str.join(", "))
71        }
72        Expr::Lambda { args, body } => {
73            let args_str: Vec<String> = args.iter().map(lambda_arg_to_source).collect();
74            format!("({}) => {}", args_str.join(", "), expr_to_source(body))
75        }
76        Expr::Conditional {
77            condition,
78            then_expr,
79            else_expr,
80        } => format!(
81            "if {} then {} else {}",
82            expr_to_source(condition),
83            expr_to_source(then_expr),
84            expr_to_source(else_expr)
85        ),
86        Expr::DoBlock {
87            statements,
88            return_expr,
89        } => {
90            let mut result = "do {".to_string();
91            for stmt in statements {
92                match stmt {
93                    DoStatement::Expression(e) => {
94                        result.push_str(&format!("\n  {}", expr_to_source(e)));
95                    }
96                    DoStatement::Comment(c) => {
97                        result.push_str(&format!("\n  {}", c));
98                    }
99                }
100            }
101            result.push_str(&format!("\n  return {}\n}}", expr_to_source(return_expr)));
102            result
103        }
104        Expr::Assignment { ident, value } => format!("{} = {}", ident, expr_to_source(value)),
105        Expr::Output { expr } => format!("output {}", expr_to_source(expr)),
106        Expr::Call { func, args } => {
107            let args_str: Vec<String> = args.iter().map(expr_to_source).collect();
108            let func_str = match &func.node {
109                // Wrap lambdas in parentheses when used in call position
110                Expr::Lambda { .. } => format!("({})", expr_to_source(func)),
111                _ => expr_to_source(func),
112            };
113            format!("{}({})", func_str, args_str.join(", "))
114        }
115        Expr::Access { expr, index } => {
116            format!("{}[{}]", expr_to_source(expr), expr_to_source(index))
117        }
118        Expr::DotAccess { expr, field } => format!("{}.{}", expr_to_source(expr), field),
119        Expr::BinaryOp { op, left, right } => {
120            let op_str = binary_op_to_source(op);
121            let left_str = if needs_parens_in_binop(op, left, true) {
122                format!("({})", expr_to_source(left))
123            } else {
124                expr_to_source(left)
125            };
126            let right_str = if needs_parens_in_binop(op, right, false) {
127                format!("({})", expr_to_source(right))
128            } else {
129                expr_to_source(right)
130            };
131            format!("{} {} {}", left_str, op_str, right_str)
132        }
133        Expr::UnaryOp { op, expr } => {
134            let op_str = unary_op_to_source(op);
135            format!("{}{}", op_str, expr_to_source(expr))
136        }
137        Expr::PostfixOp { op, expr } => {
138            let op_str = postfix_op_to_source(op);
139            format!("{}{}", expr_to_source(expr), op_str)
140        }
141        Expr::Spread(expr) => format!("...{}", expr_to_source(expr)),
142    }
143}
144
145fn lambda_arg_to_source(arg: &LambdaArg) -> String {
146    match arg {
147        LambdaArg::Required(name) => name.clone(),
148        LambdaArg::Optional(name) => format!("{}?", name),
149        LambdaArg::Rest(name) => format!("...{}", name),
150    }
151}
152
153fn record_entry_to_source(entry: &RecordEntry) -> String {
154    match &entry.key {
155        RecordKey::Static(key) => format!(
156            "{}: {}",
157            format_record_key(key),
158            expr_to_source(&entry.value)
159        ),
160        RecordKey::Dynamic(key_expr) => {
161            format!(
162                "[{}]: {}",
163                expr_to_source(key_expr),
164                expr_to_source(&entry.value)
165            )
166        }
167        RecordKey::Shorthand(name) => name.clone(),
168        RecordKey::Spread(expr) => expr_to_source(expr),
169    }
170}
171
172fn binary_op_to_source(op: &BinaryOp) -> &'static str {
173    match op {
174        BinaryOp::Add => "+",
175        BinaryOp::Subtract => "-",
176        BinaryOp::Multiply => "*",
177        BinaryOp::Divide => "/",
178        BinaryOp::Modulo => "%",
179        BinaryOp::Power => "^",
180        BinaryOp::Equal => "==",
181        BinaryOp::NotEqual => "!=",
182        BinaryOp::Less => "<",
183        BinaryOp::LessEq => "<=",
184        BinaryOp::Greater => ">",
185        BinaryOp::GreaterEq => ">=",
186        BinaryOp::DotEqual => ".==",
187        BinaryOp::DotNotEqual => ".!=",
188        BinaryOp::DotLess => ".<",
189        BinaryOp::DotLessEq => ".<=",
190        BinaryOp::DotGreater => ".>",
191        BinaryOp::DotGreaterEq => ".>=",
192        BinaryOp::And => "&&",
193        BinaryOp::NaturalAnd => "and",
194        BinaryOp::Or => "||",
195        BinaryOp::NaturalOr => "or",
196        BinaryOp::Via => "via",
197        BinaryOp::Into => "into",
198        BinaryOp::Where => "where",
199        BinaryOp::Coalesce => "??",
200    }
201}
202
203/// Check if a child expression needs parentheses when used in a binary operation
204pub fn needs_parens_in_binop(
205    parent_op: &BinaryOp,
206    child_expr: &SpannedExpr,
207    is_left: bool,
208) -> bool {
209    match &child_expr.node {
210        Expr::BinaryOp { op: child_op, .. } => {
211            let (parent_prec, parent_assoc) = operator_info(parent_op);
212            let (child_prec, _child_assoc) = operator_info(child_op);
213
214            // Need parentheses if child has lower precedence
215            if child_prec < parent_prec {
216                return true;
217            }
218
219            // For same precedence, need parentheses on right side for:
220            // - Right-associative operators (e.g., power)
221            // - Non-associative operators (subtraction, division)
222            if child_prec == parent_prec && !is_left {
223                match parent_assoc {
224                    Assoc::Right => return true,
225                    Assoc::Left => {
226                        // For left-associative operators, right side needs parens for non-associative ones
227                        if matches!(
228                            parent_op,
229                            BinaryOp::Subtract | BinaryOp::Divide | BinaryOp::Modulo
230                        ) {
231                            return true;
232                        }
233                    }
234                }
235            }
236
237            false
238        }
239        _ => false,
240    }
241}
242
243fn unary_op_to_source(op: &UnaryOp) -> &'static str {
244    match op {
245        UnaryOp::Negate => "-",
246        UnaryOp::Not => "!",
247        UnaryOp::Invert => "~",
248    }
249}
250
251fn postfix_op_to_source(op: &PostfixOp) -> &'static str {
252    match op {
253        PostfixOp::Factorial => "!",
254    }
255}
256
257/// Convert an expression to source code with inlined scope values
258pub fn expr_to_source_with_scope(
259    spanned_expr: &SpannedExpr,
260    scope: &IndexMap<String, SerializableValue>,
261) -> String {
262    match &spanned_expr.node {
263        Expr::Identifier(name) => {
264            // If the identifier is in the scope, inline its value
265            if let Some(value) = scope.get(name) {
266                serializable_value_to_source(value)
267            } else {
268                name.clone()
269            }
270        }
271        Expr::InputReference(field) => format!("#{}", field),
272        // For all other expression types, recursively process with scope
273        Expr::Number(n) => {
274            if n.fract() == 0.0 && n.abs() < 1e15 {
275                format!("{:.0}", n)
276            } else {
277                n.to_string()
278            }
279        }
280        Expr::String(s) => format!("\"{}\"", s.replace("\\", "\\\\").replace("\"", "\\\"")),
281        Expr::Bool(b) => b.to_string(),
282        Expr::Null => "null".to_string(),
283        Expr::BuiltIn(built_in) => built_in.name().to_string(),
284        Expr::List(items) => {
285            let items_str: Vec<String> = items
286                .iter()
287                .map(|e| expr_to_source_with_scope(e, scope))
288                .collect();
289            format!("[{}]", items_str.join(", "))
290        }
291        Expr::Record(entries) => {
292            let entries_str: Vec<String> = entries
293                .iter()
294                .map(|e| record_entry_to_source_with_scope(e, scope))
295                .collect();
296            format!("{{{}}}", entries_str.join(", "))
297        }
298        Expr::Lambda { args, body } => {
299            let args_str: Vec<String> = args.iter().map(lambda_arg_to_source).collect();
300            // Lambda bodies should not inline their own parameters
301            // Filter out parameter names from the scope before processing the body
302            let mut filtered_scope = scope.clone();
303            for arg in args {
304                filtered_scope.shift_remove(arg.get_name());
305            }
306            format!(
307                "({}) => {}",
308                args_str.join(", "),
309                expr_to_source_with_scope(body, &filtered_scope)
310            )
311        }
312        Expr::Conditional {
313            condition,
314            then_expr,
315            else_expr,
316        } => format!(
317            "if {} then {} else {}",
318            expr_to_source_with_scope(condition, scope),
319            expr_to_source_with_scope(then_expr, scope),
320            expr_to_source_with_scope(else_expr, scope)
321        ),
322        Expr::DoBlock {
323            statements,
324            return_expr,
325        } => {
326            let mut result = "do {".to_string();
327            for stmt in statements {
328                match stmt {
329                    DoStatement::Expression(e) => {
330                        result.push_str(&format!("\n  {}", expr_to_source_with_scope(e, scope)));
331                    }
332                    DoStatement::Comment(c) => {
333                        result.push_str(&format!("\n  // {}", c));
334                    }
335                }
336            }
337            result.push_str(&format!(
338                "\n  return {}",
339                expr_to_source_with_scope(return_expr, scope)
340            ));
341            result.push_str("\n}");
342            result
343        }
344        Expr::BinaryOp { op, left, right } => {
345            let op_str = binary_op_to_source(op);
346            let left_str = if needs_parens_in_binop(op, left, true) {
347                format!("({})", expr_to_source_with_scope(left, scope))
348            } else {
349                expr_to_source_with_scope(left, scope)
350            };
351            let right_str = if needs_parens_in_binop(op, right, false) {
352                format!("({})", expr_to_source_with_scope(right, scope))
353            } else {
354                expr_to_source_with_scope(right, scope)
355            };
356            format!("{} {} {}", left_str, op_str, right_str)
357        }
358        Expr::UnaryOp { op, expr } => {
359            let op_str = match op {
360                UnaryOp::Negate => "-",
361                UnaryOp::Not => "!",
362                UnaryOp::Invert => "~",
363            };
364            format!("{}{}", op_str, expr_to_source_with_scope(expr, scope))
365        }
366        Expr::PostfixOp { op, expr } => {
367            let op_str = postfix_op_to_source(op);
368            format!("{}{}", expr_to_source_with_scope(expr, scope), op_str)
369        }
370        Expr::Spread(expr) => format!("...{}", expr_to_source_with_scope(expr, scope)),
371        Expr::Assignment { ident, value } => {
372            format!("{} = {}", ident, expr_to_source_with_scope(value, scope))
373        }
374        Expr::Output { expr } => {
375            format!("output {}", expr_to_source_with_scope(expr, scope))
376        }
377        Expr::Call { func, args } => {
378            let args_str: Vec<String> = args
379                .iter()
380                .map(|e| expr_to_source_with_scope(e, scope))
381                .collect();
382            let func_str = match &func.node {
383                // Wrap lambdas in parentheses when used in call position
384                Expr::Lambda { .. } => {
385                    format!("({})", expr_to_source_with_scope(func, scope))
386                }
387                _ => expr_to_source_with_scope(func, scope),
388            };
389            format!("{}({})", func_str, args_str.join(", "))
390        }
391        Expr::Access { expr, index } => {
392            format!(
393                "{}[{}]",
394                expr_to_source_with_scope(expr, scope),
395                expr_to_source_with_scope(index, scope)
396            )
397        }
398        Expr::DotAccess { expr, field } => {
399            format!("{}.{}", expr_to_source_with_scope(expr, scope), field)
400        }
401    }
402}
403
404fn record_entry_to_source_with_scope(
405    entry: &RecordEntry,
406    scope: &IndexMap<String, SerializableValue>,
407) -> String {
408    match &entry.key {
409        RecordKey::Static(key) => {
410            format!(
411                "{}: {}",
412                format_record_key(key),
413                expr_to_source_with_scope(&entry.value, scope)
414            )
415        }
416        RecordKey::Dynamic(key_expr) => {
417            format!(
418                "[{}]: {}",
419                expr_to_source_with_scope(key_expr, scope),
420                expr_to_source_with_scope(&entry.value, scope)
421            )
422        }
423        RecordKey::Shorthand(name) => {
424            // For shorthand, check if the value is in scope
425            // Note: shorthand keys are always valid identifiers (they come from variable names)
426            if let Some(value) = scope.get(name) {
427                format!("{}: {}", name, serializable_value_to_source(value))
428            } else {
429                name.clone()
430            }
431        }
432        RecordKey::Spread(expr) => expr_to_source_with_scope(expr, scope),
433    }
434}
435
436/// Convert a SerializableValue to its source representation
437fn serializable_value_to_source(value: &SerializableValue) -> String {
438    match value {
439        SerializableValue::Number(n) => {
440            if n.fract() == 0.0 && n.abs() < 1e15 {
441                format!("{:.0}", n)
442            } else {
443                n.to_string()
444            }
445        }
446        SerializableValue::Bool(b) => b.to_string(),
447        SerializableValue::Null => "null".to_string(),
448        SerializableValue::String(s) => {
449            format!("\"{}\"", s.replace("\\", "\\\\").replace("\"", "\\\""))
450        }
451        SerializableValue::List(items) => {
452            let items_str: Vec<String> = items.iter().map(serializable_value_to_source).collect();
453            format!("[{}]", items_str.join(", "))
454        }
455        SerializableValue::Record(fields) => {
456            let entries_str: Vec<String> = fields
457                .iter()
458                .map(|(k, v)| {
459                    format!(
460                        "{}: {}",
461                        format_record_key(k),
462                        serializable_value_to_source(v)
463                    )
464                })
465                .collect();
466            format!("{{{}}}", entries_str.join(", "))
467        }
468        SerializableValue::Lambda(lambda_def) => {
469            let args_str: Vec<String> = lambda_def.args.iter().map(lambda_arg_to_source).collect();
470            // Wrap in parentheses so it can be used in call expressions
471            format!("(({}) => {})", args_str.join(", "), lambda_def.body)
472        }
473        SerializableValue::BuiltIn(name) => name.clone(),
474    }
475}