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