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
48/// Variable assignment: `NAME=value` (bash-style) or `local NAME = value` (scoped)
49#[derive(Debug, Clone, PartialEq)]
50pub struct Assignment {
51    pub name: String,
52    pub value: Expr,
53    /// True if declared with `local` keyword (explicit local scope)
54    pub local: bool,
55}
56
57/// A command invocation with arguments and redirections.
58#[derive(Debug, Clone, PartialEq)]
59pub struct Command {
60    pub name: String,
61    pub args: Vec<Arg>,
62    pub redirects: Vec<Redirect>,
63}
64
65/// A pipeline of commands connected by pipes.
66#[derive(Debug, Clone, PartialEq)]
67pub struct Pipeline {
68    pub commands: Vec<Command>,
69    pub background: bool,
70}
71
72/// Conditional statement.
73#[derive(Debug, Clone, PartialEq)]
74pub struct IfStmt {
75    pub condition: Box<Expr>,
76    pub then_branch: Vec<Stmt>,
77    pub else_branch: Option<Vec<Stmt>>,
78}
79
80/// For loop over items.
81#[derive(Debug, Clone, PartialEq)]
82pub struct ForLoop {
83    pub variable: String,
84    /// Items to iterate over. Each is evaluated, then word-split for iteration.
85    pub items: Vec<Expr>,
86    pub body: Vec<Stmt>,
87}
88
89/// While loop with condition.
90#[derive(Debug, Clone, PartialEq)]
91pub struct WhileLoop {
92    pub condition: Box<Expr>,
93    pub body: Vec<Stmt>,
94}
95
96/// Case statement for pattern matching.
97///
98/// ```kaish
99/// case $VAR in
100///     pattern1) commands ;;
101///     pattern2|pattern3) commands ;;
102///     *) default ;;
103/// esac
104/// ```
105#[derive(Debug, Clone, PartialEq)]
106pub struct CaseStmt {
107    /// The expression to match against
108    pub expr: Expr,
109    /// The pattern branches
110    pub branches: Vec<CaseBranch>,
111}
112
113/// A single branch in a case statement.
114#[derive(Debug, Clone, PartialEq)]
115pub struct CaseBranch {
116    /// Glob patterns to match (separated by `|`)
117    pub patterns: Vec<String>,
118    /// Commands to execute if matched
119    pub body: Vec<Stmt>,
120}
121
122/// User-defined tool.
123#[derive(Debug, Clone, PartialEq)]
124pub struct ToolDef {
125    pub name: String,
126    pub params: Vec<ParamDef>,
127    pub body: Vec<Stmt>,
128}
129
130/// Parameter definition for a tool.
131#[derive(Debug, Clone, PartialEq)]
132pub struct ParamDef {
133    pub name: String,
134    pub param_type: Option<ParamType>,
135    pub default: Option<Expr>,
136}
137
138/// Parameter type annotation.
139#[derive(Debug, Clone, PartialEq)]
140pub enum ParamType {
141    String,
142    Int,
143    Float,
144    Bool,
145}
146
147/// A command argument (positional or named).
148#[derive(Debug, Clone, PartialEq)]
149pub enum Arg {
150    /// Positional argument: `value`
151    Positional(Expr),
152    /// Named argument: `key=value`
153    Named { key: String, value: Expr },
154    /// Short flag: `-l`, `-v` (boolean flag)
155    ShortFlag(String),
156    /// Long flag: `--force`, `--verbose` (boolean flag)
157    LongFlag(String),
158    /// Double-dash marker: `--` - signals end of flags
159    DoubleDash,
160}
161
162/// I/O redirection.
163#[derive(Debug, Clone, PartialEq)]
164pub struct Redirect {
165    pub kind: RedirectKind,
166    pub target: Expr,
167}
168
169/// Type of redirection.
170#[derive(Debug, Clone, PartialEq)]
171pub enum RedirectKind {
172    /// `>` stdout to file (overwrite)
173    StdoutOverwrite,
174    /// `>>` stdout to file (append)
175    StdoutAppend,
176    /// `<` stdin from file
177    Stdin,
178    /// `<<EOF ... EOF` stdin from here-doc
179    HereDoc,
180    /// `2>` stderr to file
181    Stderr,
182    /// `&>` both stdout and stderr to file
183    Both,
184    /// `2>&1` merge stderr into stdout
185    MergeStderr,
186    /// `1>&2` or `>&2` merge stdout into stderr
187    MergeStdout,
188}
189
190/// An expression that evaluates to a value.
191#[derive(Debug, Clone, PartialEq)]
192pub enum Expr {
193    /// Literal value
194    Literal(Value),
195    /// Variable reference: `${VAR}` or `${VAR.field}` or `$VAR`
196    VarRef(VarPath),
197    /// String with interpolation: `"hello ${NAME}"` or `"hello $NAME"`
198    Interpolated(Vec<StringPart>),
199    /// Binary operation: `a && b`, `a || b`
200    BinaryOp {
201        left: Box<Expr>,
202        op: BinaryOp,
203        right: Box<Expr>,
204    },
205    /// Command substitution: `$(pipeline)` - runs a pipeline and returns its result
206    CommandSubst(Box<Pipeline>),
207    /// Test expression: `[[ -f path ]]` or `[[ $X == "value" ]]`
208    Test(Box<TestExpr>),
209    /// Positional parameter: `$0` through `$9`
210    Positional(usize),
211    /// All positional arguments: `$@`
212    AllArgs,
213    /// Argument count: `$#`
214    ArgCount,
215    /// Variable string length: `${#VAR}`
216    VarLength(String),
217    /// Variable with default: `${VAR:-default}` - use default if VAR is unset or empty
218    /// The default can contain nested variable expansions and command substitutions
219    VarWithDefault { name: String, default: Vec<StringPart> },
220    /// Arithmetic expansion: `$((expr))` - evaluates to integer
221    Arithmetic(String),
222    /// Command as condition: `if grep -q pattern file; then` - exit code determines truthiness
223    Command(Command),
224    /// Last exit code: `$?`
225    LastExitCode,
226    /// Current shell PID: `$$`
227    CurrentPid,
228}
229
230/// Test expression for `[[ ... ]]` conditionals.
231#[derive(Debug, Clone, PartialEq)]
232pub enum TestExpr {
233    /// File test: `[[ -f path ]]`, `[[ -d path ]]`, etc.
234    FileTest { op: FileTestOp, path: Box<Expr> },
235    /// String test: `[[ -z str ]]`, `[[ -n str ]]`
236    StringTest { op: StringTestOp, value: Box<Expr> },
237    /// Comparison: `[[ $X == "value" ]]`, `[[ $NUM -gt 5 ]]`
238    Comparison { left: Box<Expr>, op: TestCmpOp, right: Box<Expr> },
239    /// Logical AND: `[[ -f a && -d b ]]` (short-circuit evaluation)
240    And { left: Box<TestExpr>, right: Box<TestExpr> },
241    /// Logical OR: `[[ -f a || -d b ]]` (short-circuit evaluation)
242    Or { left: Box<TestExpr>, right: Box<TestExpr> },
243    /// Logical NOT: `[[ ! -f file ]]`
244    Not { expr: Box<TestExpr> },
245}
246
247/// File test operators for `[[ ]]`.
248#[derive(Debug, Clone, Copy, PartialEq, Eq)]
249pub enum FileTestOp {
250    /// `-e` - exists
251    Exists,
252    /// `-f` - is regular file
253    IsFile,
254    /// `-d` - is directory
255    IsDir,
256    /// `-r` - is readable
257    Readable,
258    /// `-w` - is writable
259    Writable,
260    /// `-x` - is executable
261    Executable,
262}
263
264/// String test operators for `[[ ]]`.
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266pub enum StringTestOp {
267    /// `-z` - string is empty
268    IsEmpty,
269    /// `-n` - string is non-empty
270    IsNonEmpty,
271}
272
273/// Comparison operators for `[[ ]]` tests.
274#[derive(Debug, Clone, Copy, PartialEq, Eq)]
275pub enum TestCmpOp {
276    /// `==` - string equality
277    Eq,
278    /// `!=` - string inequality
279    NotEq,
280    /// `=~` - regex match
281    Match,
282    /// `!~` - regex not match
283    NotMatch,
284    /// `-gt` - greater than (numeric)
285    Gt,
286    /// `-lt` - less than (numeric)
287    Lt,
288    /// `-ge` - greater than or equal (numeric)
289    GtEq,
290    /// `-le` - less than or equal (numeric)
291    LtEq,
292}
293
294/// A literal value.
295///
296/// Supports primitives (null, bool, int, float, string), structured JSON data
297/// (arrays and objects), and binary blob references.
298#[derive(Debug, Clone, PartialEq)]
299pub enum Value {
300    Null,
301    Bool(bool),
302    Int(i64),
303    Float(f64),
304    String(String),
305    /// Structured JSON data (arrays, objects, nested structures).
306    /// Use `jq` to query/extract values.
307    Json(serde_json::Value),
308    /// Reference to binary data stored in the virtual filesystem.
309    Blob(BlobRef),
310}
311
312/// Reference to binary data stored in `/v/blobs/{id}`.
313///
314/// Binary data flows through the blob storage system rather than being
315/// encoded as base64 in text fields.
316#[derive(Debug, Clone, PartialEq)]
317pub struct BlobRef {
318    /// Unique identifier, also the path suffix: `/v/blobs/{id}`
319    pub id: String,
320    /// Size of the blob in bytes.
321    pub size: u64,
322    /// MIME content type (e.g., "image/png", "application/octet-stream").
323    pub content_type: String,
324    /// Optional hash for integrity verification (SHA-256).
325    pub hash: Option<Vec<u8>>,
326}
327
328impl BlobRef {
329    /// Create a new blob reference.
330    pub fn new(id: impl Into<String>, size: u64, content_type: impl Into<String>) -> Self {
331        Self {
332            id: id.into(),
333            size,
334            content_type: content_type.into(),
335            hash: None,
336        }
337    }
338
339    /// Create a blob reference with a hash.
340    pub fn with_hash(mut self, hash: Vec<u8>) -> Self {
341        self.hash = Some(hash);
342        self
343    }
344
345    /// Get the VFS path for this blob.
346    pub fn path(&self) -> String {
347        format!("/v/blobs/{}", self.id)
348    }
349
350    /// Format size for display (e.g., "1.2MB", "456KB").
351    pub fn formatted_size(&self) -> String {
352        const KB: u64 = 1024;
353        const MB: u64 = 1024 * KB;
354        const GB: u64 = 1024 * MB;
355
356        if self.size >= GB {
357            format!("{:.1}GB", self.size as f64 / GB as f64)
358        } else if self.size >= MB {
359            format!("{:.1}MB", self.size as f64 / MB as f64)
360        } else if self.size >= KB {
361            format!("{:.1}KB", self.size as f64 / KB as f64)
362        } else {
363            format!("{}B", self.size)
364        }
365    }
366}
367
368/// Variable reference path: `${VAR}` or `${?.field}` for special variables.
369///
370/// Simple variable references support only field access for special variables
371/// like `$?`. Array indexing is not supported - use `jq` for JSON processing.
372#[derive(Debug, Clone, PartialEq)]
373pub struct VarPath {
374    pub segments: Vec<VarSegment>,
375}
376
377impl VarPath {
378    /// Create a simple variable reference with just a name.
379    pub fn simple(name: impl Into<String>) -> Self {
380        Self {
381            segments: vec![VarSegment::Field(name.into())],
382        }
383    }
384}
385
386/// A segment in a variable path.
387#[derive(Debug, Clone, PartialEq)]
388pub enum VarSegment {
389    /// Field access: `.field` or initial name
390    /// Only supported for special variables like `$?`
391    Field(String),
392}
393
394/// Part of an interpolated string.
395#[derive(Debug, Clone, PartialEq)]
396pub enum StringPart {
397    /// Literal text
398    Literal(String),
399    /// Variable interpolation: `${VAR}` or `$VAR`
400    Var(VarPath),
401    /// Variable with default: `${VAR:-default}` where default can contain nested expansions
402    VarWithDefault { name: String, default: Vec<StringPart> },
403    /// Variable string length: `${#VAR}`
404    VarLength(String),
405    /// Positional parameter: `$0`, `$1`, ..., `$9`
406    Positional(usize),
407    /// All arguments: `$@`
408    AllArgs,
409    /// Argument count: `$#`
410    ArgCount,
411    /// Arithmetic expansion: `$((expr))`
412    Arithmetic(String),
413    /// Command substitution: `$(pipeline)` embedded in a string
414    CommandSubst(Pipeline),
415    /// Last exit code: `$?`
416    LastExitCode,
417    /// Current shell PID: `$$`
418    CurrentPid,
419}
420
421/// Binary operators.
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub enum BinaryOp {
424    /// `&&` - logical and (short-circuit)
425    And,
426    /// `||` - logical or (short-circuit)
427    Or,
428    /// `==` - equality
429    Eq,
430    /// `!=` - inequality
431    NotEq,
432    /// `=~` - regex match
433    Match,
434    /// `!~` - regex not match
435    NotMatch,
436    /// `<` - less than
437    Lt,
438    /// `>` - greater than
439    Gt,
440    /// `<=` - less than or equal
441    LtEq,
442    /// `>=` - greater than or equal
443    GtEq,
444}
445
446impl fmt::Display for BinaryOp {
447    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
448        match self {
449            BinaryOp::And => write!(f, "&&"),
450            BinaryOp::Or => write!(f, "||"),
451            BinaryOp::Eq => write!(f, "=="),
452            BinaryOp::NotEq => write!(f, "!="),
453            BinaryOp::Match => write!(f, "=~"),
454            BinaryOp::NotMatch => write!(f, "!~"),
455            BinaryOp::Lt => write!(f, "<"),
456            BinaryOp::Gt => write!(f, ">"),
457            BinaryOp::LtEq => write!(f, "<="),
458            BinaryOp::GtEq => write!(f, ">="),
459        }
460    }
461}
462
463impl fmt::Display for RedirectKind {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        match self {
466            RedirectKind::StdoutOverwrite => write!(f, ">"),
467            RedirectKind::StdoutAppend => write!(f, ">>"),
468            RedirectKind::Stdin => write!(f, "<"),
469            RedirectKind::HereDoc => 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, "-gt"),
508            TestCmpOp::Lt => write!(f, "-lt"),
509            TestCmpOp::GtEq => write!(f, "-ge"),
510            TestCmpOp::LtEq => write!(f, "-le"),
511        }
512    }
513}