Skip to main content

kaish_kernel/ast/
types.rs

1//! AST type definitions.
2
3use std::fmt;
4
5/// A complete kaish program is a sequence of statements.
6#[derive(Debug, Clone, PartialEq)]
7pub struct Program {
8    pub statements: Vec<Stmt>,
9}
10
11/// A single statement in kaish.
12#[derive(Debug, Clone, PartialEq)]
13pub enum Stmt {
14    /// Variable assignment: `NAME=value` or `local NAME = value`
15    Assignment(Assignment),
16    /// Simple command: `tool arg1 arg2`
17    Command(Command),
18    /// Pipeline: `a | b | c`
19    Pipeline(Pipeline),
20    /// Conditional: `if cond; then ...; fi`
21    If(IfStmt),
22    /// Loop: `for X in items; do ...; done`
23    For(ForLoop),
24    /// While loop: `while cond; do ...; done`
25    While(WhileLoop),
26    /// Case statement: `case expr in pattern) ... ;; esac`
27    Case(CaseStmt),
28    /// Break out of loop: `break` or `break N`
29    Break(Option<usize>),
30    /// Continue to next iteration: `continue` or `continue N`
31    Continue(Option<usize>),
32    /// Return from tool: `return` or `return expr`
33    Return(Option<Box<Expr>>),
34    /// Exit the script: `exit` or `exit code`
35    Exit(Option<Box<Expr>>),
36    /// Tool definition: `tool name(params) { body }`
37    ToolDef(ToolDef),
38    /// Test expression: `[[ -f path ]]` or `[[ $X == "value" ]]`
39    Test(TestExpr),
40    /// Statement chain with `&&`: run right only if left succeeds
41    AndChain { left: Box<Stmt>, right: Box<Stmt> },
42    /// Statement chain with `||`: run right only if left fails
43    OrChain { left: Box<Stmt>, right: Box<Stmt> },
44    /// Empty statement (newline or semicolon only)
45    Empty,
46}
47
48impl Stmt {
49    /// Human-readable variant name for tracing spans.
50    pub fn kind_name(&self) -> &'static str {
51        match self {
52            Stmt::Assignment(_) => "assignment",
53            Stmt::Command(_) => "command",
54            Stmt::Pipeline(_) => "pipeline",
55            Stmt::If(_) => "if",
56            Stmt::For(_) => "for",
57            Stmt::While(_) => "while",
58            Stmt::Case(_) => "case",
59            Stmt::Break(_) => "break",
60            Stmt::Continue(_) => "continue",
61            Stmt::Return(_) => "return",
62            Stmt::Exit(_) => "exit",
63            Stmt::ToolDef(_) => "tooldef",
64            Stmt::Test(_) => "test",
65            Stmt::AndChain { .. } => "and_chain",
66            Stmt::OrChain { .. } => "or_chain",
67            Stmt::Empty => "empty",
68        }
69    }
70}
71
72/// Variable assignment: `NAME=value` (bash-style) or `local NAME = value` (scoped)
73#[derive(Debug, Clone, PartialEq)]
74pub struct Assignment {
75    pub name: String,
76    pub value: Expr,
77    /// True if declared with `local` keyword (explicit local scope)
78    pub local: bool,
79}
80
81/// A command invocation with arguments and redirections.
82#[derive(Debug, Clone, PartialEq)]
83pub struct Command {
84    pub name: String,
85    pub args: Vec<Arg>,
86    pub redirects: Vec<Redirect>,
87}
88
89/// A pipeline of commands connected by pipes.
90#[derive(Debug, Clone, PartialEq)]
91pub struct Pipeline {
92    pub commands: Vec<Command>,
93    pub background: bool,
94}
95
96/// Conditional statement.
97#[derive(Debug, Clone, PartialEq)]
98pub struct IfStmt {
99    pub condition: Box<Expr>,
100    pub then_branch: Vec<Stmt>,
101    pub else_branch: Option<Vec<Stmt>>,
102}
103
104/// For loop over items.
105#[derive(Debug, Clone, PartialEq)]
106pub struct ForLoop {
107    pub variable: String,
108    /// Items to iterate over. Each is evaluated, then word-split for iteration.
109    pub items: Vec<Expr>,
110    pub body: Vec<Stmt>,
111}
112
113/// While loop with condition.
114#[derive(Debug, Clone, PartialEq)]
115pub struct WhileLoop {
116    pub condition: Box<Expr>,
117    pub body: Vec<Stmt>,
118}
119
120/// Case statement for pattern matching.
121///
122/// ```kaish
123/// case $VAR in
124///     pattern1) commands ;;
125///     pattern2|pattern3) commands ;;
126///     *) default ;;
127/// esac
128/// ```
129#[derive(Debug, Clone, PartialEq)]
130pub struct CaseStmt {
131    /// The expression to match against
132    pub expr: Expr,
133    /// The pattern branches
134    pub branches: Vec<CaseBranch>,
135}
136
137/// A single branch in a case statement.
138#[derive(Debug, Clone, PartialEq)]
139pub struct CaseBranch {
140    /// Glob patterns to match (separated by `|`)
141    pub patterns: Vec<String>,
142    /// Commands to execute if matched
143    pub body: Vec<Stmt>,
144}
145
146/// User-defined tool.
147#[derive(Debug, Clone, PartialEq)]
148pub struct ToolDef {
149    pub name: String,
150    pub params: Vec<ParamDef>,
151    pub body: Vec<Stmt>,
152}
153
154/// Parameter definition for a tool.
155#[derive(Debug, Clone, PartialEq)]
156pub struct ParamDef {
157    pub name: String,
158    pub param_type: Option<ParamType>,
159    pub default: Option<Expr>,
160}
161
162/// Parameter type annotation.
163#[derive(Debug, Clone, PartialEq)]
164pub enum ParamType {
165    String,
166    Int,
167    Float,
168    Bool,
169}
170
171/// A command argument (positional or named).
172#[derive(Debug, Clone, PartialEq)]
173pub enum Arg {
174    /// Positional argument: `value`
175    Positional(Expr),
176    /// Named argument: `key=value`
177    Named { key: String, value: Expr },
178    /// Short flag: `-l`, `-v` (boolean flag)
179    ShortFlag(String),
180    /// Long flag: `--force`, `--verbose` (boolean flag)
181    LongFlag(String),
182    /// Double-dash marker: `--` - signals end of flags
183    DoubleDash,
184}
185
186/// I/O redirection.
187#[derive(Debug, Clone, PartialEq)]
188pub struct Redirect {
189    pub kind: RedirectKind,
190    pub target: Expr,
191}
192
193/// Type of redirection.
194#[derive(Debug, Clone, PartialEq)]
195pub enum RedirectKind {
196    /// `>` stdout to file (overwrite)
197    StdoutOverwrite,
198    /// `>>` stdout to file (append)
199    StdoutAppend,
200    /// `<` stdin from file
201    Stdin,
202    /// `<<EOF ... EOF` stdin from here-doc
203    HereDoc,
204    /// `<<< word` stdin from here-string (bash-style)
205    HereString,
206    /// `2>` stderr to file
207    Stderr,
208    /// `&>` both stdout and stderr to file
209    Both,
210    /// `2>&1` merge stderr into stdout
211    MergeStderr,
212    /// `1>&2` or `>&2` merge stdout into stderr
213    MergeStdout,
214}
215
216/// A `StringPart` together with its byte offset in the original source.
217///
218/// Used by [`Expr::HereDocBody`] so the validator and interpreter can attribute
219/// diagnostics to a precise location inside an interpolated heredoc body.
220/// Double-quoted strings continue to use the spanless [`Expr::Interpolated`];
221/// universal spanning is a separate, larger refactor (see plan
222/// `make-heredocs-precious-puzzle`).
223#[derive(Debug, Clone, PartialEq)]
224pub struct SpannedPart {
225    /// The part itself.
226    pub part: StringPart,
227    /// Byte offset of this part in the original source string.
228    pub offset: usize,
229    /// Byte length of the part's source representation.
230    pub len: usize,
231}
232
233/// An expression that evaluates to a value.
234#[derive(Debug, Clone, PartialEq)]
235pub enum Expr {
236    /// Literal value
237    Literal(Value),
238    /// Variable reference: `${VAR}` or `${VAR.field}` or `$VAR`
239    VarRef(VarPath),
240    /// String with interpolation: `"hello ${NAME}"` or `"hello $NAME"`
241    Interpolated(Vec<StringPart>),
242    /// Interpolated heredoc body with per-part spans for diagnostic precision.
243    ///
244    /// Heredoc bodies use this variant; double-quoted strings still use
245    /// `Interpolated` to keep the existing path untouched. `strip_tabs` is
246    /// `true` for the `<<-EOF` form — leading tabs on each body line are
247    /// stripped from `StringPart::Literal` content at materialization time
248    /// (POSIX semantics); offsets in `parts` reference the verbatim source
249    /// so spans remain meaningful.
250    HereDocBody {
251        parts: Vec<SpannedPart>,
252        strip_tabs: bool,
253    },
254    /// Binary operation: `a && b`, `a || b`
255    BinaryOp {
256        left: Box<Expr>,
257        op: BinaryOp,
258        right: Box<Expr>,
259    },
260    /// Command substitution: `$(pipeline)` - runs a pipeline and returns its result
261    CommandSubst(Box<Pipeline>),
262    /// Test expression: `[[ -f path ]]` or `[[ $X == "value" ]]`
263    Test(Box<TestExpr>),
264    /// Positional parameter: `$0` through `$9`
265    Positional(usize),
266    /// All positional arguments: `$@`
267    AllArgs,
268    /// Argument count: `$#`
269    ArgCount,
270    /// Variable string length: `${#VAR}`
271    VarLength(String),
272    /// Variable with default: `${VAR:-default}` - use default if VAR is unset or empty
273    /// The default can contain nested variable expansions and command substitutions
274    VarWithDefault { name: String, default: Vec<StringPart> },
275    /// Arithmetic expansion: `$((expr))` - evaluates to integer
276    Arithmetic(String),
277    /// Command as condition: `if grep -q pattern file; then` - exit code determines truthiness
278    Command(Command),
279    /// Last exit code: `$?`
280    LastExitCode,
281    /// Current shell PID: `$$`
282    CurrentPid,
283    /// Bare glob pattern: `*.txt`, `src/**/*.rs` — expanded during arg building
284    GlobPattern(String),
285}
286
287/// Test expression for `[[ ... ]]` conditionals.
288#[derive(Debug, Clone, PartialEq)]
289pub enum TestExpr {
290    /// File test: `[[ -f path ]]`, `[[ -d path ]]`, etc.
291    FileTest { op: FileTestOp, path: Box<Expr> },
292    /// String test: `[[ -z str ]]`, `[[ -n str ]]`
293    StringTest { op: StringTestOp, value: Box<Expr> },
294    /// Comparison: `[[ $X == "value" ]]`, `[[ $NUM -gt 5 ]]`
295    Comparison { left: Box<Expr>, op: TestCmpOp, right: Box<Expr> },
296    /// Logical AND: `[[ -f a && -d b ]]` (short-circuit evaluation)
297    And { left: Box<TestExpr>, right: Box<TestExpr> },
298    /// Logical OR: `[[ -f a || -d b ]]` (short-circuit evaluation)
299    Or { left: Box<TestExpr>, right: Box<TestExpr> },
300    /// Logical NOT: `[[ ! -f file ]]`
301    Not { expr: Box<TestExpr> },
302}
303
304/// File test operators for `[[ ]]`.
305#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum FileTestOp {
307    /// `-e` - exists
308    Exists,
309    /// `-f` - is regular file
310    IsFile,
311    /// `-d` - is directory
312    IsDir,
313    /// `-r` - is readable
314    Readable,
315    /// `-w` - is writable
316    Writable,
317    /// `-x` - is executable
318    Executable,
319}
320
321/// String test operators for `[[ ]]`.
322#[derive(Debug, Clone, Copy, PartialEq, Eq)]
323pub enum StringTestOp {
324    /// `-z` - string is empty
325    IsEmpty,
326    /// `-n` - string is non-empty
327    IsNonEmpty,
328}
329
330/// Comparison operators for `[[ ]]` tests.
331///
332/// Mirrors POSIX `[[ ]]` semantics: `==`/`!=`/`>`/`<`/`>=`/`<=` are string
333/// (lexicographic) comparisons, while `-eq`/`-ne`/`-gt`/`-lt`/`-ge`/`-le`
334/// are arithmetic comparisons that coerce string operands to numbers.
335#[derive(Debug, Clone, Copy, PartialEq, Eq)]
336pub enum TestCmpOp {
337    /// `==` / `=` — string equality
338    Eq,
339    /// `!=` — string inequality
340    NotEq,
341    /// `=~` — regex match
342    Match,
343    /// `!~` — regex not match
344    NotMatch,
345    /// `>` — string greater than (lexicographic)
346    Gt,
347    /// `<` — string less than (lexicographic)
348    Lt,
349    /// `>=` — string greater than or equal (lexicographic)
350    GtEq,
351    /// `<=` — string less than or equal (lexicographic)
352    LtEq,
353    /// `-eq` — numeric equality
354    NumEq,
355    /// `-ne` — numeric inequality
356    NumNotEq,
357    /// `-gt` — numeric greater than
358    NumGt,
359    /// `-lt` — numeric less than
360    NumLt,
361    /// `-ge` — numeric greater than or equal
362    NumGtEq,
363    /// `-le` — numeric less than or equal
364    NumLtEq,
365}
366
367// Value and BlobRef live in kaish-types.
368pub use kaish_types::{BlobRef, Value};
369
370/// Variable reference path: `${VAR}` or `${VAR.field}`.
371///
372/// `$?` resolves to the previous command's exit code as an int. Field access
373/// on `$?` is rejected by the validator (use `kaish-last` for structured data).
374/// Array indexing is not supported — use `jq` for JSON processing.
375#[derive(Debug, Clone, PartialEq)]
376pub struct VarPath {
377    pub segments: Vec<VarSegment>,
378}
379
380impl VarPath {
381    /// Create a simple variable reference with just a name.
382    pub fn simple(name: impl Into<String>) -> Self {
383        Self {
384            segments: vec![VarSegment::Field(name.into())],
385        }
386    }
387}
388
389/// A segment in a variable path.
390#[derive(Debug, Clone, PartialEq)]
391pub enum VarSegment {
392    /// Field access: `.field` or initial name
393    /// Only supported for special variables like `$?`
394    Field(String),
395}
396
397/// Part of an interpolated string.
398#[derive(Debug, Clone, PartialEq)]
399pub enum StringPart {
400    /// Literal text
401    Literal(String),
402    /// Variable interpolation: `${VAR}` or `$VAR`
403    Var(VarPath),
404    /// Variable with default: `${VAR:-default}` where default can contain nested expansions
405    VarWithDefault { name: String, default: Vec<StringPart> },
406    /// Variable string length: `${#VAR}`
407    VarLength(String),
408    /// Positional parameter: `$0`, `$1`, ..., `$9`
409    Positional(usize),
410    /// All arguments: `$@`
411    AllArgs,
412    /// Argument count: `$#`
413    ArgCount,
414    /// Arithmetic expansion: `$((expr))`
415    Arithmetic(String),
416    /// Command substitution: `$(pipeline)` embedded in a string
417    CommandSubst(Pipeline),
418    /// Last exit code: `$?`
419    LastExitCode,
420    /// Current shell PID: `$$`
421    CurrentPid,
422}
423
424/// Binary operators used to chain command/test conditions with `&&` / `||`.
425///
426/// Value-level comparisons (`==`, `-eq`, `-gt`, …) live on
427/// [`TestCmpOp`] inside `[[ ]]` and are not part of this enum.
428#[derive(Debug, Clone, Copy, PartialEq, Eq)]
429pub enum BinaryOp {
430    /// `&&` - logical and (short-circuit)
431    And,
432    /// `||` - logical or (short-circuit)
433    Or,
434}
435
436impl fmt::Display for BinaryOp {
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        match self {
439            BinaryOp::And => write!(f, "&&"),
440            BinaryOp::Or => write!(f, "||"),
441        }
442    }
443}
444
445impl fmt::Display for RedirectKind {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        match self {
448            RedirectKind::StdoutOverwrite => write!(f, ">"),
449            RedirectKind::StdoutAppend => write!(f, ">>"),
450            RedirectKind::Stdin => write!(f, "<"),
451            RedirectKind::HereDoc => write!(f, "<<"),
452            RedirectKind::HereString => write!(f, "<<<"),
453            RedirectKind::Stderr => write!(f, "2>"),
454            RedirectKind::Both => write!(f, "&>"),
455            RedirectKind::MergeStderr => write!(f, "2>&1"),
456            RedirectKind::MergeStdout => write!(f, "1>&2"),
457        }
458    }
459}
460
461impl fmt::Display for FileTestOp {
462    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
463        match self {
464            FileTestOp::Exists => write!(f, "-e"),
465            FileTestOp::IsFile => write!(f, "-f"),
466            FileTestOp::IsDir => write!(f, "-d"),
467            FileTestOp::Readable => write!(f, "-r"),
468            FileTestOp::Writable => write!(f, "-w"),
469            FileTestOp::Executable => write!(f, "-x"),
470        }
471    }
472}
473
474impl fmt::Display for StringTestOp {
475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476        match self {
477            StringTestOp::IsEmpty => write!(f, "-z"),
478            StringTestOp::IsNonEmpty => write!(f, "-n"),
479        }
480    }
481}
482
483impl fmt::Display for TestCmpOp {
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485        match self {
486            TestCmpOp::Eq => write!(f, "=="),
487            TestCmpOp::NotEq => write!(f, "!="),
488            TestCmpOp::Match => write!(f, "=~"),
489            TestCmpOp::NotMatch => write!(f, "!~"),
490            TestCmpOp::Gt => write!(f, ">"),
491            TestCmpOp::Lt => write!(f, "<"),
492            TestCmpOp::GtEq => write!(f, ">="),
493            TestCmpOp::LtEq => write!(f, "<="),
494            TestCmpOp::NumEq => write!(f, "-eq"),
495            TestCmpOp::NumNotEq => write!(f, "-ne"),
496            TestCmpOp::NumGt => write!(f, "-gt"),
497            TestCmpOp::NumLt => write!(f, "-lt"),
498            TestCmpOp::NumGtEq => write!(f, "-ge"),
499            TestCmpOp::NumLtEq => write!(f, "-le"),
500        }
501    }
502}