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 /// Long flag with attached value: `--key=value`. Always routes through
177 /// `tool_args.named` regardless of the receiving command.
178 Named { key: String, value: Expr },
179 /// Bareword shell-assignment in argv position: `key=value`.
180 ///
181 /// Only commands on the kernel's shell-assignment allowlist (`export`,
182 /// `alias`) consume this as a named arg; for every other command it's
183 /// stringified to a positional `"key=value"`. This matches bash:
184 /// `cat foo=bar` opens a file named `foo=bar`, not a magical key=value.
185 WordAssign { key: String, value: Expr },
186 /// Short flag: `-l`, `-v` (boolean flag)
187 ShortFlag(String),
188 /// Long flag: `--force`, `--verbose` (boolean flag)
189 LongFlag(String),
190 /// Double-dash marker: `--` - signals end of flags
191 DoubleDash,
192}
193
194/// I/O redirection.
195#[derive(Debug, Clone, PartialEq)]
196pub struct Redirect {
197 pub kind: RedirectKind,
198 pub target: Expr,
199}
200
201/// Type of redirection.
202#[derive(Debug, Clone, PartialEq)]
203pub enum RedirectKind {
204 /// `>` stdout to file (overwrite)
205 StdoutOverwrite,
206 /// `>>` stdout to file (append)
207 StdoutAppend,
208 /// `<` stdin from file
209 Stdin,
210 /// `<<EOF ... EOF` stdin from here-doc
211 HereDoc,
212 /// `<<< word` stdin from here-string (bash-style)
213 HereString,
214 /// `2>` stderr to file
215 Stderr,
216 /// `&>` both stdout and stderr to file
217 Both,
218 /// `2>&1` merge stderr into stdout
219 MergeStderr,
220 /// `1>&2` or `>&2` merge stdout into stderr
221 MergeStdout,
222}
223
224/// A `StringPart` together with its byte offset in the original source.
225///
226/// Used by [`Expr::HereDocBody`] so the validator and interpreter can attribute
227/// diagnostics to a precise location inside an interpolated heredoc body.
228/// Double-quoted strings continue to use the spanless [`Expr::Interpolated`];
229/// universal spanning is a separate, larger refactor (see plan
230/// `make-heredocs-precious-puzzle`).
231#[derive(Debug, Clone, PartialEq)]
232pub struct SpannedPart {
233 /// The part itself.
234 pub part: StringPart,
235 /// Byte offset of this part in the original source string.
236 pub offset: usize,
237 /// Byte length of the part's source representation.
238 pub len: usize,
239}
240
241/// An expression that evaluates to a value.
242#[derive(Debug, Clone, PartialEq)]
243pub enum Expr {
244 /// Literal value
245 Literal(Value),
246 /// Variable reference: `${VAR}` or `${VAR.field}` or `$VAR`
247 VarRef(VarPath),
248 /// String with interpolation: `"hello ${NAME}"` or `"hello $NAME"`
249 Interpolated(Vec<StringPart>),
250 /// Interpolated heredoc body with per-part spans for diagnostic precision.
251 ///
252 /// Heredoc bodies use this variant; double-quoted strings still use
253 /// `Interpolated` to keep the existing path untouched. `strip_tabs` is
254 /// `true` for the `<<-EOF` form — leading tabs on each body line are
255 /// stripped from `StringPart::Literal` content at materialization time
256 /// (POSIX semantics); offsets in `parts` reference the verbatim source
257 /// so spans remain meaningful.
258 HereDocBody {
259 parts: Vec<SpannedPart>,
260 strip_tabs: bool,
261 },
262 /// Binary operation: `a && b`, `a || b`
263 BinaryOp {
264 left: Box<Expr>,
265 op: BinaryOp,
266 right: Box<Expr>,
267 },
268 /// Command substitution: `$(pipeline)` - runs a pipeline and returns its result
269 CommandSubst(Box<Pipeline>),
270 /// Test expression: `[[ -f path ]]` or `[[ $X == "value" ]]`
271 Test(Box<TestExpr>),
272 /// Positional parameter: `$0` through `$9`
273 Positional(usize),
274 /// All positional arguments: `$@`
275 AllArgs,
276 /// Argument count: `$#`
277 ArgCount,
278 /// Variable string length: `${#VAR}`
279 VarLength(String),
280 /// Variable with default: `${VAR:-default}` - use default if VAR is unset or empty
281 /// The default can contain nested variable expansions and command substitutions
282 VarWithDefault { name: String, default: Vec<StringPart> },
283 /// Arithmetic expansion: `$((expr))` - evaluates to integer
284 Arithmetic(String),
285 /// Command as condition: `if grep -q pattern file; then` - exit code determines truthiness
286 Command(Command),
287 /// Last exit code: `$?`
288 LastExitCode,
289 /// Current shell PID: `$$`
290 CurrentPid,
291 /// Bare glob pattern: `*.txt`, `src/**/*.rs` — expanded during arg building
292 GlobPattern(String),
293}
294
295/// Test expression for `[[ ... ]]` conditionals.
296#[derive(Debug, Clone, PartialEq)]
297pub enum TestExpr {
298 /// File test: `[[ -f path ]]`, `[[ -d path ]]`, etc.
299 FileTest { op: FileTestOp, path: Box<Expr> },
300 /// String test: `[[ -z str ]]`, `[[ -n str ]]`
301 StringTest { op: StringTestOp, value: Box<Expr> },
302 /// Comparison: `[[ $X == "value" ]]`, `[[ $NUM -gt 5 ]]`
303 Comparison { left: Box<Expr>, op: TestCmpOp, right: Box<Expr> },
304 /// Logical AND: `[[ -f a && -d b ]]` (short-circuit evaluation)
305 And { left: Box<TestExpr>, right: Box<TestExpr> },
306 /// Logical OR: `[[ -f a || -d b ]]` (short-circuit evaluation)
307 Or { left: Box<TestExpr>, right: Box<TestExpr> },
308 /// Logical NOT: `[[ ! -f file ]]`
309 Not { expr: Box<TestExpr> },
310}
311
312/// File test operators for `[[ ]]`.
313#[derive(Debug, Clone, Copy, PartialEq, Eq)]
314pub enum FileTestOp {
315 /// `-e` - exists
316 Exists,
317 /// `-f` - is regular file
318 IsFile,
319 /// `-d` - is directory
320 IsDir,
321 /// `-r` - is readable
322 Readable,
323 /// `-w` - is writable
324 Writable,
325 /// `-x` - is executable
326 Executable,
327}
328
329/// String test operators for `[[ ]]`.
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum StringTestOp {
332 /// `-z` - string is empty
333 IsEmpty,
334 /// `-n` - string is non-empty
335 IsNonEmpty,
336}
337
338/// Comparison operators for `[[ ]]` tests.
339///
340/// Mirrors POSIX `[[ ]]` semantics: `==`/`!=`/`>`/`<`/`>=`/`<=` are string
341/// (lexicographic) comparisons, while `-eq`/`-ne`/`-gt`/`-lt`/`-ge`/`-le`
342/// are arithmetic comparisons that coerce string operands to numbers.
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344pub enum TestCmpOp {
345 /// `==` / `=` — string equality
346 Eq,
347 /// `!=` — string inequality
348 NotEq,
349 /// `=~` — regex match
350 Match,
351 /// `!~` — regex not match
352 NotMatch,
353 /// `>` — string greater than (lexicographic)
354 Gt,
355 /// `<` — string less than (lexicographic)
356 Lt,
357 /// `>=` — string greater than or equal (lexicographic)
358 GtEq,
359 /// `<=` — string less than or equal (lexicographic)
360 LtEq,
361 /// `-eq` — numeric equality
362 NumEq,
363 /// `-ne` — numeric inequality
364 NumNotEq,
365 /// `-gt` — numeric greater than
366 NumGt,
367 /// `-lt` — numeric less than
368 NumLt,
369 /// `-ge` — numeric greater than or equal
370 NumGtEq,
371 /// `-le` — numeric less than or equal
372 NumLtEq,
373}
374
375// Value and BlobRef live in kaish-types.
376pub use kaish_types::{BlobRef, Value};
377
378/// Variable reference path: `${VAR}` or `${VAR.field}`.
379///
380/// `$?` resolves to the previous command's exit code as an int. Field access
381/// on `$?` is rejected by the validator (use `kaish-last` for structured data).
382/// Array indexing is not supported — use `jq` for JSON processing.
383#[derive(Debug, Clone, PartialEq)]
384pub struct VarPath {
385 pub segments: Vec<VarSegment>,
386}
387
388impl VarPath {
389 /// Create a simple variable reference with just a name.
390 pub fn simple(name: impl Into<String>) -> Self {
391 Self {
392 segments: vec![VarSegment::Field(name.into())],
393 }
394 }
395}
396
397/// A segment in a variable path.
398#[derive(Debug, Clone, PartialEq)]
399pub enum VarSegment {
400 /// Field access: `.field` or initial name
401 /// Only supported for special variables like `$?`
402 Field(String),
403}
404
405/// Part of an interpolated string.
406#[derive(Debug, Clone, PartialEq)]
407pub enum StringPart {
408 /// Literal text
409 Literal(String),
410 /// Variable interpolation: `${VAR}` or `$VAR`
411 Var(VarPath),
412 /// Variable with default: `${VAR:-default}` where default can contain nested expansions
413 VarWithDefault { name: String, default: Vec<StringPart> },
414 /// Variable string length: `${#VAR}`
415 VarLength(String),
416 /// Positional parameter: `$0`, `$1`, ..., `$9`
417 Positional(usize),
418 /// All arguments: `$@`
419 AllArgs,
420 /// Argument count: `$#`
421 ArgCount,
422 /// Arithmetic expansion: `$((expr))`
423 Arithmetic(String),
424 /// Command substitution: `$(pipeline)` embedded in a string
425 CommandSubst(Pipeline),
426 /// Last exit code: `$?`
427 LastExitCode,
428 /// Current shell PID: `$$`
429 CurrentPid,
430}
431
432/// Binary operators used to chain command/test conditions with `&&` / `||`.
433///
434/// Value-level comparisons (`==`, `-eq`, `-gt`, …) live on
435/// [`TestCmpOp`] inside `[[ ]]` and are not part of this enum.
436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
437pub enum BinaryOp {
438 /// `&&` - logical and (short-circuit)
439 And,
440 /// `||` - logical or (short-circuit)
441 Or,
442}
443
444impl fmt::Display for BinaryOp {
445 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
446 match self {
447 BinaryOp::And => write!(f, "&&"),
448 BinaryOp::Or => write!(f, "||"),
449 }
450 }
451}
452
453impl fmt::Display for RedirectKind {
454 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
455 match self {
456 RedirectKind::StdoutOverwrite => write!(f, ">"),
457 RedirectKind::StdoutAppend => write!(f, ">>"),
458 RedirectKind::Stdin => write!(f, "<"),
459 RedirectKind::HereDoc => write!(f, "<<"),
460 RedirectKind::HereString => write!(f, "<<<"),
461 RedirectKind::Stderr => write!(f, "2>"),
462 RedirectKind::Both => write!(f, "&>"),
463 RedirectKind::MergeStderr => write!(f, "2>&1"),
464 RedirectKind::MergeStdout => write!(f, "1>&2"),
465 }
466 }
467}
468
469impl fmt::Display for FileTestOp {
470 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
471 match self {
472 FileTestOp::Exists => write!(f, "-e"),
473 FileTestOp::IsFile => write!(f, "-f"),
474 FileTestOp::IsDir => write!(f, "-d"),
475 FileTestOp::Readable => write!(f, "-r"),
476 FileTestOp::Writable => write!(f, "-w"),
477 FileTestOp::Executable => write!(f, "-x"),
478 }
479 }
480}
481
482impl fmt::Display for StringTestOp {
483 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
484 match self {
485 StringTestOp::IsEmpty => write!(f, "-z"),
486 StringTestOp::IsNonEmpty => write!(f, "-n"),
487 }
488 }
489}
490
491impl fmt::Display for TestCmpOp {
492 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
493 match self {
494 TestCmpOp::Eq => write!(f, "=="),
495 TestCmpOp::NotEq => write!(f, "!="),
496 TestCmpOp::Match => write!(f, "=~"),
497 TestCmpOp::NotMatch => write!(f, "!~"),
498 TestCmpOp::Gt => write!(f, ">"),
499 TestCmpOp::Lt => write!(f, "<"),
500 TestCmpOp::GtEq => write!(f, ">="),
501 TestCmpOp::LtEq => write!(f, "<="),
502 TestCmpOp::NumEq => write!(f, "-eq"),
503 TestCmpOp::NumNotEq => write!(f, "-ne"),
504 TestCmpOp::NumGt => write!(f, "-gt"),
505 TestCmpOp::NumLt => write!(f, "-lt"),
506 TestCmpOp::NumGtEq => write!(f, "-ge"),
507 TestCmpOp::NumLtEq => write!(f, "-le"),
508 }
509 }
510}