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