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    /// `2>` stderr to file
205    Stderr,
206    /// `&>` both stdout and stderr to file
207    Both,
208    /// `2>&1` merge stderr into stdout
209    MergeStderr,
210    /// `1>&2` or `>&2` merge stdout into stderr
211    MergeStdout,
212}
213
214/// An expression that evaluates to a value.
215#[derive(Debug, Clone, PartialEq)]
216pub enum Expr {
217    /// Literal value
218    Literal(Value),
219    /// Variable reference: `${VAR}` or `${VAR.field}` or `$VAR`
220    VarRef(VarPath),
221    /// String with interpolation: `"hello ${NAME}"` or `"hello $NAME"`
222    Interpolated(Vec<StringPart>),
223    /// Binary operation: `a && b`, `a || b`
224    BinaryOp {
225        left: Box<Expr>,
226        op: BinaryOp,
227        right: Box<Expr>,
228    },
229    /// Command substitution: `$(pipeline)` - runs a pipeline and returns its result
230    CommandSubst(Box<Pipeline>),
231    /// Test expression: `[[ -f path ]]` or `[[ $X == "value" ]]`
232    Test(Box<TestExpr>),
233    /// Positional parameter: `$0` through `$9`
234    Positional(usize),
235    /// All positional arguments: `$@`
236    AllArgs,
237    /// Argument count: `$#`
238    ArgCount,
239    /// Variable string length: `${#VAR}`
240    VarLength(String),
241    /// Variable with default: `${VAR:-default}` - use default if VAR is unset or empty
242    /// The default can contain nested variable expansions and command substitutions
243    VarWithDefault { name: String, default: Vec<StringPart> },
244    /// Arithmetic expansion: `$((expr))` - evaluates to integer
245    Arithmetic(String),
246    /// Command as condition: `if grep -q pattern file; then` - exit code determines truthiness
247    Command(Command),
248    /// Last exit code: `$?`
249    LastExitCode,
250    /// Current shell PID: `$$`
251    CurrentPid,
252}
253
254/// Test expression for `[[ ... ]]` conditionals.
255#[derive(Debug, Clone, PartialEq)]
256pub enum TestExpr {
257    /// File test: `[[ -f path ]]`, `[[ -d path ]]`, etc.
258    FileTest { op: FileTestOp, path: Box<Expr> },
259    /// String test: `[[ -z str ]]`, `[[ -n str ]]`
260    StringTest { op: StringTestOp, value: Box<Expr> },
261    /// Comparison: `[[ $X == "value" ]]`, `[[ $NUM -gt 5 ]]`
262    Comparison { left: Box<Expr>, op: TestCmpOp, right: Box<Expr> },
263    /// Logical AND: `[[ -f a && -d b ]]` (short-circuit evaluation)
264    And { left: Box<TestExpr>, right: Box<TestExpr> },
265    /// Logical OR: `[[ -f a || -d b ]]` (short-circuit evaluation)
266    Or { left: Box<TestExpr>, right: Box<TestExpr> },
267    /// Logical NOT: `[[ ! -f file ]]`
268    Not { expr: Box<TestExpr> },
269}
270
271/// File test operators for `[[ ]]`.
272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
273pub enum FileTestOp {
274    /// `-e` - exists
275    Exists,
276    /// `-f` - is regular file
277    IsFile,
278    /// `-d` - is directory
279    IsDir,
280    /// `-r` - is readable
281    Readable,
282    /// `-w` - is writable
283    Writable,
284    /// `-x` - is executable
285    Executable,
286}
287
288/// String test operators for `[[ ]]`.
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
290pub enum StringTestOp {
291    /// `-z` - string is empty
292    IsEmpty,
293    /// `-n` - string is non-empty
294    IsNonEmpty,
295}
296
297/// Comparison operators for `[[ ]]` tests.
298#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum TestCmpOp {
300    /// `==` - string equality
301    Eq,
302    /// `!=` - string inequality
303    NotEq,
304    /// `=~` - regex match
305    Match,
306    /// `!~` - regex not match
307    NotMatch,
308    /// `-gt` - greater than (numeric)
309    Gt,
310    /// `-lt` - less than (numeric)
311    Lt,
312    /// `-ge` - greater than or equal (numeric)
313    GtEq,
314    /// `-le` - less than or equal (numeric)
315    LtEq,
316}
317
318/// A literal value.
319///
320/// Supports primitives (null, bool, int, float, string), structured JSON data
321/// (arrays and objects), and binary blob references.
322#[derive(Debug, Clone, PartialEq)]
323pub enum Value {
324    Null,
325    Bool(bool),
326    Int(i64),
327    Float(f64),
328    String(String),
329    /// Structured JSON data (arrays, objects, nested structures).
330    /// Use `jq` to query/extract values.
331    Json(serde_json::Value),
332    /// Reference to binary data stored in the virtual filesystem.
333    Blob(BlobRef),
334}
335
336/// Reference to binary data stored in `/v/blobs/{id}`.
337///
338/// Binary data flows through the blob storage system rather than being
339/// encoded as base64 in text fields.
340#[derive(Debug, Clone, PartialEq)]
341pub struct BlobRef {
342    /// Unique identifier, also the path suffix: `/v/blobs/{id}`
343    pub id: String,
344    /// Size of the blob in bytes.
345    pub size: u64,
346    /// MIME content type (e.g., "image/png", "application/octet-stream").
347    pub content_type: String,
348    /// Optional hash for integrity verification (SHA-256).
349    pub hash: Option<Vec<u8>>,
350}
351
352impl BlobRef {
353    /// Create a new blob reference.
354    pub fn new(id: impl Into<String>, size: u64, content_type: impl Into<String>) -> Self {
355        Self {
356            id: id.into(),
357            size,
358            content_type: content_type.into(),
359            hash: None,
360        }
361    }
362
363    /// Create a blob reference with a hash.
364    pub fn with_hash(mut self, hash: Vec<u8>) -> Self {
365        self.hash = Some(hash);
366        self
367    }
368
369    /// Get the VFS path for this blob.
370    pub fn path(&self) -> String {
371        format!("/v/blobs/{}", self.id)
372    }
373
374    /// Format size for display (e.g., "1.2MB", "456KB").
375    pub fn formatted_size(&self) -> String {
376        const KB: u64 = 1024;
377        const MB: u64 = 1024 * KB;
378        const GB: u64 = 1024 * MB;
379
380        if self.size >= GB {
381            format!("{:.1}GB", self.size as f64 / GB as f64)
382        } else if self.size >= MB {
383            format!("{:.1}MB", self.size as f64 / MB as f64)
384        } else if self.size >= KB {
385            format!("{:.1}KB", self.size as f64 / KB as f64)
386        } else {
387            format!("{}B", self.size)
388        }
389    }
390}
391
392/// Variable reference path: `${VAR}` or `${?.field}` for special variables.
393///
394/// Simple variable references support only field access for special variables
395/// like `$?`. Array indexing is not supported - use `jq` for JSON processing.
396#[derive(Debug, Clone, PartialEq)]
397pub struct VarPath {
398    pub segments: Vec<VarSegment>,
399}
400
401impl VarPath {
402    /// Create a simple variable reference with just a name.
403    pub fn simple(name: impl Into<String>) -> Self {
404        Self {
405            segments: vec![VarSegment::Field(name.into())],
406        }
407    }
408}
409
410/// A segment in a variable path.
411#[derive(Debug, Clone, PartialEq)]
412pub enum VarSegment {
413    /// Field access: `.field` or initial name
414    /// Only supported for special variables like `$?`
415    Field(String),
416}
417
418/// Part of an interpolated string.
419#[derive(Debug, Clone, PartialEq)]
420pub enum StringPart {
421    /// Literal text
422    Literal(String),
423    /// Variable interpolation: `${VAR}` or `$VAR`
424    Var(VarPath),
425    /// Variable with default: `${VAR:-default}` where default can contain nested expansions
426    VarWithDefault { name: String, default: Vec<StringPart> },
427    /// Variable string length: `${#VAR}`
428    VarLength(String),
429    /// Positional parameter: `$0`, `$1`, ..., `$9`
430    Positional(usize),
431    /// All arguments: `$@`
432    AllArgs,
433    /// Argument count: `$#`
434    ArgCount,
435    /// Arithmetic expansion: `$((expr))`
436    Arithmetic(String),
437    /// Command substitution: `$(pipeline)` embedded in a string
438    CommandSubst(Pipeline),
439    /// Last exit code: `$?`
440    LastExitCode,
441    /// Current shell PID: `$$`
442    CurrentPid,
443}
444
445/// Binary operators.
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub enum BinaryOp {
448    /// `&&` - logical and (short-circuit)
449    And,
450    /// `||` - logical or (short-circuit)
451    Or,
452    /// `==` - equality
453    Eq,
454    /// `!=` - inequality
455    NotEq,
456    /// `=~` - regex match
457    Match,
458    /// `!~` - regex not match
459    NotMatch,
460    /// `<` - less than
461    Lt,
462    /// `>` - greater than
463    Gt,
464    /// `<=` - less than or equal
465    LtEq,
466    /// `>=` - greater than or equal
467    GtEq,
468}
469
470impl fmt::Display for BinaryOp {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        match self {
473            BinaryOp::And => write!(f, "&&"),
474            BinaryOp::Or => write!(f, "||"),
475            BinaryOp::Eq => write!(f, "=="),
476            BinaryOp::NotEq => write!(f, "!="),
477            BinaryOp::Match => write!(f, "=~"),
478            BinaryOp::NotMatch => write!(f, "!~"),
479            BinaryOp::Lt => write!(f, "<"),
480            BinaryOp::Gt => write!(f, ">"),
481            BinaryOp::LtEq => write!(f, "<="),
482            BinaryOp::GtEq => write!(f, ">="),
483        }
484    }
485}
486
487impl fmt::Display for RedirectKind {
488    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
489        match self {
490            RedirectKind::StdoutOverwrite => write!(f, ">"),
491            RedirectKind::StdoutAppend => write!(f, ">>"),
492            RedirectKind::Stdin => write!(f, "<"),
493            RedirectKind::HereDoc => write!(f, "<<"),
494            RedirectKind::Stderr => write!(f, "2>"),
495            RedirectKind::Both => write!(f, "&>"),
496            RedirectKind::MergeStderr => write!(f, "2>&1"),
497            RedirectKind::MergeStdout => write!(f, "1>&2"),
498        }
499    }
500}
501
502impl fmt::Display for FileTestOp {
503    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504        match self {
505            FileTestOp::Exists => write!(f, "-e"),
506            FileTestOp::IsFile => write!(f, "-f"),
507            FileTestOp::IsDir => write!(f, "-d"),
508            FileTestOp::Readable => write!(f, "-r"),
509            FileTestOp::Writable => write!(f, "-w"),
510            FileTestOp::Executable => write!(f, "-x"),
511        }
512    }
513}
514
515impl fmt::Display for StringTestOp {
516    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517        match self {
518            StringTestOp::IsEmpty => write!(f, "-z"),
519            StringTestOp::IsNonEmpty => write!(f, "-n"),
520        }
521    }
522}
523
524impl fmt::Display for TestCmpOp {
525    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
526        match self {
527            TestCmpOp::Eq => write!(f, "=="),
528            TestCmpOp::NotEq => write!(f, "!="),
529            TestCmpOp::Match => write!(f, "=~"),
530            TestCmpOp::NotMatch => write!(f, "!~"),
531            TestCmpOp::Gt => write!(f, "-gt"),
532            TestCmpOp::Lt => write!(f, "-lt"),
533            TestCmpOp::GtEq => write!(f, "-ge"),
534            TestCmpOp::LtEq => write!(f, "-le"),
535        }
536    }
537}