Skip to main content

kaish_kernel/ast/
sexpr.rs

1//! S-expression formatter for kaish AST.
2//!
3//! Converts AST nodes to the S-expression format used in test snapshots.
4//! S-expressions provide a stable, readable format that's easier to diff
5//! than Debug output.
6
7use super::*;
8
9/// Format a Program as an S-expression.
10/// For single-statement programs, formats just the statement.
11/// For multi-statement programs, formats as a sequence.
12pub fn format_program(program: &Program) -> String {
13    let stmts: Vec<_> = program
14        .statements
15        .iter()
16        .filter(|s| !matches!(s, Stmt::Empty))
17        .collect();
18
19    match stmts.len() {
20        0 => "(program)".to_string(),
21        1 => format_stmt(stmts[0]),
22        _ => {
23            let parts: Vec<String> = stmts.iter().map(|s| format_stmt(s)).collect();
24            format!("(program {})", parts.join(" "))
25        }
26    }
27}
28
29/// Format a statement as an S-expression.
30pub fn format_stmt(stmt: &Stmt) -> String {
31    match stmt {
32        Stmt::Assignment(a) => format_assignment(a),
33        Stmt::Command(cmd) => format_command(cmd),
34        Stmt::Pipeline(p) => format_pipeline(p),
35        Stmt::If(if_stmt) => format_if(if_stmt),
36        Stmt::For(for_loop) => format_for(for_loop),
37        Stmt::While(while_loop) => format_while(while_loop),
38        Stmt::Case(case_stmt) => format_case(case_stmt),
39        Stmt::Break(n) => match n {
40            Some(level) => format!("(break {})", level),
41            None => "(break)".to_string(),
42        },
43        Stmt::Continue(n) => match n {
44            Some(level) => format!("(continue {})", level),
45            None => "(continue)".to_string(),
46        },
47        Stmt::Return(expr) => match expr {
48            Some(e) => format!("(return {})", format_expr(e)),
49            None => "(return)".to_string(),
50        },
51        Stmt::Exit(expr) => match expr {
52            Some(e) => format!("(exit {})", format_expr(e)),
53            None => "(exit)".to_string(),
54        },
55        Stmt::ToolDef(tool) => format_tooldef(tool),
56        Stmt::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
57        Stmt::AndChain { left, right } => {
58            format!("(and-chain {} {})", format_stmt(left), format_stmt(right))
59        }
60        Stmt::OrChain { left, right } => {
61            format!("(or-chain {} {})", format_stmt(left), format_stmt(right))
62        }
63        Stmt::Empty => "(empty)".to_string(),
64    }
65}
66
67/// Format an assignment as an S-expression.
68fn format_assignment(a: &Assignment) -> String {
69    let value = format_expr(&a.value);
70    format!("(assign {} {} local={})", a.name, value, a.local)
71}
72
73/// Format a command as an S-expression.
74pub fn format_command(cmd: &Command) -> String {
75    let mut parts = vec![format!("(cmd {}", cmd.name)];
76
77    for arg in &cmd.args {
78        parts.push(format_arg(arg));
79    }
80
81    for redir in &cmd.redirects {
82        parts.push(format_redirect(redir));
83    }
84
85    format!("{})", parts.join(" "))
86}
87
88/// Format an argument as an S-expression.
89fn format_arg(arg: &Arg) -> String {
90    match arg {
91        Arg::Positional(expr) => format!("(pos {})", format_expr(expr)),
92        Arg::Named { key, value } => format!("(named {} {})", key, format_expr(value)),
93        Arg::WordAssign { key, value } => format!("(wordassign {} {})", key, format_expr(value)),
94        Arg::ShortFlag(f) => format!("(shortflag {})", f),
95        Arg::LongFlag(f) => format!("(longflag {})", f),
96        Arg::DoubleDash => "(doubledash)".to_string(),
97    }
98}
99
100/// Format a redirect as an S-expression.
101fn format_redirect(redir: &Redirect) -> String {
102    let kind = match redir.kind {
103        RedirectKind::StdoutOverwrite => ">",
104        RedirectKind::StdoutAppend => ">>",
105        RedirectKind::Stdin => "<",
106        RedirectKind::HereDoc => "<<",
107        RedirectKind::HereString => "<<<",
108        RedirectKind::Stderr => "2>",
109        RedirectKind::Both => "&>",
110        RedirectKind::MergeStderr => "2>&1",
111        RedirectKind::MergeStdout => "1>&2",
112    };
113    format!("(redir {} {})", kind, format_expr(&redir.target))
114}
115
116/// Format a pipeline as an S-expression.
117pub fn format_pipeline(p: &Pipeline) -> String {
118    let cmds: Vec<String> = p.commands.iter().map(format_command).collect();
119
120    if p.background {
121        if cmds.len() == 1 {
122            format!("(background {})", cmds[0])
123        } else {
124            format!("(background (pipeline {}))", cmds.join(" "))
125        }
126    } else {
127        format!("(pipeline {})", cmds.join(" "))
128    }
129}
130
131/// Format an if statement as an S-expression.
132fn format_if(if_stmt: &IfStmt) -> String {
133    let cond = format_expr(&if_stmt.condition);
134    let then_stmts: Vec<String> = if_stmt
135        .then_branch
136        .iter()
137        .filter(|s| !matches!(s, Stmt::Empty))
138        .map(format_stmt)
139        .collect();
140    let then_part = format!("(then {})", then_stmts.join(" "));
141
142    match &if_stmt.else_branch {
143        Some(else_stmts) => {
144            let else_inner: Vec<String> = else_stmts
145                .iter()
146                .filter(|s| !matches!(s, Stmt::Empty))
147                .map(format_stmt)
148                .collect();
149            if else_inner.is_empty() {
150                format!("(if {} {} (else))", cond, then_part)
151            } else {
152                format!("(if {} {} (else {}))", cond, then_part, else_inner.join(" "))
153            }
154        }
155        None => format!("(if {} {} (else))", cond, then_part),
156    }
157}
158
159/// Format a for loop as an S-expression.
160fn format_for(for_loop: &ForLoop) -> String {
161    let items: Vec<String> = for_loop.items.iter().map(format_expr).collect();
162    let body_stmts: Vec<String> = for_loop
163        .body
164        .iter()
165        .filter(|s| !matches!(s, Stmt::Empty))
166        .map(format_stmt)
167        .collect();
168    format!(
169        "(for {} (in {}) (do {}))",
170        for_loop.variable,
171        items.join(" "),
172        body_stmts.join(" ")
173    )
174}
175
176/// Format a while loop as an S-expression.
177fn format_while(while_loop: &WhileLoop) -> String {
178    let cond = format_expr(&while_loop.condition);
179    let body_stmts: Vec<String> = while_loop
180        .body
181        .iter()
182        .filter(|s| !matches!(s, Stmt::Empty))
183        .map(format_stmt)
184        .collect();
185    format!("(while {} (do {}))", cond, body_stmts.join(" "))
186}
187
188/// Format a case statement as an S-expression.
189fn format_case(case_stmt: &CaseStmt) -> String {
190    let expr = format_expr(&case_stmt.expr);
191    let branches: Vec<String> = case_stmt
192        .branches
193        .iter()
194        .map(format_case_branch)
195        .collect();
196    format!("(case {} ({}))", expr, branches.join(" "))
197}
198
199/// Format a case branch as an S-expression.
200fn format_case_branch(branch: &CaseBranch) -> String {
201    let patterns = branch.patterns.join("|");
202    let body_stmts: Vec<String> = branch
203        .body
204        .iter()
205        .filter(|s| !matches!(s, Stmt::Empty))
206        .map(format_stmt)
207        .collect();
208    format!("(branch \"{}\" ({}))", patterns, body_stmts.join(" "))
209}
210
211/// Format a tool definition as an S-expression.
212fn format_tooldef(tool: &ToolDef) -> String {
213    let params: Vec<String> = tool.params.iter().map(format_param).collect();
214    let body_stmts: Vec<String> = tool
215        .body
216        .iter()
217        .filter(|s| !matches!(s, Stmt::Empty))
218        .map(format_stmt)
219        .collect();
220    format!(
221        "(tooldef {} ({}) ({}))",
222        tool.name,
223        params.join(" "),
224        body_stmts.join(" ")
225    )
226}
227
228/// Format a parameter definition as an S-expression.
229fn format_param(param: &ParamDef) -> String {
230    let type_str = param
231        .param_type
232        .as_ref()
233        .map(|t| match t {
234            ParamType::String => "string",
235            ParamType::Int => "int",
236            ParamType::Float => "float",
237            ParamType::Bool => "bool",
238        })
239        .unwrap_or("any");
240
241    match &param.default {
242        Some(default) => format!("(param {} {} {})", param.name, type_str, format_expr(default)),
243        None => format!("(param {} {})", param.name, type_str),
244    }
245}
246
247/// Format an expression as an S-expression.
248pub fn format_expr(expr: &Expr) -> String {
249    match expr {
250        Expr::Literal(value) => format_value(value),
251        Expr::VarRef(path) => format!("(varref {})", format_varpath(path)),
252        Expr::Interpolated(parts) => {
253            let parts_str: Vec<String> = parts
254                .iter()
255                .map(format_string_part)
256                .collect();
257            format!("(interpolated {})", parts_str.join(" "))
258        }
259        Expr::HereDocBody { parts, strip_tabs } => {
260            let parts_str: Vec<String> = parts
261                .iter()
262                .map(|sp| format_string_part(&sp.part))
263                .collect();
264            format!(
265                "(heredoc-body strip-tabs={} {})",
266                strip_tabs,
267                parts_str.join(" ")
268            )
269        }
270        Expr::BinaryOp { left, op, right } => {
271            let op_str = match op {
272                BinaryOp::And => "and",
273                BinaryOp::Or => "or",
274            };
275            format!("({} {} {})", op_str, format_expr(left), format_expr(right))
276        }
277        Expr::CommandSubst(pipeline) => {
278            format!("(cmdsubst {})", format_pipeline(pipeline))
279        }
280        Expr::Test(test_expr) => format!("(test {})", format_test_expr(test_expr)),
281        Expr::Positional(n) => format!("(positional {})", n),
282        Expr::AllArgs => "(all-args)".to_string(),
283        Expr::ArgCount => "(arg-count)".to_string(),
284        Expr::VarLength(name) => format!("(var-length {})", name),
285        Expr::VarWithDefault { name, default } => {
286            let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
287            format!("(var-default {} ({}))", name, default_parts.join(" "))
288        }
289        Expr::Arithmetic(expr_str) => format!("(arithmetic \"{}\")", expr_str),
290        Expr::Command(cmd) => format_command(cmd),
291        Expr::LastExitCode => "(last-exit-code)".to_string(),
292        Expr::CurrentPid => "(current-pid)".to_string(),
293        Expr::GlobPattern(s) => format!("(glob \"{}\")", s),
294    }
295}
296
297/// Format a test expression as an S-expression.
298pub fn format_test_expr(test: &TestExpr) -> String {
299    match test {
300        TestExpr::FileTest { op, path } => {
301            let op_str = match op {
302                FileTestOp::Exists => "-e",
303                FileTestOp::IsFile => "-f",
304                FileTestOp::IsDir => "-d",
305                FileTestOp::Readable => "-r",
306                FileTestOp::Writable => "-w",
307                FileTestOp::Executable => "-x",
308            };
309            format!("(file {} {})", op_str, format_expr(path))
310        }
311        TestExpr::StringTest { op, value } => {
312            let op_str = match op {
313                StringTestOp::IsEmpty => "-z",
314                StringTestOp::IsNonEmpty => "-n",
315            };
316            format!("(string {} {})", op_str, format_expr(value))
317        }
318        TestExpr::Comparison { left, op, right } => {
319            let op_str = match op {
320                TestCmpOp::Eq => "==",
321                TestCmpOp::NotEq => "!=",
322                TestCmpOp::Match => "=~",
323                TestCmpOp::NotMatch => "!~",
324                TestCmpOp::Gt => ">",
325                TestCmpOp::Lt => "<",
326                TestCmpOp::GtEq => ">=",
327                TestCmpOp::LtEq => "<=",
328                TestCmpOp::NumEq => "-eq",
329                TestCmpOp::NumNotEq => "-ne",
330                TestCmpOp::NumGt => "-gt",
331                TestCmpOp::NumLt => "-lt",
332                TestCmpOp::NumGtEq => "-ge",
333                TestCmpOp::NumLtEq => "-le",
334            };
335            format!(
336                "(cmp {} {} {})",
337                op_str,
338                format_expr(left),
339                format_expr(right)
340            )
341        }
342        TestExpr::And { left, right } => {
343            format!("(and {} {})", format_test_expr(left), format_test_expr(right))
344        }
345        TestExpr::Or { left, right } => {
346            format!("(or {} {})", format_test_expr(left), format_test_expr(right))
347        }
348        TestExpr::Not { expr } => {
349            format!("(not {})", format_test_expr(expr))
350        }
351    }
352}
353
354/// Format a StringPart as an S-expression.
355fn format_string_part(part: &StringPart) -> String {
356    match part {
357        StringPart::Literal(s) => format!("\"{}\"", escape_for_display(s)),
358        StringPart::Var(path) => format!("(varref {})", format_varpath(path)),
359        StringPart::VarWithDefault { name, default } => {
360            let default_parts: Vec<String> = default.iter().map(format_string_part).collect();
361            format!("(vardefault {} ({}))", name, default_parts.join(" "))
362        }
363        StringPart::VarLength(name) => format!("(varlength {})", name),
364        StringPart::Positional(n) => format!("(positional {})", n),
365        StringPart::AllArgs => "(allargs)".to_string(),
366        StringPart::ArgCount => "(argcount)".to_string(),
367        StringPart::Arithmetic(expr) => format!("(arith \"{}\")", expr),
368        StringPart::CommandSubst(pipeline) => format!("(cmdsubst {})", format_pipeline(pipeline)),
369        StringPart::LastExitCode => "(last-exit-code)".to_string(),
370        StringPart::CurrentPid => "(current-pid)".to_string(),
371    }
372}
373
374/// Escape control characters for display in test output.
375fn escape_for_display(s: &str) -> String {
376    s.replace('\n', "\\n")
377        .replace('\t', "\\t")
378        .replace('\r', "\\r")
379}
380
381/// Format a value as an S-expression.
382pub fn format_value(value: &Value) -> String {
383    match value {
384        Value::Null => "(null)".to_string(),
385        Value::Bool(b) => format!("(bool {})", b),
386        Value::Int(n) => format!("(int {})", n),
387        Value::Float(f) => format!("(float {})", f),
388        Value::String(s) => format!("(string \"{}\")", escape_for_display(s)),
389        Value::Json(json) => format!("(json {})", json),
390        Value::Blob(blob) => format!("(blob id={} size={} type={})", blob.id, blob.size, blob.content_type),
391    }
392}
393
394/// Format a variable path as an S-expression.
395pub fn format_varpath(path: &VarPath) -> String {
396    path.segments
397        .iter()
398        .map(|seg| match seg {
399            VarSegment::Field(name) => name.clone(),
400        })
401        .collect::<Vec<_>>()
402        .join(".")
403}
404
405#[cfg(test)]
406mod tests {
407    use super::*;
408
409    #[test]
410    fn format_simple_int() {
411        assert_eq!(format_value(&Value::Int(42)), "(int 42)");
412    }
413
414    #[test]
415    fn format_simple_string() {
416        assert_eq!(format_value(&Value::String("hello".to_string())), "(string \"hello\")");
417    }
418
419    #[test]
420    fn format_varpath_simple() {
421        let path = VarPath::simple("X");
422        assert_eq!(format_varpath(&path), "X");
423    }
424
425    #[test]
426    fn format_varpath_nested() {
427        let path = VarPath {
428            segments: vec![
429                VarSegment::Field("VAR".to_string()),
430                VarSegment::Field("field".to_string()),
431            ],
432        };
433        assert_eq!(format_varpath(&path), "VAR.field");
434    }
435}