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