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