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}