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