Skip to main content

kaish_kernel/interpreter/
eval.rs

1//! Expression evaluation for kaish.
2//!
3//! The evaluator takes AST expressions and reduces them to values.
4//! Variable references are resolved through the Scope, and string
5//! interpolation is expanded.
6//!
7//! Command substitution (`$(pipeline)`) requires an executor, which is
8//! provided by higher layers (L6: Pipes & Jobs).
9
10use std::fmt;
11
12use crate::arithmetic;
13use crate::ast::{BinaryOp, Expr, FileTestOp, Pipeline, StringPart, StringTestOp, TestCmpOp, TestExpr, Value, VarPath};
14use crate::vfs::DirEntry;
15use std::path::Path;
16
17use super::result::ExecResult;
18use super::scope::Scope;
19
20/// Errors that can occur during expression evaluation.
21#[derive(Debug, Clone, PartialEq)]
22pub enum EvalError {
23    /// Variable not found in scope.
24    UndefinedVariable(String),
25    /// Path resolution failed (bad field/index access).
26    InvalidPath(String),
27    /// Type mismatch for operation.
28    TypeError { expected: &'static str, got: String },
29    /// Command substitution failed.
30    CommandFailed(String),
31    /// No executor available for command substitution.
32    NoExecutor,
33    /// Division by zero or similar arithmetic error.
34    ArithmeticError(String),
35    /// Invalid regex pattern.
36    RegexError(String),
37}
38
39impl fmt::Display for EvalError {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        match self {
42            EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
43            EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
44            EvalError::TypeError { expected, got } => {
45                write!(f, "type error: expected {expected}, got {got}")
46            }
47            EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
48            EvalError::NoExecutor => write!(f, "no executor available for command substitution"),
49            EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
50            EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
51        }
52    }
53}
54
55impl std::error::Error for EvalError {}
56
57/// Result type for evaluation.
58pub type EvalResult<T> = Result<T, EvalError>;
59
60/// Trait for executing pipelines (command substitution).
61///
62/// This is implemented by higher layers (L6: Pipes & Jobs) to provide
63/// actual command execution. The evaluator calls this when it encounters
64/// a `$(pipeline)` expression.
65pub trait Executor {
66    /// Execute a pipeline and return its result.
67    ///
68    /// The executor should:
69    /// 1. Parse and execute the pipeline
70    /// 2. Capture stdout/stderr
71    /// 3. Return an ExecResult with code, output, and parsed data
72    fn execute(&mut self, pipeline: &Pipeline, scope: &mut Scope) -> EvalResult<ExecResult>;
73
74    /// Stat a file path through the VFS.
75    ///
76    /// Returns `Some(entry)` if the path exists, `None` otherwise.
77    /// Used by `[[ -d path ]]`, `[[ -f path ]]`, etc.
78    ///
79    /// Default: falls back to `std::fs::metadata` (bypasses VFS).
80    fn file_stat(&self, path: &Path) -> Option<DirEntry> {
81        std::fs::metadata(path).ok().map(|meta| {
82            if meta.is_dir() {
83                DirEntry::directory(path.file_name().unwrap_or_default().to_string_lossy())
84            } else {
85                let mut entry = DirEntry::file(
86                    path.file_name().unwrap_or_default().to_string_lossy(),
87                    meta.len(),
88                );
89                #[cfg(unix)]
90                {
91                    use std::os::unix::fs::PermissionsExt;
92                    entry.permissions = Some(meta.permissions().mode());
93                }
94                entry
95            }
96        })
97    }
98}
99
100/// A stub executor that always returns an error.
101///
102/// Used in L3 before the full executor is available.
103pub struct NoOpExecutor;
104
105impl Executor for NoOpExecutor {
106    fn execute(&mut self, _pipeline: &Pipeline, _scope: &mut Scope) -> EvalResult<ExecResult> {
107        Err(EvalError::NoExecutor)
108    }
109}
110
111/// Expression evaluator.
112///
113/// Evaluates AST expressions to values, using the provided scope for
114/// variable lookup and the executor for command substitution.
115pub struct Evaluator<'a, E: Executor> {
116    scope: &'a mut Scope,
117    executor: &'a mut E,
118}
119
120impl<'a, E: Executor> Evaluator<'a, E> {
121    /// Create a new evaluator with the given scope and executor.
122    pub fn new(scope: &'a mut Scope, executor: &'a mut E) -> Self {
123        Self { scope, executor }
124    }
125
126    /// Evaluate an expression to a value.
127    pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
128        match expr {
129            Expr::Literal(value) => self.eval_literal(value),
130            Expr::VarRef(path) => self.eval_var_ref(path),
131            Expr::Interpolated(parts) => self.eval_interpolated(parts),
132            Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
133            Expr::CommandSubst(pipeline) => self.eval_command_subst(pipeline),
134            Expr::Test(test_expr) => self.eval_test(test_expr),
135            Expr::Positional(n) => self.eval_positional(*n),
136            Expr::AllArgs => self.eval_all_args(),
137            Expr::ArgCount => self.eval_arg_count(),
138            Expr::VarLength(name) => self.eval_var_length(name),
139            Expr::VarWithDefault { name, default } => self.eval_var_with_default(name, default),
140            Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
141            Expr::Command(cmd) => self.eval_command(cmd),
142            Expr::LastExitCode => self.eval_last_exit_code(),
143            Expr::CurrentPid => self.eval_current_pid(),
144        }
145    }
146
147    /// Evaluate last exit code ($?).
148    fn eval_last_exit_code(&self) -> EvalResult<Value> {
149        Ok(Value::Int(self.scope.last_result().code))
150    }
151
152    /// Evaluate current shell PID ($$).
153    fn eval_current_pid(&self) -> EvalResult<Value> {
154        Ok(Value::Int(self.scope.pid() as i64))
155    }
156
157    /// Evaluate a command as a condition (exit code determines truthiness).
158    fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
159        // Special-case true/false builtins - they have well-known return values
160        // and don't need an executor to evaluate. Like real shells, any args are ignored.
161        match cmd.name.as_str() {
162            "true" => return Ok(Value::Bool(true)),
163            "false" => return Ok(Value::Bool(false)),
164            _ => {}
165        }
166
167        // For other commands, create a single-command pipeline to execute
168        let pipeline = crate::ast::Pipeline {
169            commands: vec![cmd.clone()],
170            background: false,
171        };
172        let result = self.executor.execute(&pipeline, self.scope)?;
173        // Exit code 0 = true, non-zero = false
174        Ok(Value::Bool(result.code == 0))
175    }
176
177    /// Evaluate arithmetic expansion: `$((expr))`
178    fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
179        arithmetic::eval_arithmetic(expr_str, self.scope)
180            .map(Value::Int)
181            .map_err(|e| EvalError::ArithmeticError(e.to_string()))
182    }
183
184    /// Evaluate a test expression `[[ ... ]]` to a boolean value.
185    fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
186        let result = match test_expr {
187            TestExpr::FileTest { op, path } => {
188                let path_value = self.eval(path)?;
189                let path_str = value_to_string(&path_value);
190                let path = Path::new(&path_str);
191                let entry = self.executor.file_stat(path);
192                match op {
193                    FileTestOp::Exists => entry.is_some(),
194                    FileTestOp::IsFile => entry.as_ref().is_some_and(|e| e.is_file()),
195                    FileTestOp::IsDir => entry.as_ref().is_some_and(|e| e.is_dir()),
196                    FileTestOp::Readable => entry.is_some(),
197                    FileTestOp::Writable => entry.as_ref().is_some_and(|e| {
198                        e.permissions.is_none_or(|p| p & 0o222 != 0)
199                    }),
200                    FileTestOp::Executable => entry.as_ref().is_some_and(|e| {
201                        e.permissions.is_some_and(|p| p & 0o111 != 0)
202                    }),
203                }
204            }
205            TestExpr::StringTest { op, value } => {
206                let val = self.eval(value)?;
207                let s = value_to_string(&val);
208                match op {
209                    StringTestOp::IsEmpty => s.is_empty(),
210                    StringTestOp::IsNonEmpty => !s.is_empty(),
211                }
212            }
213            TestExpr::Comparison { left, op, right } => {
214                let left_val = self.eval(left)?;
215                let right_val = self.eval(right)?;
216
217                match op {
218                    TestCmpOp::Eq => values_equal(&left_val, &right_val),
219                    TestCmpOp::NotEq => !values_equal(&left_val, &right_val),
220                    TestCmpOp::Match => {
221                        // Regex match
222                        match regex_match(&left_val, &right_val, false) {
223                            Ok(Value::Bool(b)) => b,
224                            Ok(_) => false,
225                            Err(_) => false,
226                        }
227                    }
228                    TestCmpOp::NotMatch => {
229                        // Regex not match
230                        match regex_match(&left_val, &right_val, true) {
231                            Ok(Value::Bool(b)) => b,
232                            Ok(_) => true,
233                            Err(_) => true,
234                        }
235                    }
236                    TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
237                        // Ordered comparison (works for strings and numbers)
238                        // Type mismatches are errors - no silent coercion
239                        let ord = compare_values(&left_val, &right_val)?;
240                        match op {
241                            TestCmpOp::Gt => ord.is_gt(),
242                            TestCmpOp::Lt => ord.is_lt(),
243                            TestCmpOp::GtEq => ord.is_ge(),
244                            TestCmpOp::LtEq => ord.is_le(),
245                            _ => unreachable!(),
246                        }
247                    }
248                }
249            }
250            TestExpr::And { left, right } => {
251                // Short-circuit evaluation: evaluate left first
252                let left_result = self.eval_test(left)?;
253                if !value_to_bool(&left_result) {
254                    false // Short-circuit: left is false, don't evaluate right
255                } else {
256                    value_to_bool(&self.eval_test(right)?)
257                }
258            }
259            TestExpr::Or { left, right } => {
260                // Short-circuit evaluation: evaluate left first
261                let left_result = self.eval_test(left)?;
262                if value_to_bool(&left_result) {
263                    true // Short-circuit: left is true, don't evaluate right
264                } else {
265                    value_to_bool(&self.eval_test(right)?)
266                }
267            }
268            TestExpr::Not { expr } => {
269                let result = self.eval_test(expr)?;
270                !value_to_bool(&result)
271            }
272        };
273        Ok(Value::Bool(result))
274    }
275
276    /// Evaluate a literal value.
277    fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
278        Ok(value.clone())
279    }
280
281    /// Evaluate a variable reference.
282    fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
283        self.scope
284            .resolve_path(path)
285            .ok_or_else(|| EvalError::InvalidPath(format_path(path)))
286    }
287
288    /// Evaluate a positional parameter ($0-$9).
289    fn eval_positional(&self, n: usize) -> EvalResult<Value> {
290        match self.scope.get_positional(n) {
291            Some(s) => Ok(Value::String(s.to_string())),
292            None => Ok(Value::String(String::new())), // Unset positional returns empty string
293        }
294    }
295
296    /// Evaluate all arguments ($@).
297    ///
298    /// Returns a space-separated string of all positional arguments (POSIX-style).
299    fn eval_all_args(&self) -> EvalResult<Value> {
300        let args = self.scope.all_args();
301        Ok(Value::String(args.join(" ")))
302    }
303
304    /// Evaluate argument count ($#).
305    fn eval_arg_count(&self) -> EvalResult<Value> {
306        Ok(Value::Int(self.scope.arg_count() as i64))
307    }
308
309    /// Evaluate variable string length (${#VAR}).
310    fn eval_var_length(&self, name: &str) -> EvalResult<Value> {
311        match self.scope.get(name) {
312            Some(value) => {
313                let s = value_to_string(value);
314                Ok(Value::Int(s.len() as i64))
315            }
316            None => Ok(Value::Int(0)), // Unset variable has length 0
317        }
318    }
319
320    /// Evaluate variable with default (${VAR:-default}).
321    /// Returns the variable value if set and non-empty, otherwise evaluates the default parts.
322    fn eval_var_with_default(&mut self, name: &str, default: &[StringPart]) -> EvalResult<Value> {
323        match self.scope.get(name) {
324            Some(value) => {
325                let s = value_to_string(value);
326                if s.is_empty() {
327                    // Variable is set but empty, evaluate the default parts
328                    self.eval_interpolated(default)
329                } else {
330                    Ok(value.clone())
331                }
332            }
333            None => {
334                // Variable is unset, evaluate the default parts
335                self.eval_interpolated(default)
336            }
337        }
338    }
339
340    /// Evaluate an interpolated string.
341    fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
342        let mut result = String::new();
343        for part in parts {
344            match part {
345                StringPart::Literal(s) => result.push_str(s),
346                StringPart::Var(path) => {
347                    // Unset variables expand to empty string (bash-compatible)
348                    if let Some(value) = self.scope.resolve_path(path) {
349                        result.push_str(&value_to_string(&value));
350                    }
351                }
352                StringPart::VarWithDefault { name, default } => {
353                    let value = self.eval_var_with_default(name, default)?;
354                    result.push_str(&value_to_string(&value));
355                }
356                StringPart::VarLength(name) => {
357                    let value = self.eval_var_length(name)?;
358                    result.push_str(&value_to_string(&value));
359                }
360                StringPart::Positional(n) => {
361                    let value = self.eval_positional(*n)?;
362                    result.push_str(&value_to_string(&value));
363                }
364                StringPart::AllArgs => {
365                    let value = self.eval_all_args()?;
366                    result.push_str(&value_to_string(&value));
367                }
368                StringPart::ArgCount => {
369                    let value = self.eval_arg_count()?;
370                    result.push_str(&value_to_string(&value));
371                }
372                StringPart::Arithmetic(expr) => {
373                    // Parse and evaluate the arithmetic expression
374                    let value = self.eval_arithmetic_string(expr)?;
375                    result.push_str(&value_to_string(&value));
376                }
377                StringPart::CommandSubst(pipeline) => {
378                    // Execute the pipeline and capture its output
379                    let value = self.eval_command_subst(pipeline)?;
380                    result.push_str(&value_to_string(&value));
381                }
382                StringPart::LastExitCode => {
383                    result.push_str(&self.scope.last_result().code.to_string());
384                }
385                StringPart::CurrentPid => {
386                    result.push_str(&self.scope.pid().to_string());
387                }
388            }
389        }
390        Ok(Value::String(result))
391    }
392
393    /// Evaluate an arithmetic string expression (from `$((expr))` in interpolation).
394    fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
395        // Use the existing arithmetic evaluator
396        arithmetic::eval_arithmetic(expr, self.scope)
397            .map(Value::Int)
398            .map_err(|e| EvalError::ArithmeticError(e.to_string()))
399    }
400
401    /// Evaluate a binary operation.
402    fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
403        match op {
404            // Short-circuit logical operators
405            BinaryOp::And => {
406                let left_val = self.eval(left)?;
407                if !is_truthy(&left_val) {
408                    return Ok(left_val);
409                }
410                self.eval(right)
411            }
412            BinaryOp::Or => {
413                let left_val = self.eval(left)?;
414                if is_truthy(&left_val) {
415                    return Ok(left_val);
416                }
417                self.eval(right)
418            }
419            // Comparison operators
420            BinaryOp::Eq => {
421                let left_val = self.eval(left)?;
422                let right_val = self.eval(right)?;
423                Ok(Value::Bool(values_equal(&left_val, &right_val)))
424            }
425            BinaryOp::NotEq => {
426                let left_val = self.eval(left)?;
427                let right_val = self.eval(right)?;
428                Ok(Value::Bool(!values_equal(&left_val, &right_val)))
429            }
430            BinaryOp::Lt => {
431                let left_val = self.eval(left)?;
432                let right_val = self.eval(right)?;
433                compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_lt()))
434            }
435            BinaryOp::Gt => {
436                let left_val = self.eval(left)?;
437                let right_val = self.eval(right)?;
438                compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_gt()))
439            }
440            BinaryOp::LtEq => {
441                let left_val = self.eval(left)?;
442                let right_val = self.eval(right)?;
443                compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_le()))
444            }
445            BinaryOp::GtEq => {
446                let left_val = self.eval(left)?;
447                let right_val = self.eval(right)?;
448                compare_values(&left_val, &right_val).map(|ord| Value::Bool(ord.is_ge()))
449            }
450            // Regex match operators
451            BinaryOp::Match => {
452                let left_val = self.eval(left)?;
453                let right_val = self.eval(right)?;
454                regex_match(&left_val, &right_val, false)
455            }
456            BinaryOp::NotMatch => {
457                let left_val = self.eval(left)?;
458                let right_val = self.eval(right)?;
459                regex_match(&left_val, &right_val, true)
460            }
461        }
462    }
463
464    /// Evaluate command substitution.
465    fn eval_command_subst(&mut self, pipeline: &Pipeline) -> EvalResult<Value> {
466        let result = self.executor.execute(pipeline, self.scope)?;
467
468        // Update $? with the result
469        self.scope.set_last_result(result.clone());
470
471        // Return the result as a value (the result object itself)
472        // The caller can access .ok, .data, etc.
473        Ok(result_to_value(&result))
474    }
475}
476
477/// Convert a Value to its string representation for interpolation.
478pub fn value_to_string(value: &Value) -> String {
479    match value {
480        Value::Null => "null".to_string(),
481        Value::Bool(b) => b.to_string(),
482        Value::Int(i) => i.to_string(),
483        Value::Float(f) => f.to_string(),
484        Value::String(s) => s.clone(),
485        Value::Json(json) => json.to_string(),
486        Value::Blob(blob) => format!("[blob: {} {}]", blob.formatted_size(), blob.content_type),
487    }
488}
489
490/// Convert a Value to its boolean representation.
491///
492/// - `Bool(b)` → `b`
493/// - `Int(0)` → `false`, other ints → `true`
494/// - `String("")` → `false`, non-empty → `true`
495/// - `Null` → `false`
496/// - `Float(0.0)` → `false`, other floats → `true`
497/// - `Json(null)` → `false`, `Json([])` → `false`, `Json({})` → `false`, others → `true`
498/// - `Blob(_)` → `true` (blobs always exist if referenced)
499pub fn value_to_bool(value: &Value) -> bool {
500    match value {
501        Value::Null => false,
502        Value::Bool(b) => *b,
503        Value::Int(i) => *i != 0,
504        Value::Float(f) => *f != 0.0,
505        Value::String(s) => !s.is_empty(),
506        Value::Json(json) => match json {
507            serde_json::Value::Null => false,
508            serde_json::Value::Array(arr) => !arr.is_empty(),
509            serde_json::Value::Object(obj) => !obj.is_empty(),
510            serde_json::Value::Bool(b) => *b,
511            serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
512            serde_json::Value::String(s) => !s.is_empty(),
513        },
514        Value::Blob(_) => true, // Blob references are always truthy
515    }
516}
517
518/// Expand tilde (~) to home directory.
519///
520/// - `~` alone → `$HOME`
521/// - `~/path` → `$HOME/path`
522/// - `~user` → user's home directory (Unix only, reads /etc/passwd)
523/// - `~user/path` → user's home directory + path
524/// - Other strings are returned unchanged.
525pub fn expand_tilde(s: &str) -> String {
526    if s == "~" {
527        std::env::var("HOME").unwrap_or_else(|_| "~".to_string())
528    } else if s.starts_with("~/") {
529        match std::env::var("HOME") {
530            Ok(home) => format!("{}{}", home, &s[1..]),
531            Err(_) => s.to_string(),
532        }
533    } else if s.starts_with('~') {
534        // Try ~user expansion
535        expand_tilde_user(s)
536    } else {
537        s.to_string()
538    }
539}
540
541/// Expand ~user to the user's home directory by reading /etc/passwd.
542#[cfg(unix)]
543fn expand_tilde_user(s: &str) -> String {
544    // Extract username from ~user or ~user/path
545    let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
546        (&s[1..slash_pos + 1], &s[slash_pos + 1..])
547    } else {
548        (&s[1..], "")
549    };
550
551    if username.is_empty() {
552        return s.to_string();
553    }
554
555    // Look up user's home directory by reading /etc/passwd
556    // Format: username:x:uid:gid:gecos:home:shell
557    let passwd = match std::fs::read_to_string("/etc/passwd") {
558        Ok(content) => content,
559        Err(_) => return s.to_string(),
560    };
561
562    for line in passwd.lines() {
563        let fields: Vec<&str> = line.split(':').collect();
564        if fields.len() >= 6 && fields[0] == username {
565            let home_dir = fields[5];
566            return if rest.is_empty() {
567                home_dir.to_string()
568            } else {
569                format!("{}{}", home_dir, rest)
570            };
571        }
572    }
573
574    // User not found, return unchanged
575    s.to_string()
576}
577
578#[cfg(not(unix))]
579fn expand_tilde_user(s: &str) -> String {
580    // ~user expansion not supported on non-Unix
581    s.to_string()
582}
583
584/// Convert a Value to its string representation, with tilde expansion for paths.
585pub fn value_to_string_with_tilde(value: &Value) -> String {
586    match value {
587        Value::String(s) if s.starts_with('~') => expand_tilde(s),
588        _ => value_to_string(value),
589    }
590}
591
592/// Format a VarPath for error messages.
593fn format_path(path: &VarPath) -> String {
594    use crate::ast::VarSegment;
595    let mut result = String::from("${");
596    for (i, seg) in path.segments.iter().enumerate() {
597        match seg {
598            VarSegment::Field(name) => {
599                if i > 0 {
600                    result.push('.');
601                }
602                result.push_str(name);
603            }
604        }
605    }
606    result.push('}');
607    result
608}
609
610/// Check if a value is "truthy" for boolean operations.
611///
612/// - `null` → false
613/// - `false` → false
614/// - `0` → false
615/// - `""` → false
616/// - `Json(null)`, `Json([])`, `Json({})` → false
617/// - `Blob(_)` → true
618/// - Everything else → true
619fn is_truthy(value: &Value) -> bool {
620    // Delegate to value_to_bool for consistent behavior
621    value_to_bool(value)
622}
623
624/// Check if two values are equal.
625///
626/// Handles cross-type comparisons for shell compatibility:
627/// - String-to-Int: Try parsing the string as an integer
628/// - String-to-Float: Try parsing the string as a float
629/// - Json: Deep equality comparison
630/// - Blob: Compare by id
631fn values_equal(left: &Value, right: &Value) -> bool {
632    match (left, right) {
633        (Value::Null, Value::Null) => true,
634        (Value::Bool(a), Value::Bool(b)) => a == b,
635        (Value::Int(a), Value::Int(b)) => a == b,
636        (Value::Float(a), Value::Float(b)) => (a - b).abs() < f64::EPSILON,
637        (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
638            (*a as f64 - b).abs() < f64::EPSILON
639        }
640        (Value::String(a), Value::String(b)) => a == b,
641        // String-Int comparison: try to parse string as integer
642        (Value::String(s), Value::Int(n)) | (Value::Int(n), Value::String(s)) => {
643            s.parse::<i64>().map(|parsed| parsed == *n).unwrap_or(false)
644        }
645        // String-Float comparison: try to parse string as float
646        (Value::String(s), Value::Float(f)) | (Value::Float(f), Value::String(s)) => {
647            s.parse::<f64>().map(|parsed| (parsed - f).abs() < f64::EPSILON).unwrap_or(false)
648        }
649        // Json deep equality
650        (Value::Json(a), Value::Json(b)) => a == b,
651        // Blob equality by id
652        (Value::Blob(a), Value::Blob(b)) => a.id == b.id,
653        _ => false,
654    }
655}
656
657/// Compare two values for ordering.
658fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
659    match (left, right) {
660        (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
661        (Value::Float(a), Value::Float(b)) => {
662            a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
663        }
664        (Value::Int(a), Value::Float(b)) => {
665            (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
666        }
667        (Value::Float(a), Value::Int(b)) => {
668            a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
669        }
670        (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
671        _ => Err(EvalError::TypeError {
672            expected: "comparable types (numbers or strings)",
673            got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
674        }),
675    }
676}
677
678/// Get a human-readable type name for a value.
679fn type_name(value: &Value) -> &'static str {
680    match value {
681        Value::Null => "null",
682        Value::Bool(_) => "bool",
683        Value::Int(_) => "int",
684        Value::Float(_) => "float",
685        Value::String(_) => "string",
686        Value::Json(_) => "json",
687        Value::Blob(_) => "blob",
688    }
689}
690
691/// Convert an ExecResult to a Value for command substitution return.
692///
693/// Prefers structured data if available (for iteration in for loops),
694/// otherwise returns stdout (trimmed) as a string.
695/// Access to other result fields (code, err, etc.) is via ${?.field}.
696fn result_to_value(result: &ExecResult) -> Value {
697    // Prefer structured data if available (enables `for i in $(cmd)` iteration)
698    if let Some(data) = &result.data {
699        return data.clone();
700    }
701    // Otherwise return stdout as single string (NO implicit splitting)
702    Value::String(result.out.trim_end().to_string())
703}
704
705/// Perform regex match or not-match on two values.
706///
707/// The left operand is the string to match against.
708/// The right operand is the regex pattern.
709fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
710    let text = match left {
711        Value::String(s) => s.as_str(),
712        _ => {
713            return Err(EvalError::TypeError {
714                expected: "string",
715                got: type_name(left).to_string(),
716            })
717        }
718    };
719
720    let pattern = match right {
721        Value::String(s) => s.as_str(),
722        _ => {
723            return Err(EvalError::TypeError {
724                expected: "string (regex pattern)",
725                got: type_name(right).to_string(),
726            })
727        }
728    };
729
730    let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
731    let matches = re.is_match(text);
732
733    Ok(Value::Bool(if negate { !matches } else { matches }))
734}
735
736/// Convenience function to evaluate an expression with a scope.
737///
738/// Uses NoOpExecutor, so command substitution will fail.
739pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
740    let mut executor = NoOpExecutor;
741    let mut evaluator = Evaluator::new(scope, &mut executor);
742    evaluator.eval(expr)
743}
744
745#[cfg(test)]
746mod tests {
747    use super::*;
748    use crate::ast::VarSegment;
749
750    // Helper to create a simple variable expression
751    fn var_expr(name: &str) -> Expr {
752        Expr::VarRef(VarPath::simple(name))
753    }
754
755    #[test]
756    fn eval_literal_int() {
757        let mut scope = Scope::new();
758        let expr = Expr::Literal(Value::Int(42));
759        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
760    }
761
762    #[test]
763    fn eval_literal_string() {
764        let mut scope = Scope::new();
765        let expr = Expr::Literal(Value::String("hello".into()));
766        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
767    }
768
769    #[test]
770    fn eval_literal_bool() {
771        let mut scope = Scope::new();
772        assert_eq!(
773            eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
774            Ok(Value::Bool(true))
775        );
776    }
777
778    #[test]
779    fn eval_literal_null() {
780        let mut scope = Scope::new();
781        assert_eq!(
782            eval_expr(&Expr::Literal(Value::Null), &mut scope),
783            Ok(Value::Null)
784        );
785    }
786
787    #[test]
788    fn eval_literal_float() {
789        let mut scope = Scope::new();
790        let expr = Expr::Literal(Value::Float(3.14));
791        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
792    }
793
794    #[test]
795    fn eval_variable_ref() {
796        let mut scope = Scope::new();
797        scope.set("X", Value::Int(100));
798        assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
799    }
800
801    #[test]
802    fn eval_undefined_variable() {
803        let mut scope = Scope::new();
804        let result = eval_expr(&var_expr("MISSING"), &mut scope);
805        assert!(matches!(result, Err(EvalError::InvalidPath(_))));
806    }
807
808    #[test]
809    fn eval_interpolated_string() {
810        let mut scope = Scope::new();
811        scope.set("NAME", Value::String("World".into()));
812
813        let expr = Expr::Interpolated(vec![
814            StringPart::Literal("Hello, ".into()),
815            StringPart::Var(VarPath::simple("NAME")),
816            StringPart::Literal("!".into()),
817        ]);
818        assert_eq!(
819            eval_expr(&expr, &mut scope),
820            Ok(Value::String("Hello, World!".into()))
821        );
822    }
823
824    #[test]
825    fn eval_interpolated_with_number() {
826        let mut scope = Scope::new();
827        scope.set("COUNT", Value::Int(42));
828
829        let expr = Expr::Interpolated(vec![
830            StringPart::Literal("Count: ".into()),
831            StringPart::Var(VarPath::simple("COUNT")),
832        ]);
833        assert_eq!(
834            eval_expr(&expr, &mut scope),
835            Ok(Value::String("Count: 42".into()))
836        );
837    }
838
839    #[test]
840    fn eval_and_short_circuit_true() {
841        let mut scope = Scope::new();
842        let expr = Expr::BinaryOp {
843            left: Box::new(Expr::Literal(Value::Bool(true))),
844            op: BinaryOp::And,
845            right: Box::new(Expr::Literal(Value::Int(42))),
846        };
847        // true && 42 => 42 (returns right operand)
848        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
849    }
850
851    #[test]
852    fn eval_and_short_circuit_false() {
853        let mut scope = Scope::new();
854        let expr = Expr::BinaryOp {
855            left: Box::new(Expr::Literal(Value::Bool(false))),
856            op: BinaryOp::And,
857            right: Box::new(Expr::Literal(Value::Int(42))),
858        };
859        // false && 42 => false (returns left operand, short-circuits)
860        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
861    }
862
863    #[test]
864    fn eval_or_short_circuit_true() {
865        let mut scope = Scope::new();
866        let expr = Expr::BinaryOp {
867            left: Box::new(Expr::Literal(Value::Bool(true))),
868            op: BinaryOp::Or,
869            right: Box::new(Expr::Literal(Value::Int(42))),
870        };
871        // true || 42 => true (returns left operand, short-circuits)
872        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
873    }
874
875    #[test]
876    fn eval_or_short_circuit_false() {
877        let mut scope = Scope::new();
878        let expr = Expr::BinaryOp {
879            left: Box::new(Expr::Literal(Value::Bool(false))),
880            op: BinaryOp::Or,
881            right: Box::new(Expr::Literal(Value::Int(42))),
882        };
883        // false || 42 => 42 (returns right operand)
884        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
885    }
886
887    #[test]
888    fn eval_equality() {
889        let mut scope = Scope::new();
890        let expr = Expr::BinaryOp {
891            left: Box::new(Expr::Literal(Value::Int(5))),
892            op: BinaryOp::Eq,
893            right: Box::new(Expr::Literal(Value::Int(5))),
894        };
895        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
896    }
897
898    #[test]
899    fn eval_inequality() {
900        let mut scope = Scope::new();
901        let expr = Expr::BinaryOp {
902            left: Box::new(Expr::Literal(Value::Int(5))),
903            op: BinaryOp::NotEq,
904            right: Box::new(Expr::Literal(Value::Int(3))),
905        };
906        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
907    }
908
909    #[test]
910    fn eval_less_than() {
911        let mut scope = Scope::new();
912        let expr = Expr::BinaryOp {
913            left: Box::new(Expr::Literal(Value::Int(3))),
914            op: BinaryOp::Lt,
915            right: Box::new(Expr::Literal(Value::Int(5))),
916        };
917        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
918    }
919
920    #[test]
921    fn eval_greater_than() {
922        let mut scope = Scope::new();
923        let expr = Expr::BinaryOp {
924            left: Box::new(Expr::Literal(Value::Int(5))),
925            op: BinaryOp::Gt,
926            right: Box::new(Expr::Literal(Value::Int(3))),
927        };
928        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
929    }
930
931    #[test]
932    fn eval_less_than_or_equal() {
933        let mut scope = Scope::new();
934        let eq = Expr::BinaryOp {
935            left: Box::new(Expr::Literal(Value::Int(5))),
936            op: BinaryOp::LtEq,
937            right: Box::new(Expr::Literal(Value::Int(5))),
938        };
939        let lt = Expr::BinaryOp {
940            left: Box::new(Expr::Literal(Value::Int(3))),
941            op: BinaryOp::LtEq,
942            right: Box::new(Expr::Literal(Value::Int(5))),
943        };
944        assert_eq!(eval_expr(&eq, &mut scope), Ok(Value::Bool(true)));
945        assert_eq!(eval_expr(&lt, &mut scope), Ok(Value::Bool(true)));
946    }
947
948    #[test]
949    fn eval_greater_than_or_equal() {
950        let mut scope = Scope::new();
951        let eq = Expr::BinaryOp {
952            left: Box::new(Expr::Literal(Value::Int(5))),
953            op: BinaryOp::GtEq,
954            right: Box::new(Expr::Literal(Value::Int(5))),
955        };
956        let gt = Expr::BinaryOp {
957            left: Box::new(Expr::Literal(Value::Int(7))),
958            op: BinaryOp::GtEq,
959            right: Box::new(Expr::Literal(Value::Int(5))),
960        };
961        assert_eq!(eval_expr(&eq, &mut scope), Ok(Value::Bool(true)));
962        assert_eq!(eval_expr(&gt, &mut scope), Ok(Value::Bool(true)));
963    }
964
965    #[test]
966    fn eval_string_comparison() {
967        let mut scope = Scope::new();
968        let expr = Expr::BinaryOp {
969            left: Box::new(Expr::Literal(Value::String("apple".into()))),
970            op: BinaryOp::Lt,
971            right: Box::new(Expr::Literal(Value::String("banana".into()))),
972        };
973        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
974    }
975
976    #[test]
977    fn eval_mixed_int_float_comparison() {
978        let mut scope = Scope::new();
979        let expr = Expr::BinaryOp {
980            left: Box::new(Expr::Literal(Value::Int(3))),
981            op: BinaryOp::Lt,
982            right: Box::new(Expr::Literal(Value::Float(3.5))),
983        };
984        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
985    }
986
987    #[test]
988    fn eval_int_float_equality() {
989        let mut scope = Scope::new();
990        let expr = Expr::BinaryOp {
991            left: Box::new(Expr::Literal(Value::Int(5))),
992            op: BinaryOp::Eq,
993            right: Box::new(Expr::Literal(Value::Float(5.0))),
994        };
995        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
996    }
997
998    #[test]
999    fn eval_type_mismatch_comparison() {
1000        let mut scope = Scope::new();
1001        let expr = Expr::BinaryOp {
1002            left: Box::new(Expr::Literal(Value::Int(5))),
1003            op: BinaryOp::Lt,
1004            right: Box::new(Expr::Literal(Value::String("five".into()))),
1005        };
1006        assert!(matches!(eval_expr(&expr, &mut scope), Err(EvalError::TypeError { .. })));
1007    }
1008
1009    #[test]
1010    fn is_truthy_values() {
1011        assert!(!is_truthy(&Value::Null));
1012        assert!(!is_truthy(&Value::Bool(false)));
1013        assert!(is_truthy(&Value::Bool(true)));
1014        assert!(!is_truthy(&Value::Int(0)));
1015        assert!(is_truthy(&Value::Int(1)));
1016        assert!(is_truthy(&Value::Int(-1)));
1017        assert!(!is_truthy(&Value::Float(0.0)));
1018        assert!(is_truthy(&Value::Float(0.1)));
1019        assert!(!is_truthy(&Value::String("".into())));
1020        assert!(is_truthy(&Value::String("x".into())));
1021    }
1022
1023    #[test]
1024    fn eval_command_subst_fails_without_executor() {
1025        use crate::ast::{Command, Pipeline};
1026
1027        let mut scope = Scope::new();
1028        let pipeline = Pipeline {
1029            commands: vec![Command {
1030                name: "echo".into(),
1031                args: vec![],
1032                redirects: vec![],
1033            }],
1034            background: false,
1035        };
1036        let expr = Expr::CommandSubst(Box::new(pipeline));
1037
1038        assert!(matches!(
1039            eval_expr(&expr, &mut scope),
1040            Err(EvalError::NoExecutor)
1041        ));
1042    }
1043
1044    #[test]
1045    fn eval_last_result_field() {
1046        let mut scope = Scope::new();
1047        scope.set_last_result(ExecResult::failure(42, "test error"));
1048
1049        // ${?.code}
1050        let expr = Expr::VarRef(VarPath {
1051            segments: vec![
1052                VarSegment::Field("?".into()),
1053                VarSegment::Field("code".into()),
1054            ],
1055        });
1056        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1057
1058        // ${?.err}
1059        let expr = Expr::VarRef(VarPath {
1060            segments: vec![
1061                VarSegment::Field("?".into()),
1062                VarSegment::Field("err".into()),
1063            ],
1064        });
1065        assert_eq!(
1066            eval_expr(&expr, &mut scope),
1067            Ok(Value::String("test error".into()))
1068        );
1069    }
1070
1071    #[test]
1072    fn value_to_string_all_types() {
1073        assert_eq!(value_to_string(&Value::Null), "null");
1074        assert_eq!(value_to_string(&Value::Bool(true)), "true");
1075        assert_eq!(value_to_string(&Value::Int(42)), "42");
1076        assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1077        assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1078    }
1079
1080    // Additional comprehensive tests
1081
1082    #[test]
1083    fn eval_negative_int() {
1084        let mut scope = Scope::new();
1085        let expr = Expr::Literal(Value::Int(-42));
1086        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1087    }
1088
1089    #[test]
1090    fn eval_negative_float() {
1091        let mut scope = Scope::new();
1092        let expr = Expr::Literal(Value::Float(-3.14));
1093        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1094    }
1095
1096    #[test]
1097    fn eval_zero_values() {
1098        let mut scope = Scope::new();
1099        assert_eq!(
1100            eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1101            Ok(Value::Int(0))
1102        );
1103        assert_eq!(
1104            eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1105            Ok(Value::Float(0.0))
1106        );
1107    }
1108
1109    #[test]
1110    fn eval_interpolation_empty_var() {
1111        let mut scope = Scope::new();
1112        scope.set("EMPTY", Value::String("".into()));
1113
1114        let expr = Expr::Interpolated(vec![
1115            StringPart::Literal("prefix".into()),
1116            StringPart::Var(VarPath::simple("EMPTY")),
1117            StringPart::Literal("suffix".into()),
1118        ]);
1119        assert_eq!(
1120            eval_expr(&expr, &mut scope),
1121            Ok(Value::String("prefixsuffix".into()))
1122        );
1123    }
1124
1125    #[test]
1126    fn eval_chained_and() {
1127        let mut scope = Scope::new();
1128        // true && true && 42
1129        let expr = Expr::BinaryOp {
1130            left: Box::new(Expr::BinaryOp {
1131                left: Box::new(Expr::Literal(Value::Bool(true))),
1132                op: BinaryOp::And,
1133                right: Box::new(Expr::Literal(Value::Bool(true))),
1134            }),
1135            op: BinaryOp::And,
1136            right: Box::new(Expr::Literal(Value::Int(42))),
1137        };
1138        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1139    }
1140
1141    #[test]
1142    fn eval_chained_or() {
1143        let mut scope = Scope::new();
1144        // false || false || 42
1145        let expr = Expr::BinaryOp {
1146            left: Box::new(Expr::BinaryOp {
1147                left: Box::new(Expr::Literal(Value::Bool(false))),
1148                op: BinaryOp::Or,
1149                right: Box::new(Expr::Literal(Value::Bool(false))),
1150            }),
1151            op: BinaryOp::Or,
1152            right: Box::new(Expr::Literal(Value::Int(42))),
1153        };
1154        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1155    }
1156
1157    #[test]
1158    fn eval_mixed_and_or() {
1159        let mut scope = Scope::new();
1160        // true || false && false  (and binds tighter, but here we test explicit tree)
1161        // This tests: (true || false) && true
1162        let expr = Expr::BinaryOp {
1163            left: Box::new(Expr::BinaryOp {
1164                left: Box::new(Expr::Literal(Value::Bool(true))),
1165                op: BinaryOp::Or,
1166                right: Box::new(Expr::Literal(Value::Bool(false))),
1167            }),
1168            op: BinaryOp::And,
1169            right: Box::new(Expr::Literal(Value::Bool(true))),
1170        };
1171        // (true || false) = true, true && true = true
1172        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1173    }
1174
1175    #[test]
1176    fn eval_comparison_with_variables() {
1177        let mut scope = Scope::new();
1178        scope.set("X", Value::Int(10));
1179        scope.set("Y", Value::Int(5));
1180
1181        let expr = Expr::BinaryOp {
1182            left: Box::new(var_expr("X")),
1183            op: BinaryOp::Gt,
1184            right: Box::new(var_expr("Y")),
1185        };
1186        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1187    }
1188
1189    #[test]
1190    fn eval_string_equality() {
1191        let mut scope = Scope::new();
1192        let expr = Expr::BinaryOp {
1193            left: Box::new(Expr::Literal(Value::String("hello".into()))),
1194            op: BinaryOp::Eq,
1195            right: Box::new(Expr::Literal(Value::String("hello".into()))),
1196        };
1197        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1198    }
1199
1200    #[test]
1201    fn eval_string_inequality() {
1202        let mut scope = Scope::new();
1203        let expr = Expr::BinaryOp {
1204            left: Box::new(Expr::Literal(Value::String("hello".into()))),
1205            op: BinaryOp::NotEq,
1206            right: Box::new(Expr::Literal(Value::String("world".into()))),
1207        };
1208        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1209    }
1210
1211    #[test]
1212    fn eval_null_equality() {
1213        let mut scope = Scope::new();
1214        let expr = Expr::BinaryOp {
1215            left: Box::new(Expr::Literal(Value::Null)),
1216            op: BinaryOp::Eq,
1217            right: Box::new(Expr::Literal(Value::Null)),
1218        };
1219        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1220    }
1221
1222    #[test]
1223    fn eval_null_not_equal_to_int() {
1224        let mut scope = Scope::new();
1225        let expr = Expr::BinaryOp {
1226            left: Box::new(Expr::Literal(Value::Null)),
1227            op: BinaryOp::Eq,
1228            right: Box::new(Expr::Literal(Value::Int(0))),
1229        };
1230        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1231    }
1232
1233    #[test]
1234    fn eval_float_comparison_boundary() {
1235        let mut scope = Scope::new();
1236        // 1.0 == 1.0 (exact)
1237        let expr = Expr::BinaryOp {
1238            left: Box::new(Expr::Literal(Value::Float(1.0))),
1239            op: BinaryOp::Eq,
1240            right: Box::new(Expr::Literal(Value::Float(1.0))),
1241        };
1242        assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1243    }
1244
1245    #[test]
1246    fn eval_interpolation_with_bool() {
1247        let mut scope = Scope::new();
1248        scope.set("FLAG", Value::Bool(true));
1249
1250        let expr = Expr::Interpolated(vec![
1251            StringPart::Literal("enabled: ".into()),
1252            StringPart::Var(VarPath::simple("FLAG")),
1253        ]);
1254        assert_eq!(
1255            eval_expr(&expr, &mut scope),
1256            Ok(Value::String("enabled: true".into()))
1257        );
1258    }
1259
1260    #[test]
1261    fn eval_interpolation_with_null() {
1262        let mut scope = Scope::new();
1263        scope.set("VAL", Value::Null);
1264
1265        let expr = Expr::Interpolated(vec![
1266            StringPart::Literal("value: ".into()),
1267            StringPart::Var(VarPath::simple("VAL")),
1268        ]);
1269        assert_eq!(
1270            eval_expr(&expr, &mut scope),
1271            Ok(Value::String("value: null".into()))
1272        );
1273    }
1274
1275    #[test]
1276    fn eval_format_path_simple() {
1277        let path = VarPath::simple("X");
1278        assert_eq!(format_path(&path), "${X}");
1279    }
1280
1281    #[test]
1282    fn eval_format_path_nested() {
1283        let path = VarPath {
1284            segments: vec![
1285                VarSegment::Field("?".into()),
1286                VarSegment::Field("code".into()),
1287            ],
1288        };
1289        assert_eq!(format_path(&path), "${?.code}");
1290    }
1291
1292    #[test]
1293    fn type_name_all_types() {
1294        assert_eq!(type_name(&Value::Null), "null");
1295        assert_eq!(type_name(&Value::Bool(true)), "bool");
1296        assert_eq!(type_name(&Value::Int(1)), "int");
1297        assert_eq!(type_name(&Value::Float(1.0)), "float");
1298        assert_eq!(type_name(&Value::String("".into())), "string");
1299    }
1300
1301    #[test]
1302    fn expand_tilde_home() {
1303        // Only test if HOME is set
1304        if let Ok(home) = std::env::var("HOME") {
1305            assert_eq!(expand_tilde("~"), home);
1306            assert_eq!(expand_tilde("~/foo"), format!("{}/foo", home));
1307            assert_eq!(expand_tilde("~/foo/bar"), format!("{}/foo/bar", home));
1308        }
1309    }
1310
1311    #[test]
1312    fn expand_tilde_passthrough() {
1313        // These should not be expanded
1314        assert_eq!(expand_tilde("/home/user"), "/home/user");
1315        assert_eq!(expand_tilde("foo~bar"), "foo~bar");
1316        assert_eq!(expand_tilde(""), "");
1317    }
1318
1319    #[test]
1320    #[cfg(unix)]
1321    fn expand_tilde_user() {
1322        // Test ~root expansion (root user exists on all Unix systems)
1323        let expanded = expand_tilde("~root");
1324        // root's home is typically /root or /var/root (macOS)
1325        assert!(
1326            expanded == "/root" || expanded == "/var/root",
1327            "expected /root or /var/root, got: {}",
1328            expanded
1329        );
1330
1331        // Test ~root/subpath
1332        let expanded_path = expand_tilde("~root/subdir");
1333        assert!(
1334            expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1335            "expected /root/subdir or /var/root/subdir, got: {}",
1336            expanded_path
1337        );
1338
1339        // Nonexistent user should remain unchanged
1340        let nonexistent = expand_tilde("~nonexistent_user_12345");
1341        assert_eq!(nonexistent, "~nonexistent_user_12345");
1342    }
1343
1344    #[test]
1345    fn value_to_string_with_tilde_expansion() {
1346        if let Ok(home) = std::env::var("HOME") {
1347            let val = Value::String("~/test".into());
1348            assert_eq!(value_to_string_with_tilde(&val), format!("{}/test", home));
1349        }
1350    }
1351
1352    #[test]
1353    fn eval_positional_param() {
1354        let mut scope = Scope::new();
1355        scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1356
1357        // $0 is the tool name
1358        let expr = Expr::Positional(0);
1359        let result = eval_expr(&expr, &mut scope).unwrap();
1360        assert_eq!(result, Value::String("my_tool".into()));
1361
1362        // $1 is the first argument
1363        let expr = Expr::Positional(1);
1364        let result = eval_expr(&expr, &mut scope).unwrap();
1365        assert_eq!(result, Value::String("hello".into()));
1366
1367        // $2 is the second argument
1368        let expr = Expr::Positional(2);
1369        let result = eval_expr(&expr, &mut scope).unwrap();
1370        assert_eq!(result, Value::String("world".into()));
1371
1372        // $3 is not set, returns empty string
1373        let expr = Expr::Positional(3);
1374        let result = eval_expr(&expr, &mut scope).unwrap();
1375        assert_eq!(result, Value::String("".into()));
1376    }
1377
1378    #[test]
1379    fn eval_all_args() {
1380        let mut scope = Scope::new();
1381        scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1382
1383        let expr = Expr::AllArgs;
1384        let result = eval_expr(&expr, &mut scope).unwrap();
1385
1386        // $@ returns a space-separated string (POSIX-style)
1387        assert_eq!(result, Value::String("a b c".into()));
1388    }
1389
1390    #[test]
1391    fn eval_arg_count() {
1392        let mut scope = Scope::new();
1393        scope.set_positional("test", vec!["x".into(), "y".into()]);
1394
1395        let expr = Expr::ArgCount;
1396        let result = eval_expr(&expr, &mut scope).unwrap();
1397        assert_eq!(result, Value::Int(2));
1398    }
1399
1400    #[test]
1401    fn eval_arg_count_empty() {
1402        let mut scope = Scope::new();
1403
1404        let expr = Expr::ArgCount;
1405        let result = eval_expr(&expr, &mut scope).unwrap();
1406        assert_eq!(result, Value::Int(0));
1407    }
1408
1409    #[test]
1410    fn eval_var_length_string() {
1411        let mut scope = Scope::new();
1412        scope.set("NAME", Value::String("hello".into()));
1413
1414        let expr = Expr::VarLength("NAME".into());
1415        let result = eval_expr(&expr, &mut scope).unwrap();
1416        assert_eq!(result, Value::Int(5));
1417    }
1418
1419    #[test]
1420    fn eval_var_length_empty_string() {
1421        let mut scope = Scope::new();
1422        scope.set("EMPTY", Value::String("".into()));
1423
1424        let expr = Expr::VarLength("EMPTY".into());
1425        let result = eval_expr(&expr, &mut scope).unwrap();
1426        assert_eq!(result, Value::Int(0));
1427    }
1428
1429    #[test]
1430    fn eval_var_length_unset() {
1431        let mut scope = Scope::new();
1432
1433        // Unset variable has length 0
1434        let expr = Expr::VarLength("MISSING".into());
1435        let result = eval_expr(&expr, &mut scope).unwrap();
1436        assert_eq!(result, Value::Int(0));
1437    }
1438
1439    #[test]
1440    fn eval_var_length_int() {
1441        let mut scope = Scope::new();
1442        scope.set("NUM", Value::Int(12345));
1443
1444        // Length of the string representation
1445        let expr = Expr::VarLength("NUM".into());
1446        let result = eval_expr(&expr, &mut scope).unwrap();
1447        assert_eq!(result, Value::Int(5)); // "12345" has length 5
1448    }
1449
1450    #[test]
1451    fn eval_var_with_default_set() {
1452        let mut scope = Scope::new();
1453        scope.set("NAME", Value::String("Alice".into()));
1454
1455        // Variable is set, return its value
1456        let expr = Expr::VarWithDefault {
1457            name: "NAME".into(),
1458            default: vec![StringPart::Literal("default".into())],
1459        };
1460        let result = eval_expr(&expr, &mut scope).unwrap();
1461        assert_eq!(result, Value::String("Alice".into()));
1462    }
1463
1464    #[test]
1465    fn eval_var_with_default_unset() {
1466        let mut scope = Scope::new();
1467
1468        // Variable is unset, return default
1469        let expr = Expr::VarWithDefault {
1470            name: "MISSING".into(),
1471            default: vec![StringPart::Literal("fallback".into())],
1472        };
1473        let result = eval_expr(&expr, &mut scope).unwrap();
1474        assert_eq!(result, Value::String("fallback".into()));
1475    }
1476
1477    #[test]
1478    fn eval_var_with_default_empty() {
1479        let mut scope = Scope::new();
1480        scope.set("EMPTY", Value::String("".into()));
1481
1482        // Variable is set but empty, return default
1483        let expr = Expr::VarWithDefault {
1484            name: "EMPTY".into(),
1485            default: vec![StringPart::Literal("not empty".into())],
1486        };
1487        let result = eval_expr(&expr, &mut scope).unwrap();
1488        assert_eq!(result, Value::String("not empty".into()));
1489    }
1490
1491    #[test]
1492    fn eval_var_with_default_non_string() {
1493        let mut scope = Scope::new();
1494        scope.set("NUM", Value::Int(42));
1495
1496        // Variable is set to a non-string value, return the value
1497        let expr = Expr::VarWithDefault {
1498            name: "NUM".into(),
1499            default: vec![StringPart::Literal("default".into())],
1500        };
1501        let result = eval_expr(&expr, &mut scope).unwrap();
1502        assert_eq!(result, Value::Int(42));
1503    }
1504
1505    #[test]
1506    fn eval_unset_variable_is_empty() {
1507        let mut scope = Scope::new();
1508        let parts = vec![
1509            StringPart::Literal("prefix:".into()),
1510            StringPart::Var(VarPath::simple("UNSET")),
1511            StringPart::Literal(":suffix".into()),
1512        ];
1513        let expr = Expr::Interpolated(parts);
1514        let result = eval_expr(&expr, &mut scope).unwrap();
1515        assert_eq!(result, Value::String("prefix::suffix".into()));
1516    }
1517
1518    #[test]
1519    fn eval_unset_variable_multiple() {
1520        let mut scope = Scope::new();
1521        scope.set("SET", Value::String("hello".into()));
1522        let parts = vec![
1523            StringPart::Var(VarPath::simple("UNSET1")),
1524            StringPart::Literal("-".into()),
1525            StringPart::Var(VarPath::simple("SET")),
1526            StringPart::Literal("-".into()),
1527            StringPart::Var(VarPath::simple("UNSET2")),
1528        ];
1529        let expr = Expr::Interpolated(parts);
1530        let result = eval_expr(&expr, &mut scope).unwrap();
1531        assert_eq!(result, Value::String("-hello-".into()));
1532    }
1533}