Skip to main content

oxdock_parser/
ast.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4pub use crate::commands::StepKind;
5
6#[derive(Copy, Clone, Debug, Eq, PartialEq)]
7pub enum Command {
8    InheritEnv,
9    Workdir,
10    Workspace,
11    Env,
12    Echo,
13    Run,
14    Copy,
15    WithIo,
16    CopyGit,
17    HashSha256,
18    Symlink,
19    Mkdir,
20    Ls,
21    Cwd,
22    Read,
23    ReadLine,
24    Write,
25    Append,
26    Expand,
27    AssertFile,
28    AssertDir,
29    AssertAbsent,
30    AssertStdout,
31    Exit,
32    Async,
33    Timeout,
34    Sleep,
35}
36
37pub const COMMANDS: &[Command] = &[
38    Command::InheritEnv,
39    Command::Workdir,
40    Command::Workspace,
41    Command::Env,
42    Command::Echo,
43    Command::Run,
44    Command::Copy,
45    Command::WithIo,
46    Command::CopyGit,
47    Command::HashSha256,
48    Command::Symlink,
49    Command::Mkdir,
50    Command::Ls,
51    Command::Cwd,
52    Command::Read,
53    Command::ReadLine,
54    Command::Write,
55    Command::Append,
56    Command::Expand,
57    Command::AssertFile,
58    Command::AssertDir,
59    Command::AssertAbsent,
60    Command::AssertStdout,
61    Command::Exit,
62    Command::Timeout,
63    Command::Sleep,
64];
65
66impl Command {
67    pub const fn as_str(self) -> &'static str {
68        match self {
69            Command::InheritEnv => "INHERIT_ENV",
70            Command::Workdir => "WORKDIR",
71            Command::Workspace => "WORKSPACE",
72            Command::Env => "ENV",
73            Command::Echo => "ECHO",
74            Command::Run => "RUN",
75            Command::Copy => "COPY",
76            Command::WithIo => "WITH_IO",
77            Command::CopyGit => "COPY_GIT",
78            Command::HashSha256 => "HASH_SHA256",
79            Command::Symlink => "SYMLINK",
80            Command::Mkdir => "MKDIR",
81            Command::Ls => "LS",
82            Command::Cwd => "CWD",
83            Command::Read => "READ",
84            Command::ReadLine => "READ_LINE",
85            Command::Write => "WRITE",
86            Command::Append => "APPEND",
87            Command::Expand => "EXPAND",
88            Command::AssertFile => "ASSERT_FILE",
89            Command::AssertDir => "ASSERT_DIR",
90            Command::AssertAbsent => "ASSERT_ABSENT",
91            Command::AssertStdout => "ASSERT_STDOUT",
92            Command::Exit => "EXIT",
93            Command::Async => "ASYNC",
94            Command::Timeout => "TIMEOUT",
95            Command::Sleep => "SLEEP",
96        }
97    }
98
99    pub const fn syntax(self) -> &'static str {
100        match self {
101            Command::InheritEnv => "INHERIT_ENV [KEY1, KEY2, ...]",
102            Command::Workdir => "WORKDIR <path>",
103            Command::Workspace => "WORKSPACE SNAPSHOT|LOCAL",
104            Command::Env => "ENV KEY=value",
105            Command::Echo => "ECHO <message>",
106            Command::Run => "RUN <command...>",
107            Command::Copy => "COPY [--from-current-workspace] <from> <to>",
108            Command::CopyGit => "COPY_GIT [--include-dirty] <rev> <src> <dst>",
109            Command::WithIo => "WITH_IO [bindings] [command | { block }]",
110            Command::HashSha256 => "HASH_SHA256 <path>",
111            Command::Symlink => "SYMLINK <from> <to>",
112            Command::Mkdir => "MKDIR <path>",
113            Command::Ls => "LS [<path>]",
114            Command::Cwd => "CWD",
115            Command::Read => "READ [<path>]",
116            Command::ReadLine => "READ_LINE $var",
117            Command::Write => "WRITE <path> [<contents>]",
118            Command::Append => "APPEND <path> [<contents>]",
119            Command::Expand => "EXPAND [<path>] [<KEY=val> ...]",
120            Command::AssertFile => "ASSERT_FILE [--hash <sha256>] <path> [<expected>]",
121            Command::AssertDir => "ASSERT_DIR <path>",
122            Command::AssertAbsent => "ASSERT_ABSENT <path>",
123            Command::AssertStdout => "ASSERT_STDOUT <substring>",
124            Command::Exit => "EXIT <code>",
125            Command::Async => "ASYNC <command...> | ASYNC { <commands> }",
126            Command::Timeout => {
127                "TIMEOUT <duration> <command...> | TIMEOUT <duration> { <commands> }"
128            }
129            Command::Sleep => "SLEEP <duration>",
130        }
131    }
132
133    pub const fn expects_inner_command(self) -> bool {
134        matches!(self, Command::WithIo | Command::Async | Command::Timeout)
135    }
136
137    pub fn parse(s: &str) -> Option<Self> {
138        match s {
139            "INHERIT_ENV" => Some(Command::InheritEnv),
140            "WORKDIR" => Some(Command::Workdir),
141            "WORKSPACE" => Some(Command::Workspace),
142            "ENV" => Some(Command::Env),
143            "ECHO" => Some(Command::Echo),
144            "RUN" => Some(Command::Run),
145            "COPY" => Some(Command::Copy),
146            "WITH_IO" => Some(Command::WithIo),
147            "COPY_GIT" => Some(Command::CopyGit),
148            "HASH_SHA256" => Some(Command::HashSha256),
149            "SYMLINK" => Some(Command::Symlink),
150            "MKDIR" => Some(Command::Mkdir),
151            "LS" => Some(Command::Ls),
152            "CWD" => Some(Command::Cwd),
153            "READ" => Some(Command::Read),
154            "READ_LINE" => Some(Command::ReadLine),
155            "WRITE" => Some(Command::Write),
156            "APPEND" => Some(Command::Append),
157            "EXPAND" => Some(Command::Expand),
158            "ASSERT_FILE" => Some(Command::AssertFile),
159            "ASSERT_DIR" => Some(Command::AssertDir),
160            "ASSERT_ABSENT" => Some(Command::AssertAbsent),
161            "ASSERT_STDOUT" => Some(Command::AssertStdout),
162            "EXIT" => Some(Command::Exit),
163            "ASYNC" => Some(Command::Async),
164            "TIMEOUT" => Some(Command::Timeout),
165            "SLEEP" => Some(Command::Sleep),
166            _ => None,
167        }
168    }
169}
170
171#[derive(Copy, Clone, Debug, Eq, PartialEq)]
172pub enum PlatformGuard {
173    Unix,
174    Windows,
175    Macos,
176    Linux,
177}
178
179#[derive(Debug, Clone, Eq, PartialEq)]
180pub enum Guard {
181    Platform { target: PlatformGuard },
182    EnvExists { key: String },
183    EnvEquals { key: String, value: String },
184    StaticBool { value: String },
185}
186
187#[derive(Debug, Clone, Eq, PartialEq)]
188pub enum GuardExpr {
189    Predicate(Guard),
190    All(Vec<GuardExpr>),
191    Or(Vec<GuardExpr>),
192    Not(Box<GuardExpr>),
193}
194
195impl GuardExpr {
196    pub fn all(exprs: Vec<GuardExpr>) -> GuardExpr {
197        let mut flat = Vec::new();
198        for expr in exprs {
199            match expr {
200                GuardExpr::All(children) => flat.extend(children),
201                other => flat.push(other),
202            }
203        }
204        match flat.len() {
205            0 => panic!("GuardExpr::all requires at least one expression"),
206            1 => flat.into_iter().next().unwrap(),
207            _ => GuardExpr::All(flat),
208        }
209    }
210
211    pub fn or(exprs: Vec<GuardExpr>) -> GuardExpr {
212        let mut flat = Vec::new();
213        for expr in exprs {
214            match expr {
215                GuardExpr::Or(children) => flat.extend(children),
216                other => flat.push(other),
217            }
218        }
219        match flat.len() {
220            0 => panic!("GuardExpr::or requires at least one expression"),
221            1 => flat.into_iter().next().unwrap(),
222            _ => GuardExpr::Or(flat),
223        }
224    }
225
226    pub fn invert(expr: GuardExpr) -> GuardExpr {
227        match expr {
228            GuardExpr::Not(inner) => *inner,
229            other => GuardExpr::Not(Box::new(other)),
230        }
231    }
232}
233
234impl std::ops::Not for GuardExpr {
235    type Output = GuardExpr;
236
237    fn not(self) -> GuardExpr {
238        match self {
239            GuardExpr::Not(inner) => *inner,
240            other => GuardExpr::Not(Box::new(other)),
241        }
242    }
243}
244
245impl From<Guard> for GuardExpr {
246    fn from(guard: Guard) -> Self {
247        GuardExpr::Predicate(guard)
248    }
249}
250
251/// A command argument — either an expandable string or an expression.
252#[derive(Debug, Clone, Eq, PartialEq)]
253pub enum Arg {
254    /// Expandable string. The `bool` indicates whether the argument was
255    /// quoted in the source (`true`) or unquoted (`false`). Quoted arguments
256    /// that start with `--` are positional, not flags.
257    String(String, bool),
258    /// Expression — resolved at runtime via evaluate_expr.
259    Expr(Expr),
260}
261
262impl Arg {
263    pub fn as_str(&self) -> &str {
264        match self {
265            Arg::String(s, _) => s,
266            Arg::Expr(_) => "",
267        }
268    }
269
270    pub fn is_quoted(&self) -> bool {
271        matches!(self, Arg::String(_, true))
272    }
273}
274
275impl From<String> for Arg {
276    fn from(s: String) -> Self {
277        Arg::String(s, false)
278    }
279}
280
281impl From<&str> for Arg {
282    fn from(s: &str) -> Self {
283        Arg::String(s.to_string(), false)
284    }
285}
286
287impl std::fmt::Display for Arg {
288    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
289        match self {
290            Arg::String(s, _) => write!(f, "{}", s),
291            Arg::Expr(e) => write!(f, "{}", e),
292        }
293    }
294}
295
296impl AsRef<str> for Arg {
297    fn as_ref(&self) -> &str {
298        self.as_str()
299    }
300}
301
302impl PartialEq<str> for Arg {
303    fn eq(&self, other: &str) -> bool {
304        self.as_str() == other
305    }
306}
307
308impl PartialEq<&str> for Arg {
309    fn eq(&self, other: &&str) -> bool {
310        self.as_str() == *other
311    }
312}
313
314#[derive(Debug, Clone, Eq, PartialEq)]
315pub enum IoStream {
316    Stdin,
317    Stdout,
318    Stderr,
319}
320
321#[derive(Debug, Clone, Eq, PartialEq)]
322pub struct IoBinding {
323    pub stream: IoStream,
324    pub pipe: Option<String>,
325}
326
327#[derive(Debug, Clone, Eq, PartialEq)]
328pub enum Value {
329    String(String),
330    Int(i64),
331    List(Vec<Value>),
332    Map(std::collections::BTreeMap<String, Value>),
333    Bool(bool),
334    /// Handle to a background ASYNC task. The `u64` is the task ID
335    /// used to look up the handle in `ExecState.named_tasks`.
336    TaskHandle(u64),
337}
338
339#[derive(Debug, Clone, Copy, Eq, PartialEq)]
340pub enum CompareOp {
341    Eq,
342    Ne,
343}
344
345#[derive(Debug, Clone, Copy, Eq, PartialEq)]
346pub enum LogicalOp {
347    And,
348    Or,
349}
350
351#[derive(Debug, Clone, Eq, PartialEq)]
352pub enum Expr {
353    Literal(Value),
354    Var(String),
355    KeyPath {
356        base: String,
357        keys: Vec<String>,
358    },
359    List(Vec<Expr>),
360    Map(Vec<(String, Expr)>),
361    Call {
362        name: String,
363        args: Vec<Expr>,
364    },
365    Compare {
366        op: CompareOp,
367        left: Box<Expr>,
368        right: Box<Expr>,
369    },
370    Not(Box<Expr>),
371    Logical {
372        op: LogicalOp,
373        left: Box<Expr>,
374        right: Box<Expr>,
375    },
376}
377
378#[derive(Debug, Clone, Eq, PartialEq)]
379pub struct Step {
380    pub guard: Option<GuardExpr>,
381    pub kind: StepKind,
382    pub scope_enter: usize,
383    pub scope_exit: usize,
384}
385
386#[derive(Debug, Clone, Eq, PartialEq)]
387pub enum WorkspaceTarget {
388    Snapshot,
389    Local,
390}
391
392fn platform_matches(target: PlatformGuard) -> bool {
393    #[allow(clippy::disallowed_macros)]
394    match target {
395        PlatformGuard::Unix => cfg!(unix),
396        PlatformGuard::Windows => cfg!(windows),
397        PlatformGuard::Macos => cfg!(target_os = "macos"),
398        PlatformGuard::Linux => cfg!(target_os = "linux"),
399    }
400}
401
402pub trait EnvLookup {
403    fn get_env(&self, key: &str) -> Option<&str>;
404}
405
406impl EnvLookup for HashMap<String, String> {
407    fn get_env(&self, key: &str) -> Option<&str> {
408        self.get(key).map(|s| s.as_str())
409    }
410}
411
412impl EnvLookup for Arc<HashMap<String, String>> {
413    fn get_env(&self, key: &str) -> Option<&str> {
414        (**self).get_env(key)
415    }
416}
417
418pub fn guard_allows(guard: &Guard, env: &impl EnvLookup) -> bool {
419    match guard {
420        Guard::Platform { target } => platform_matches(*target),
421        Guard::EnvExists { key } => env.get_env(key).map(|v| !v.is_empty()).unwrap_or(false),
422        Guard::EnvEquals { key, value } => env
423            .get_env(key)
424            .map(|v| v == value.as_str())
425            .unwrap_or(false),
426        Guard::StaticBool { value } => value.parse::<bool>().unwrap_or(false),
427    }
428}
429
430pub fn guard_expr_allows(expr: &GuardExpr, env: &impl EnvLookup) -> bool {
431    match expr {
432        GuardExpr::Predicate(guard) => guard_allows(guard, env),
433        GuardExpr::All(children) => children.iter().all(|g| guard_expr_allows(g, env)),
434        GuardExpr::Or(children) => children.iter().any(|g| guard_expr_allows(g, env)),
435        GuardExpr::Not(child) => !guard_expr_allows(child, env),
436    }
437}
438
439pub fn guard_option_allows(expr: Option<&GuardExpr>, env: &impl EnvLookup) -> bool {
440    match expr {
441        Some(e) => guard_expr_allows(e, env),
442        None => true,
443    }
444}
445
446use std::fmt;
447
448impl fmt::Display for PlatformGuard {
449    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
450        match self {
451            PlatformGuard::Unix => write!(f, "unix"),
452            PlatformGuard::Windows => write!(f, "windows"),
453            PlatformGuard::Macos => write!(f, "macos"),
454            PlatformGuard::Linux => write!(f, "linux"),
455        }
456    }
457}
458
459impl fmt::Display for Guard {
460    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
461        match self {
462            Guard::Platform { target } => write!(f, "{}", target),
463            Guard::EnvExists { key } => write!(f, "env:{}", key),
464            Guard::EnvEquals { key, value } => write!(f, "eq(env:{}, {})", key, value),
465            Guard::StaticBool { value } => write!(f, "bool:{}", value),
466        }
467    }
468}
469
470impl fmt::Display for WorkspaceTarget {
471    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
472        match self {
473            WorkspaceTarget::Snapshot => write!(f, "SNAPSHOT"),
474            WorkspaceTarget::Local => write!(f, "LOCAL"),
475        }
476    }
477}
478
479impl fmt::Display for Value {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        match self {
482            Value::String(s) => write!(f, "\"{}\"", s),
483            Value::Int(i) => write!(f, "{}", i),
484            Value::List(items) => {
485                write!(f, "[")?;
486                for (i, item) in items.iter().enumerate() {
487                    if i > 0 {
488                        write!(f, ", ")?;
489                    }
490                    write!(f, "{}", item)?;
491                }
492                write!(f, "]")
493            }
494            Value::Map(map) => {
495                write!(f, "{{")?;
496                for (i, (k, v)) in map.iter().enumerate() {
497                    if i > 0 {
498                        write!(f, ", ")?;
499                    }
500                    write!(f, "{}: {}", k, v)?;
501                }
502                write!(f, "}}")
503            }
504            Value::Bool(b) => write!(f, "{}", b),
505            Value::TaskHandle(id) => write!(f, "task#{}", id),
506        }
507    }
508}
509
510impl fmt::Display for Expr {
511    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
512        match self {
513            Expr::Literal(v) => write!(f, "{}", v),
514            Expr::Var(name) => write!(f, "${}", name),
515            Expr::KeyPath { base, keys } => {
516                write!(f, "${}", base)?;
517                for key in keys {
518                    write!(f, ".{}", key)?;
519                }
520                Ok(())
521            }
522            Expr::Call { name, args } => {
523                write!(f, "{}(", name)?;
524                for (i, arg) in args.iter().enumerate() {
525                    if i > 0 {
526                        write!(f, ", ")?;
527                    }
528                    write!(f, "{}", arg)?;
529                }
530                write!(f, ")")
531            }
532            Expr::List(items) => {
533                write!(f, "[")?;
534                for (i, item) in items.iter().enumerate() {
535                    if i > 0 {
536                        write!(f, ", ")?;
537                    }
538                    write!(f, "{}", item)?;
539                }
540                write!(f, "]")
541            }
542            Expr::Map(entries) => {
543                write!(f, "{{")?;
544                for (i, (key, val)) in entries.iter().enumerate() {
545                    if i > 0 {
546                        write!(f, ", ")?;
547                    }
548                    write!(f, "\"{}\": {}", key, val)?;
549                }
550                write!(f, "}}")
551            }
552            Expr::Compare { op, left, right } => {
553                write!(f, "{} {} {}", left, op, right)
554            }
555            Expr::Not(inner) => {
556                // Parenthesize compound operands so Display round-trips:
557                // `!(a == b)` must not render as `!a == b` (= `(!a) == b`).
558                match inner.as_ref() {
559                    Expr::Compare { .. } => write!(f, "!({})", inner),
560                    _ => write!(f, "!{}", inner),
561                }
562            }
563            Expr::Logical { op, left, right } => {
564                write!(f, "({} {} {})", left, op, right)
565            }
566        }
567    }
568}
569
570impl fmt::Display for CompareOp {
571    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
572        match self {
573            CompareOp::Eq => write!(f, "=="),
574            CompareOp::Ne => write!(f, "!="),
575        }
576    }
577}
578
579impl fmt::Display for LogicalOp {
580    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
581        match self {
582            LogicalOp::And => write!(f, "&&"),
583            LogicalOp::Or => write!(f, "||"),
584        }
585    }
586}
587
588enum GuardDisplayContext {
589    Root,
590    InAnyArg,
591    InNot,
592    InAll,
593}
594
595impl GuardExpr {
596    fn fmt_with_ctx(&self, f: &mut fmt::Formatter<'_>, ctx: GuardDisplayContext) -> fmt::Result {
597        match self {
598            GuardExpr::Predicate(guard) => write!(f, "{}", guard),
599            GuardExpr::All(children) => {
600                let wrap = matches!(
601                    ctx,
602                    GuardDisplayContext::InAnyArg | GuardDisplayContext::InNot
603                ) && children.len() > 1;
604                if wrap {
605                    write!(f, "(")?;
606                }
607                for (i, child) in children.iter().enumerate() {
608                    if i > 0 {
609                        write!(f, ", ")?;
610                    }
611                    child.fmt_with_ctx(f, GuardDisplayContext::InAll)?;
612                }
613                if wrap {
614                    write!(f, ")")?;
615                }
616                Ok(())
617            }
618            GuardExpr::Or(children) => {
619                write!(f, "any(")?;
620                for (i, child) in children.iter().enumerate() {
621                    if i > 0 {
622                        write!(f, ", ")?;
623                    }
624                    child.fmt_with_ctx(f, GuardDisplayContext::InAnyArg)?;
625                }
626                write!(f, ")")
627            }
628            GuardExpr::Not(child) => {
629                write!(f, "not(")?;
630                child.fmt_with_ctx(f, GuardDisplayContext::InNot)?;
631                write!(f, ")")
632            }
633        }
634    }
635}
636
637impl fmt::Display for GuardExpr {
638    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639        self.fmt_with_ctx(f, GuardDisplayContext::Root)
640    }
641}
642
643impl fmt::Display for Step {
644    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
645        if let Some(expr) = &self.guard {
646            write!(f, "[{}] ", expr)?;
647        }
648        write!(f, "{}", self.kind)
649    }
650}