1use std::fmt;
13
14use kaish_types::json_to_value_no_envelope;
15
16use crate::arithmetic;
17use crate::ast::{
18 spread_non_list_message, BinaryOp, Expr, ListElem, RecordEntry, RecordKey,
19 StringPart, StringTestOp, TestCmpOp, TestExpr, Value, VarPath,
20};
21
22use super::scope::Scope;
23
24pub fn strip_leading_tabs(s: &str) -> String {
30 let mut out = String::with_capacity(s.len());
31 let mut at_line_start = true;
32 for ch in s.chars() {
33 if at_line_start && ch == '\t' {
34 continue;
36 }
37 out.push(ch);
38 at_line_start = ch == '\n';
39 }
40 out
41}
42
43pub struct HeredocAssembler {
57 out: String,
58 strip_tabs: bool,
59 at_line_start: bool,
60}
61
62impl HeredocAssembler {
63 pub fn new(strip_tabs: bool) -> Self {
64 Self {
65 out: String::new(),
66 strip_tabs,
67 at_line_start: true,
68 }
69 }
70
71 pub fn push_literal(&mut self, literal: &str) {
74 if !self.strip_tabs {
75 self.out.push_str(literal);
76 return;
77 }
78 for ch in literal.chars() {
79 match ch {
80 '\n' => {
81 self.out.push(ch);
82 self.at_line_start = true;
83 }
84 '\t' if self.at_line_start => {} _ => {
86 self.out.push(ch);
87 self.at_line_start = false;
88 }
89 }
90 }
91 }
92
93 pub fn push_interpolated(&mut self, value: &str) {
98 self.out.push_str(value);
99 if self.strip_tabs {
100 self.at_line_start = false;
101 }
102 }
103
104 pub fn into_string(self) -> String {
105 self.out
106 }
107}
108
109#[derive(Debug, Clone, PartialEq)]
111pub enum EvalError {
112 UndefinedVariable(String),
114 InvalidPath(String),
116 TypeError { expected: &'static str, got: String },
118 CommandFailed(String),
120 NoExecutor,
125 ArithmeticError(String),
127 RegexError(String),
129 Unsupported(String),
133}
134
135impl fmt::Display for EvalError {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 match self {
138 EvalError::UndefinedVariable(name) => write!(f, "undefined variable: {name}"),
139 EvalError::InvalidPath(path) => write!(f, "invalid path: {path}"),
140 EvalError::TypeError { expected, got } => {
141 write!(f, "type error: expected {expected}, got {got}")
142 }
143 EvalError::CommandFailed(msg) => write!(f, "command failed: {msg}"),
144 EvalError::NoExecutor => write!(
145 f,
146 "command substitution must be resolved by the async evaluator before sync evaluation"
147 ),
148 EvalError::ArithmeticError(msg) => write!(f, "arithmetic error: {msg}"),
149 EvalError::RegexError(msg) => write!(f, "regex error: {msg}"),
150 EvalError::Unsupported(msg) => write!(f, "{msg}"),
151 }
152 }
153}
154
155impl std::error::Error for EvalError {}
156
157pub type EvalResult<T> = Result<T, EvalError>;
159
160pub struct Evaluator<'a> {
167 scope: &'a mut Scope,
168}
169
170impl<'a> Evaluator<'a> {
171 pub fn new(scope: &'a mut Scope) -> Self {
173 Self { scope }
174 }
175
176 pub fn eval(&mut self, expr: &Expr) -> EvalResult<Value> {
178 match expr {
179 Expr::Literal(value) => self.eval_literal(value),
180 Expr::VarRef(path) => self.eval_var_ref(path),
181 Expr::Interpolated(parts) => self.eval_interpolated(parts),
182 Expr::HereDocBody { parts, strip_tabs } => {
183 let mut asm = HeredocAssembler::new(*strip_tabs);
186 for sp in parts {
187 match &sp.part {
188 StringPart::Literal(s) => asm.push_literal(s),
189 other => {
190 let value = self.eval_interpolated(std::slice::from_ref(other))?;
191 asm.push_interpolated(&value_to_string(&value));
192 }
193 }
194 }
195 Ok(Value::String(asm.into_string()))
196 }
197 Expr::BinaryOp { left, op, right } => self.eval_binary_op(left, *op, right),
198 Expr::CommandSubst(_) => Err(EvalError::NoExecutor),
202 Expr::Test(test_expr) => self.eval_test(test_expr),
203 Expr::Positional(n) => self.eval_positional(*n),
204 Expr::AllArgs => self.eval_all_args(),
205 Expr::ArgCount => self.eval_arg_count(),
206 Expr::VarLength(path) => self.eval_var_length(path),
207 Expr::VarWithDefault { path, default } => self.eval_var_with_default(path, default),
208 Expr::Arithmetic(expr_str) => self.eval_arithmetic(expr_str),
209 Expr::Command(cmd) => self.eval_command(cmd),
210 Expr::LastExitCode => self.eval_last_exit_code(),
211 Expr::CurrentPid => self.eval_current_pid(),
212 Expr::GlobPattern(s) => Ok(Value::String(s.clone())),
213 Expr::ListLiteral(elems) => self.eval_list_literal(elems),
214 Expr::RecordLiteral(entries) => self.eval_record_literal(entries),
215 }
216 }
217
218 fn eval_list_literal(&mut self, elems: &[ListElem]) -> EvalResult<Value> {
222 let mut out = Vec::with_capacity(elems.len());
223 for elem in elems {
224 match elem {
225 ListElem::Item(e) => {
226 let value = self.eval(e)?;
227 out.push(kaish_types::value_to_json(&value));
228 }
229 ListElem::Spread(e) => {
230 let value = self.eval(e)?;
231 match value {
232 Value::Json(serde_json::Value::Array(items)) => out.extend(items),
233 other => return Err(EvalError::Unsupported(spread_non_list_message(&other))),
234 }
235 }
236 }
237 }
238 Ok(Value::Json(serde_json::Value::Array(out)))
239 }
240
241 fn eval_record_literal(&mut self, entries: &[RecordEntry]) -> EvalResult<Value> {
246 let mut map = serde_json::Map::new();
247 for entry in entries {
248 let key = match &entry.key {
249 RecordKey::Bare(s) | RecordKey::Quoted(s) => s.clone(),
250 RecordKey::Interpolated(parts) => {
254 value_to_string(&self.eval_interpolated(parts)?)
255 }
256 };
257 let value = self.eval(&entry.value)?;
258 map.insert(key, kaish_types::value_to_json(&value));
259 }
260 Ok(Value::Json(serde_json::Value::Object(map)))
261 }
262
263 fn eval_last_exit_code(&self) -> EvalResult<Value> {
265 Ok(Value::Int(self.scope.last_result().code))
266 }
267
268 fn eval_current_pid(&self) -> EvalResult<Value> {
270 Ok(Value::Int(self.scope.pid() as i64))
271 }
272
273 fn eval_command(&mut self, cmd: &crate::ast::Command) -> EvalResult<Value> {
275 match cmd.name.as_str() {
278 "true" => Ok(Value::Bool(true)),
279 "false" => Ok(Value::Bool(false)),
280 _ => Err(EvalError::NoExecutor),
284 }
285 }
286
287 fn eval_arithmetic(&mut self, expr_str: &str) -> EvalResult<Value> {
289 arithmetic::eval_arithmetic(expr_str, self.scope)
290 .map(Value::Int)
291 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
292 }
293
294 fn eval_test(&mut self, test_expr: &TestExpr) -> EvalResult<Value> {
296 let result = match test_expr {
297 TestExpr::FileTest { .. } => {
298 return Err(EvalError::Unsupported(
305 "file tests must be resolved by the async evaluator".to_string(),
306 ));
307 }
308 TestExpr::StringTest { op, value } => match op {
309 StringTestOp::IsEmpty | StringTestOp::IsNonEmpty => {
310 let val = self.eval(value)?;
311 let symbol = match op {
315 StringTestOp::IsEmpty => "-z",
316 StringTestOp::IsNonEmpty => "-n",
317 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
318 };
319 if let Some(msg) = scalar_test_operand_error(symbol, &val) {
320 return Err(EvalError::Unsupported(msg));
321 }
322 let s = value_to_string(&val);
323 match op {
324 StringTestOp::IsEmpty => s.is_empty(),
325 StringTestOp::IsNonEmpty => !s.is_empty(),
326 StringTestOp::IsList | StringTestOp::IsRecord => unreachable!(),
327 }
328 }
329 StringTestOp::IsList | StringTestOp::IsRecord => {
336 let val = self.eval(value)?;
337 op.matches_shape(&val)
338 }
339 },
340 TestExpr::Comparison { left, op, right } => {
341 let left_val = self.eval(left)?;
342 let right_val = self.eval(right)?;
343
344 match op {
345 TestCmpOp::Eq => values_equal(&left_val, &right_val)?,
346 TestCmpOp::NotEq => !(values_equal(&left_val, &right_val)?),
347 TestCmpOp::Match => {
348 guard_scalar_test_operands(op, &left_val, &right_val)?;
350 match regex_match(&left_val, &right_val, false)? {
352 Value::Bool(b) => b,
353 _ => false,
354 }
355 }
356 TestCmpOp::NotMatch => {
357 guard_scalar_test_operands(op, &left_val, &right_val)?;
358 match regex_match(&left_val, &right_val, true)? {
360 Value::Bool(b) => b,
361 _ => true,
362 }
363 }
364 TestCmpOp::Gt | TestCmpOp::Lt | TestCmpOp::GtEq | TestCmpOp::LtEq => {
365 guard_scalar_test_operands(op, &left_val, &right_val)?;
367 let ord = compare_values(&left_val, &right_val)?;
369 match op {
370 TestCmpOp::Gt => ord.is_gt(),
371 TestCmpOp::Lt => ord.is_lt(),
372 TestCmpOp::GtEq => ord.is_ge(),
373 TestCmpOp::LtEq => ord.is_le(),
374 _ => unreachable!(),
375 }
376 }
377 TestCmpOp::NumEq
378 | TestCmpOp::NumNotEq
379 | TestCmpOp::NumGt
380 | TestCmpOp::NumLt
381 | TestCmpOp::NumGtEq
382 | TestCmpOp::NumLtEq => {
383 guard_scalar_test_operands(op, &left_val, &right_val)?;
385 let ord = numeric_compare(&left_val, &right_val)?;
388 match op {
389 TestCmpOp::NumEq => ord.is_eq(),
390 TestCmpOp::NumNotEq => !ord.is_eq(),
391 TestCmpOp::NumGt => ord.is_gt(),
392 TestCmpOp::NumLt => ord.is_lt(),
393 TestCmpOp::NumGtEq => ord.is_ge(),
394 TestCmpOp::NumLtEq => ord.is_le(),
395 _ => unreachable!(),
396 }
397 }
398 }
399 }
400 TestExpr::And { left, right } => {
401 let left_result = self.eval_test(left)?;
403 if !value_to_bool(&left_result) {
404 false } else {
406 value_to_bool(&self.eval_test(right)?)
407 }
408 }
409 TestExpr::Or { left, right } => {
410 let left_result = self.eval_test(left)?;
412 if value_to_bool(&left_result) {
413 true } else {
415 value_to_bool(&self.eval_test(right)?)
416 }
417 }
418 TestExpr::Not { expr } => {
419 let result = self.eval_test(expr)?;
420 !value_to_bool(&result)
421 }
422 TestExpr::In { left, right } => {
423 let left_val = self.eval(left)?;
424 let right_val = self.eval(right)?;
425 eval_membership(&left_val, &right_val)?
426 }
427 TestExpr::NotIn { left, right } => {
428 let left_val = self.eval(left)?;
429 let right_val = self.eval(right)?;
430 !eval_membership(&left_val, &right_val)?
431 }
432 };
433 Ok(Value::Bool(result))
434 }
435
436 fn eval_literal(&mut self, value: &Value) -> EvalResult<Value> {
438 Ok(value.clone())
439 }
440
441 fn eval_var_ref(&mut self, path: &VarPath) -> EvalResult<Value> {
443 match self.scope.resolve_path(path) {
444 Ok(v) => Ok(v),
445 Err(super::scope::PathError::UndefinedRoot(_)) => {
447 Err(EvalError::InvalidPath(format_path(path)))
448 }
449 Err(super::scope::PathError::Absence(msg))
452 | Err(super::scope::PathError::Shape(msg)) => Err(EvalError::InvalidPath(msg)),
453 }
454 }
455
456 fn eval_positional(&self, n: usize) -> EvalResult<Value> {
458 match self.scope.get_positional(n) {
459 Some(s) => Ok(Value::String(s.to_string())),
460 None => Ok(Value::String(String::new())), }
462 }
463
464 fn eval_all_args(&self) -> EvalResult<Value> {
468 let args = self.scope.all_args();
469 Ok(Value::String(args.join(" ")))
470 }
471
472 fn eval_arg_count(&self) -> EvalResult<Value> {
474 Ok(Value::Int(self.scope.arg_count() as i64))
475 }
476
477 fn eval_var_length(&self, path: &VarPath) -> EvalResult<Value> {
479 resolve_length(self.scope, path)
480 .map(Value::Int)
481 .map_err(EvalError::InvalidPath)
482 }
483
484 fn eval_var_with_default(&mut self, path: &VarPath, default: &[StringPart]) -> EvalResult<Value> {
488 match resolve_default(self.scope, path).map_err(EvalError::InvalidPath)? {
489 Some(value) => Ok(value),
490 None => self.eval_interpolated(default),
491 }
492 }
493
494 fn eval_interpolated(&mut self, parts: &[StringPart]) -> EvalResult<Value> {
496 let mut result = String::new();
497 for part in parts {
498 match part {
499 StringPart::Literal(s) => result.push_str(s),
500 StringPart::Var(path) => {
501 match self.scope.resolve_path(path) {
502 Ok(value) => result.push_str(&value_to_text_sink(&value)?),
504 Err(super::scope::PathError::UndefinedRoot(_)) => {}
506 Err(super::scope::PathError::Absence(msg))
509 | Err(super::scope::PathError::Shape(msg)) => {
510 return Err(EvalError::InvalidPath(msg))
511 }
512 }
513 }
514 StringPart::VarWithDefault { path, default } => {
515 let value = self.eval_var_with_default(path, default)?;
516 result.push_str(&value_to_text_sink(&value)?);
517 }
518 StringPart::VarLength(path) => {
519 let value = self.eval_var_length(path)?;
520 result.push_str(&value_to_text_sink(&value)?);
521 }
522 StringPart::Positional(n) => {
523 let value = self.eval_positional(*n)?;
524 result.push_str(&value_to_text_sink(&value)?);
525 }
526 StringPart::AllArgs => {
527 let value = self.eval_all_args()?;
528 result.push_str(&value_to_text_sink(&value)?);
529 }
530 StringPart::ArgCount => {
531 let value = self.eval_arg_count()?;
532 result.push_str(&value_to_text_sink(&value)?);
533 }
534 StringPart::Arithmetic(expr) => {
535 let value = self.eval_arithmetic_string(expr)?;
537 result.push_str(&value_to_text_sink(&value)?);
538 }
539 StringPart::CommandSubst(_) => {
540 return Err(EvalError::NoExecutor);
546 }
547 StringPart::LastExitCode => {
548 result.push_str(&self.scope.last_result().code.to_string());
549 }
550 StringPart::CurrentPid => {
551 result.push_str(&self.scope.pid().to_string());
552 }
553 }
554 }
555 Ok(Value::String(result))
556 }
557
558 fn eval_arithmetic_string(&mut self, expr: &str) -> EvalResult<Value> {
560 arithmetic::eval_arithmetic(expr, self.scope)
562 .map(Value::Int)
563 .map_err(|e| EvalError::ArithmeticError(e.to_string()))
564 }
565
566 fn eval_binary_op(&mut self, left: &Expr, op: BinaryOp, right: &Expr) -> EvalResult<Value> {
570 match op {
571 BinaryOp::And => {
572 let left_val = self.eval(left)?;
573 if !is_truthy(&left_val) {
574 return Ok(left_val);
575 }
576 self.eval(right)
577 }
578 BinaryOp::Or => {
579 let left_val = self.eval(left)?;
580 if is_truthy(&left_val) {
581 return Ok(left_val);
582 }
583 self.eval(right)
584 }
585 }
586 }
587
588}
589
590pub fn value_to_exit_code(value: &Value) -> anyhow::Result<i64> {
597 match value {
598 Value::Int(n) => Ok(*n),
599 Value::Bool(b) => Ok(if *b { 0 } else { 1 }),
600 Value::Float(f) => Ok(*f as i64),
601 Value::String(s) => {
602 let trimmed = s.trim();
603 trimmed.parse::<i64>().map_err(|_| {
604 anyhow::anyhow!("numeric argument required: {:?}", s)
605 })
606 }
607 Value::Null | Value::Json(_) | Value::Bytes(_) => {
608 anyhow::bail!("numeric argument required (got {:?})", value)
609 }
610 }
611}
612
613pub fn value_length(value: &Value) -> i64 {
618 match value {
619 Value::Json(serde_json::Value::Array(a)) => a.len() as i64,
620 Value::Json(serde_json::Value::Object(o)) => o.len() as i64,
621 Value::Bytes(b) => b.len() as i64,
623 other => value_to_string(other).len() as i64,
624 }
625}
626
627pub fn value_defaults_on_emptiness(value: &Value) -> bool {
634 match value {
635 Value::Null | Value::Json(serde_json::Value::Null) => true,
636 Value::String(s) => s.is_empty(),
637 _ => false,
638 }
639}
640
641pub fn resolve_length(scope: &Scope, path: &VarPath) -> Result<i64, String> {
648 match scope.resolve_path(path) {
649 Ok(value) => Ok(value_length(&value)),
650 Err(super::scope::PathError::UndefinedRoot(_)) if path.segments.len() <= 1 => Ok(0),
651 Err(super::scope::PathError::UndefinedRoot(_)) => {
652 Err(format!("{}: undefined variable", format_path(path)))
653 }
654 Err(super::scope::PathError::Absence(msg)) | Err(super::scope::PathError::Shape(msg)) => {
655 Err(msg)
656 }
657 }
658}
659
660pub fn resolve_default(scope: &Scope, path: &VarPath) -> Result<Option<Value>, String> {
666 match scope.resolve_path(path) {
667 Ok(value) if value_defaults_on_emptiness(&value) => Ok(None),
668 Ok(value) => Ok(Some(value)),
669 Err(super::scope::PathError::UndefinedRoot(_))
670 | Err(super::scope::PathError::Absence(_)) => Ok(None),
671 Err(super::scope::PathError::Shape(msg)) => Err(msg),
672 }
673}
674
675pub fn structured_export_error(vars: &[(String, Value)]) -> Option<String> {
681 for (name, value) in vars {
682 if let Value::Json(j) = value {
683 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) {
684 let kind = if j.is_array() { "list" } else { "record" };
685 return Some(format!(
686 "cannot export '{name}': it holds a {kind}, which can't be an OS environment variable — serialize it explicitly first, e.g. `export {name}=$(tojson ${name})`"
687 ));
688 }
689 }
690 }
691 None
692}
693
694pub fn is_collection(value: &Value) -> bool {
699 matches!(
700 value,
701 Value::Json(serde_json::Value::Array(_)) | Value::Json(serde_json::Value::Object(_))
702 )
703}
704
705fn collection_kind(value: &Value) -> &'static str {
708 match value {
709 Value::Json(serde_json::Value::Array(_)) => "list",
710 Value::Json(serde_json::Value::Object(_)) => "record",
711 _ => "collection",
712 }
713}
714
715pub fn structured_boundary_error(sink: &str, value: &Value) -> Option<String> {
725 if is_collection(value) {
726 let kind = collection_kind(value);
727 Some(format!(
728 "cannot use a {kind} as {sink} — serialize it explicitly first, e.g. `cmd $(tojson $x)`"
729 ))
730 } else {
731 None
732 }
733}
734
735pub fn scalar_test_operand_error(op_symbol: &str, value: &Value) -> Option<String> {
744 if is_collection(value) {
745 let kind = collection_kind(value);
746 Some(format!(
747 "`{op_symbol}` needs a scalar; got a {kind} — use `${{#x}}` for length, \
748 `-list`/`-record` to test shape, or `in` for membership"
749 ))
750 } else {
751 None
752 }
753}
754
755pub fn value_to_string(value: &Value) -> String {
756 match value {
757 Value::Null => "null".to_string(),
758 Value::Bool(b) => b.to_string(),
759 Value::Int(i) => i.to_string(),
760 Value::Float(f) => f.to_string(),
761 Value::String(s) => s.clone(),
762 Value::Json(json) => json.to_string(),
763 Value::Bytes(b) => format!("[binary: {} bytes]", b.len()),
769 }
770}
771
772pub fn value_to_text_sink(value: &Value) -> EvalResult<String> {
790 match value {
791 Value::Bytes(b) => match std::str::from_utf8(b) {
792 Ok(s) => Ok(s.to_string()),
793 Err(_) => Err(EvalError::Unsupported(format!(
794 "binary data ({} bytes) cannot be used as text — decode it \
795 (base64/xxd) or redirect to a file",
796 b.len()
797 ))),
798 },
799 other => Ok(value_to_string(other)),
800 }
801}
802
803pub fn value_to_bool(value: &Value) -> bool {
813 match value {
814 Value::Null => false,
815 Value::Bool(b) => *b,
816 Value::Int(i) => *i != 0,
817 Value::Float(f) => *f != 0.0,
818 Value::String(s) => !s.is_empty(),
819 Value::Json(json) => match json {
820 serde_json::Value::Null => false,
821 serde_json::Value::Array(arr) => !arr.is_empty(),
822 serde_json::Value::Object(obj) => !obj.is_empty(),
823 serde_json::Value::Bool(b) => *b,
824 serde_json::Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(false),
825 serde_json::Value::String(s) => !s.is_empty(),
826 },
827 Value::Bytes(b) => !b.is_empty(), }
829}
830
831pub fn expand_tilde(s: &str, home: Option<&str>) -> String {
845 if s == "~" {
846 home.map(|h| h.to_string()).unwrap_or_else(|| "~".to_string())
847 } else if s.starts_with("~/") {
848 match home {
849 Some(home) => format!("{}{}", home, &s[1..]),
850 None => s.to_string(),
851 }
852 } else if s.starts_with('~') {
853 expand_tilde_user(s)
855 } else {
856 s.to_string()
857 }
858}
859
860#[cfg(all(unix, feature = "host"))]
865fn expand_tilde_user(s: &str) -> String {
866 let (username, rest) = if let Some(slash_pos) = s[1..].find('/') {
868 (&s[1..slash_pos + 1], &s[slash_pos + 1..])
869 } else {
870 (&s[1..], "")
871 };
872
873 if username.is_empty() {
874 return s.to_string();
875 }
876
877 let passwd = match std::fs::read_to_string("/etc/passwd") {
880 Ok(content) => content,
881 Err(_) => return s.to_string(),
882 };
883
884 for line in passwd.lines() {
885 let fields: Vec<&str> = line.split(':').collect();
886 if fields.len() >= 6 && fields[0] == username {
887 let home_dir = fields[5];
888 return if rest.is_empty() {
889 home_dir.to_string()
890 } else {
891 format!("{}{}", home_dir, rest)
892 };
893 }
894 }
895
896 s.to_string()
898}
899
900#[cfg(not(all(unix, feature = "host")))]
901fn expand_tilde_user(s: &str) -> String {
902 s.to_string()
905}
906
907pub fn value_to_string_with_tilde(value: &Value, home: Option<&str>) -> String {
912 match value {
913 Value::String(s) if s.starts_with('~') => expand_tilde(s, home),
914 _ => value_to_string(value),
915 }
916}
917
918pub(crate) fn format_path(path: &VarPath) -> String {
923 use crate::ast::VarSegment;
924 let mut result = String::from("${");
925 for (i, seg) in path.segments.iter().enumerate() {
926 match seg {
927 VarSegment::Field(name) => {
928 if i > 0 {
929 result.push('.');
930 }
931 result.push_str(name);
932 }
933 VarSegment::Index(idx) => result.push_str(&format!("[{idx}]")),
934 VarSegment::Key(k) => result.push_str(&format!("[{k}]")),
935 VarSegment::Dynamic(v) => result.push_str(&format!("[${v}]")),
936 VarSegment::Slice(a, b) => {
937 let s = a.map(|n| n.to_string()).unwrap_or_default();
938 let e = b.map(|n| n.to_string()).unwrap_or_default();
939 result.push_str(&format!("[{s}:{e}]"));
940 }
941 }
942 }
943 result.push('}');
944 result
945}
946
947fn is_truthy(value: &Value) -> bool {
957 value_to_bool(value)
959}
960
961pub fn values_equal(left: &Value, right: &Value) -> EvalResult<bool> {
972 match (left, right) {
973 (Value::Null, Value::Null) => Ok(true),
974 (Value::Bool(a), Value::Bool(b)) => Ok(a == b),
975 (Value::Int(a), Value::Int(b)) => Ok(a == b),
976 (Value::Float(a), Value::Float(b)) => Ok((a - b).abs() < f64::EPSILON),
977 (Value::Int(a), Value::Float(b)) | (Value::Float(b), Value::Int(a)) => {
978 Ok((*a as f64 - b).abs() < f64::EPSILON)
979 }
980 (Value::String(a), Value::String(b)) => Ok(a == b),
981 (Value::Json(a), Value::Json(b)) => Ok(a == b),
982 (Value::Bytes(a), Value::Bytes(b)) => Ok(a == b),
983 (Value::Json(j), other) | (other, Value::Json(j))
989 if matches!(j, serde_json::Value::Array(_) | serde_json::Value::Object(_)) =>
990 {
991 let kind = if j.is_array() { "list" } else { "record" };
992 Err(EvalError::Unsupported(format!(
993 "cannot compare a {kind} to a {other_kind} with ==/!= — test membership with `[[ x in $coll ]]`, or compare structures with `jq`",
994 other_kind = type_name(other),
995 )))
996 }
997 _ => Ok(value_to_string(left) == value_to_string(right)),
1000 }
1001}
1002
1003fn element_matches(needle: &Value, element: &Value) -> bool {
1012 match (needle, element) {
1013 (Value::Json(a), Value::Json(b)) => a == b,
1014 (Value::Json(_), _) | (_, Value::Json(_)) => false,
1015 _ => values_equal(needle, element).unwrap_or(false),
1018 }
1019}
1020
1021fn eval_membership(needle: &Value, haystack: &Value) -> EvalResult<bool> {
1031 match haystack {
1032 Value::Json(serde_json::Value::Array(items)) => {
1033 for item in items {
1034 let element = json_to_value_no_envelope(item.clone());
1035 if element_matches(needle, &element) {
1036 return Ok(true);
1037 }
1038 }
1039 Ok(false)
1040 }
1041 Value::Json(serde_json::Value::Object(map)) => {
1042 Ok(map.contains_key(&value_to_string(needle)))
1043 }
1044 other => Err(EvalError::Unsupported(format!(
1045 "`in` requires a list or record on the right-hand side, got {} — substring tests use `=~`, glob (`[[ $s == *sub* ]]`), or `case`",
1046 type_name(other),
1047 ))),
1048 }
1049}
1050
1051fn cmp_op_symbol(op: &TestCmpOp) -> &'static str {
1054 match op {
1055 TestCmpOp::Eq => "==",
1056 TestCmpOp::NotEq => "!=",
1057 TestCmpOp::Match => "=~",
1058 TestCmpOp::NotMatch => "!~",
1059 TestCmpOp::Gt => ">",
1060 TestCmpOp::Lt => "<",
1061 TestCmpOp::GtEq => ">=",
1062 TestCmpOp::LtEq => "<=",
1063 TestCmpOp::NumEq => "-eq",
1064 TestCmpOp::NumNotEq => "-ne",
1065 TestCmpOp::NumGt => "-gt",
1066 TestCmpOp::NumLt => "-lt",
1067 TestCmpOp::NumGtEq => "-ge",
1068 TestCmpOp::NumLtEq => "-le",
1069 }
1070}
1071
1072fn guard_scalar_test_operands(op: &TestCmpOp, left: &Value, right: &Value) -> EvalResult<()> {
1076 let symbol = cmp_op_symbol(op);
1077 if let Some(msg) = scalar_test_operand_error(symbol, left) {
1078 return Err(EvalError::Unsupported(msg));
1079 }
1080 if let Some(msg) = scalar_test_operand_error(symbol, right) {
1081 return Err(EvalError::Unsupported(msg));
1082 }
1083 Ok(())
1084}
1085
1086fn compare_values(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1088 match (left, right) {
1089 (Value::Int(a), Value::Int(b)) => Ok(a.cmp(b)),
1090 (Value::Float(a), Value::Float(b)) => {
1091 a.partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1092 }
1093 (Value::Int(a), Value::Float(b)) => {
1094 (*a as f64).partial_cmp(b).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1095 }
1096 (Value::Float(a), Value::Int(b)) => {
1097 a.partial_cmp(&(*b as f64)).ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into()))
1098 }
1099 (Value::String(a), Value::String(b)) => Ok(a.cmp(b)),
1100 _ => Err(EvalError::TypeError {
1101 expected: "comparable types (numbers or strings)",
1102 got: format!("{:?} vs {:?}", type_name(left), type_name(right)),
1103 }),
1104 }
1105}
1106
1107enum Num {
1112 Int(i64),
1113 Float(f64),
1114}
1115
1116fn value_to_num(value: &Value) -> EvalResult<Num> {
1117 match value {
1118 Value::Int(n) => Ok(Num::Int(*n)),
1119 Value::Float(f) => Ok(Num::Float(*f)),
1120 Value::String(s) => {
1121 let t = s.trim();
1122 if let Ok(n) = t.parse::<i64>() {
1123 Ok(Num::Int(n))
1124 } else if let Ok(f) = t.parse::<f64>() {
1125 Ok(Num::Float(f))
1126 } else {
1127 Err(EvalError::TypeError {
1128 expected: "numeric operand",
1129 got: format!("non-numeric string {:?}", s),
1130 })
1131 }
1132 }
1133 _ => Err(EvalError::TypeError {
1134 expected: "numeric operand",
1135 got: type_name(value).to_string(),
1136 }),
1137 }
1138}
1139
1140pub fn numeric_compare(left: &Value, right: &Value) -> EvalResult<std::cmp::Ordering> {
1145 let l = value_to_num(left)?;
1146 let r = value_to_num(right)?;
1147 match (l, r) {
1148 (Num::Int(a), Num::Int(b)) => Ok(a.cmp(&b)),
1149 (Num::Float(a), Num::Float(b)) => a
1150 .partial_cmp(&b)
1151 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1152 (Num::Int(a), Num::Float(b)) => (a as f64)
1153 .partial_cmp(&b)
1154 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1155 (Num::Float(a), Num::Int(b)) => a
1156 .partial_cmp(&(b as f64))
1157 .ok_or_else(|| EvalError::ArithmeticError("NaN comparison".into())),
1158 }
1159}
1160
1161fn type_name(value: &Value) -> &'static str {
1163 match value {
1164 Value::Null => "null",
1165 Value::Bool(_) => "bool",
1166 Value::Int(_) => "int",
1167 Value::Float(_) => "float",
1168 Value::String(_) => "string",
1169 Value::Json(_) => "json",
1170 Value::Bytes(_) => "bytes",
1171 }
1172}
1173
1174fn regex_match(left: &Value, right: &Value, negate: bool) -> EvalResult<Value> {
1179 let text = match left {
1180 Value::String(s) => s.as_str(),
1181 _ => {
1182 return Err(EvalError::TypeError {
1183 expected: "string",
1184 got: type_name(left).to_string(),
1185 })
1186 }
1187 };
1188
1189 let pattern = match right {
1190 Value::String(s) => s.as_str(),
1191 _ => {
1192 return Err(EvalError::TypeError {
1193 expected: "string (regex pattern)",
1194 got: type_name(right).to_string(),
1195 })
1196 }
1197 };
1198
1199 let re = regex::Regex::new(pattern).map_err(|e| EvalError::RegexError(e.to_string()))?;
1200 let matches = re.is_match(text);
1201
1202 Ok(Value::Bool(if negate { !matches } else { matches }))
1203}
1204
1205pub fn eval_expr(expr: &Expr, scope: &mut Scope) -> EvalResult<Value> {
1212 let mut evaluator = Evaluator::new(scope);
1213 evaluator.eval(expr)
1214}
1215
1216#[cfg(test)]
1217#[allow(clippy::approx_constant)]
1218mod tests {
1219 use super::*;
1220 use crate::ast::{Stmt, VarSegment};
1221 use super::super::result::ExecResult;
1222
1223 fn var_expr(name: &str) -> Expr {
1225 Expr::VarRef(VarPath::simple(name))
1226 }
1227
1228 #[test]
1229 fn eval_literal_int() {
1230 let mut scope = Scope::new();
1231 let expr = Expr::Literal(Value::Int(42));
1232 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1233 }
1234
1235 #[test]
1236 fn eval_literal_string() {
1237 let mut scope = Scope::new();
1238 let expr = Expr::Literal(Value::String("hello".into()));
1239 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::String("hello".into())));
1240 }
1241
1242 #[test]
1243 fn eval_literal_bool() {
1244 let mut scope = Scope::new();
1245 assert_eq!(
1246 eval_expr(&Expr::Literal(Value::Bool(true)), &mut scope),
1247 Ok(Value::Bool(true))
1248 );
1249 }
1250
1251 #[test]
1252 fn eval_literal_null() {
1253 let mut scope = Scope::new();
1254 assert_eq!(
1255 eval_expr(&Expr::Literal(Value::Null), &mut scope),
1256 Ok(Value::Null)
1257 );
1258 }
1259
1260 #[test]
1261 fn eval_literal_float() {
1262 let mut scope = Scope::new();
1263 let expr = Expr::Literal(Value::Float(3.14));
1264 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(3.14)));
1265 }
1266
1267 #[test]
1268 fn eval_variable_ref() {
1269 let mut scope = Scope::new();
1270 scope.set("X", Value::Int(100));
1271 assert_eq!(eval_expr(&var_expr("X"), &mut scope), Ok(Value::Int(100)));
1272 }
1273
1274 #[test]
1275 fn eval_undefined_variable() {
1276 let mut scope = Scope::new();
1277 let result = eval_expr(&var_expr("MISSING"), &mut scope);
1278 assert!(matches!(result, Err(EvalError::InvalidPath(_))));
1279 }
1280
1281 #[test]
1282 fn eval_interpolated_string() {
1283 let mut scope = Scope::new();
1284 scope.set("NAME", Value::String("World".into()));
1285
1286 let expr = Expr::Interpolated(vec![
1287 StringPart::Literal("Hello, ".into()),
1288 StringPart::Var(VarPath::simple("NAME")),
1289 StringPart::Literal("!".into()),
1290 ]);
1291 assert_eq!(
1292 eval_expr(&expr, &mut scope),
1293 Ok(Value::String("Hello, World!".into()))
1294 );
1295 }
1296
1297 #[test]
1298 fn eval_interpolated_with_number() {
1299 let mut scope = Scope::new();
1300 scope.set("COUNT", Value::Int(42));
1301
1302 let expr = Expr::Interpolated(vec![
1303 StringPart::Literal("Count: ".into()),
1304 StringPart::Var(VarPath::simple("COUNT")),
1305 ]);
1306 assert_eq!(
1307 eval_expr(&expr, &mut scope),
1308 Ok(Value::String("Count: 42".into()))
1309 );
1310 }
1311
1312 #[test]
1313 fn eval_and_short_circuit_true() {
1314 let mut scope = Scope::new();
1315 let expr = Expr::BinaryOp {
1316 left: Box::new(Expr::Literal(Value::Bool(true))),
1317 op: BinaryOp::And,
1318 right: Box::new(Expr::Literal(Value::Int(42))),
1319 };
1320 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1322 }
1323
1324 #[test]
1325 fn eval_and_short_circuit_false() {
1326 let mut scope = Scope::new();
1327 let expr = Expr::BinaryOp {
1328 left: Box::new(Expr::Literal(Value::Bool(false))),
1329 op: BinaryOp::And,
1330 right: Box::new(Expr::Literal(Value::Int(42))),
1331 };
1332 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(false)));
1334 }
1335
1336 #[test]
1337 fn eval_or_short_circuit_true() {
1338 let mut scope = Scope::new();
1339 let expr = Expr::BinaryOp {
1340 left: Box::new(Expr::Literal(Value::Bool(true))),
1341 op: BinaryOp::Or,
1342 right: Box::new(Expr::Literal(Value::Int(42))),
1343 };
1344 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1346 }
1347
1348 #[test]
1349 fn eval_or_short_circuit_false() {
1350 let mut scope = Scope::new();
1351 let expr = Expr::BinaryOp {
1352 left: Box::new(Expr::Literal(Value::Bool(false))),
1353 op: BinaryOp::Or,
1354 right: Box::new(Expr::Literal(Value::Int(42))),
1355 };
1356 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1358 }
1359
1360 #[test]
1361 fn is_truthy_values() {
1362 assert!(!is_truthy(&Value::Null));
1363 assert!(!is_truthy(&Value::Bool(false)));
1364 assert!(is_truthy(&Value::Bool(true)));
1365 assert!(!is_truthy(&Value::Int(0)));
1366 assert!(is_truthy(&Value::Int(1)));
1367 assert!(is_truthy(&Value::Int(-1)));
1368 assert!(!is_truthy(&Value::Float(0.0)));
1369 assert!(is_truthy(&Value::Float(0.1)));
1370 assert!(!is_truthy(&Value::String("".into())));
1371 assert!(is_truthy(&Value::String("x".into())));
1372 }
1373
1374 #[test]
1375 fn sync_command_subst_is_loud_not_silent() {
1376 use crate::ast::Command;
1380
1381 let mut scope = Scope::new();
1382 let expr = Expr::CommandSubst(vec![Stmt::Command(Command {
1383 name: "echo".into(),
1384 args: vec![],
1385 redirects: vec![],
1386 })]);
1387
1388 assert!(matches!(
1389 eval_expr(&expr, &mut scope),
1390 Err(EvalError::NoExecutor)
1391 ));
1392 }
1393
1394 #[test]
1395 fn eval_last_result_bare() {
1396 let mut scope = Scope::new();
1399 scope.set_last_result(ExecResult::failure(42, "test error"));
1400
1401 let expr = Expr::VarRef(VarPath {
1402 segments: vec![VarSegment::Field("?".into())],
1403 });
1404 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1405 }
1406
1407 #[test]
1408 fn value_to_string_all_types() {
1409 assert_eq!(value_to_string(&Value::Null), "null");
1410 assert_eq!(value_to_string(&Value::Bool(true)), "true");
1411 assert_eq!(value_to_string(&Value::Int(42)), "42");
1412 assert_eq!(value_to_string(&Value::Float(3.14)), "3.14");
1413 assert_eq!(value_to_string(&Value::String("hello".into())), "hello");
1414 }
1415
1416 #[test]
1419 fn eval_negative_int() {
1420 let mut scope = Scope::new();
1421 let expr = Expr::Literal(Value::Int(-42));
1422 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(-42)));
1423 }
1424
1425 #[test]
1426 fn eval_negative_float() {
1427 let mut scope = Scope::new();
1428 let expr = Expr::Literal(Value::Float(-3.14));
1429 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Float(-3.14)));
1430 }
1431
1432 #[test]
1433 fn eval_zero_values() {
1434 let mut scope = Scope::new();
1435 assert_eq!(
1436 eval_expr(&Expr::Literal(Value::Int(0)), &mut scope),
1437 Ok(Value::Int(0))
1438 );
1439 assert_eq!(
1440 eval_expr(&Expr::Literal(Value::Float(0.0)), &mut scope),
1441 Ok(Value::Float(0.0))
1442 );
1443 }
1444
1445 #[test]
1446 fn eval_interpolation_empty_var() {
1447 let mut scope = Scope::new();
1448 scope.set("EMPTY", Value::String("".into()));
1449
1450 let expr = Expr::Interpolated(vec![
1451 StringPart::Literal("prefix".into()),
1452 StringPart::Var(VarPath::simple("EMPTY")),
1453 StringPart::Literal("suffix".into()),
1454 ]);
1455 assert_eq!(
1456 eval_expr(&expr, &mut scope),
1457 Ok(Value::String("prefixsuffix".into()))
1458 );
1459 }
1460
1461 #[test]
1462 fn eval_chained_and() {
1463 let mut scope = Scope::new();
1464 let expr = Expr::BinaryOp {
1466 left: Box::new(Expr::BinaryOp {
1467 left: Box::new(Expr::Literal(Value::Bool(true))),
1468 op: BinaryOp::And,
1469 right: Box::new(Expr::Literal(Value::Bool(true))),
1470 }),
1471 op: BinaryOp::And,
1472 right: Box::new(Expr::Literal(Value::Int(42))),
1473 };
1474 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1475 }
1476
1477 #[test]
1478 fn eval_chained_or() {
1479 let mut scope = Scope::new();
1480 let expr = Expr::BinaryOp {
1482 left: Box::new(Expr::BinaryOp {
1483 left: Box::new(Expr::Literal(Value::Bool(false))),
1484 op: BinaryOp::Or,
1485 right: Box::new(Expr::Literal(Value::Bool(false))),
1486 }),
1487 op: BinaryOp::Or,
1488 right: Box::new(Expr::Literal(Value::Int(42))),
1489 };
1490 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Int(42)));
1491 }
1492
1493 #[test]
1494 fn eval_mixed_and_or() {
1495 let mut scope = Scope::new();
1496 let expr = Expr::BinaryOp {
1499 left: Box::new(Expr::BinaryOp {
1500 left: Box::new(Expr::Literal(Value::Bool(true))),
1501 op: BinaryOp::Or,
1502 right: Box::new(Expr::Literal(Value::Bool(false))),
1503 }),
1504 op: BinaryOp::And,
1505 right: Box::new(Expr::Literal(Value::Bool(true))),
1506 };
1507 assert_eq!(eval_expr(&expr, &mut scope), Ok(Value::Bool(true)));
1509 }
1510
1511 #[test]
1512 fn eval_interpolation_with_bool() {
1513 let mut scope = Scope::new();
1514 scope.set("FLAG", Value::Bool(true));
1515
1516 let expr = Expr::Interpolated(vec![
1517 StringPart::Literal("enabled: ".into()),
1518 StringPart::Var(VarPath::simple("FLAG")),
1519 ]);
1520 assert_eq!(
1521 eval_expr(&expr, &mut scope),
1522 Ok(Value::String("enabled: true".into()))
1523 );
1524 }
1525
1526 #[test]
1527 fn eval_interpolation_with_null() {
1528 let mut scope = Scope::new();
1529 scope.set("VAL", Value::Null);
1530
1531 let expr = Expr::Interpolated(vec![
1532 StringPart::Literal("value: ".into()),
1533 StringPart::Var(VarPath::simple("VAL")),
1534 ]);
1535 assert_eq!(
1536 eval_expr(&expr, &mut scope),
1537 Ok(Value::String("value: null".into()))
1538 );
1539 }
1540
1541 #[test]
1542 fn eval_format_path_simple() {
1543 let path = VarPath::simple("X");
1544 assert_eq!(format_path(&path), "${X}");
1545 }
1546
1547 #[test]
1548 fn eval_format_path_nested() {
1549 let path = VarPath {
1550 segments: vec![
1551 VarSegment::Field("X".into()),
1552 VarSegment::Field("field".into()),
1553 ],
1554 };
1555 assert_eq!(format_path(&path), "${X.field}");
1556 }
1557
1558 #[test]
1559 fn type_name_all_types() {
1560 assert_eq!(type_name(&Value::Null), "null");
1561 assert_eq!(type_name(&Value::Bool(true)), "bool");
1562 assert_eq!(type_name(&Value::Int(1)), "int");
1563 assert_eq!(type_name(&Value::Float(1.0)), "float");
1564 assert_eq!(type_name(&Value::String("".into())), "string");
1565 }
1566
1567 #[test]
1568 fn expand_tilde_home() {
1569 let home = "/home/session";
1571 assert_eq!(expand_tilde("~", Some(home)), home);
1572 assert_eq!(expand_tilde("~/foo", Some(home)), format!("{}/foo", home));
1573 assert_eq!(
1574 expand_tilde("~/foo/bar", Some(home)),
1575 format!("{}/foo/bar", home)
1576 );
1577 }
1578
1579 #[test]
1580 fn expand_tilde_hermetic_no_home_does_not_leak_host() {
1581 assert_eq!(expand_tilde("~", None), "~");
1584 assert_eq!(expand_tilde("~/foo", None), "~/foo");
1585 }
1586
1587 #[test]
1588 fn expand_tilde_passthrough() {
1589 assert_eq!(expand_tilde("/home/user", Some("/h")), "/home/user");
1591 assert_eq!(expand_tilde("foo~bar", Some("/h")), "foo~bar");
1592 assert_eq!(expand_tilde("", Some("/h")), "");
1593 }
1594
1595 #[test]
1596 #[cfg(all(unix, feature = "host"))]
1597 fn expand_tilde_user() {
1598 let expanded = expand_tilde("~root", None);
1601 assert!(
1603 expanded == "/root" || expanded == "/var/root",
1604 "expected /root or /var/root, got: {}",
1605 expanded
1606 );
1607
1608 let expanded_path = expand_tilde("~root/subdir", None);
1610 assert!(
1611 expanded_path == "/root/subdir" || expanded_path == "/var/root/subdir",
1612 "expected /root/subdir or /var/root/subdir, got: {}",
1613 expanded_path
1614 );
1615
1616 let nonexistent = expand_tilde("~nonexistent_user_12345", None);
1618 assert_eq!(nonexistent, "~nonexistent_user_12345");
1619 }
1620
1621 #[test]
1622 fn value_to_string_with_tilde_expansion() {
1623 let val = Value::String("~/test".into());
1625 assert_eq!(
1626 value_to_string_with_tilde(&val, Some("/home/session")),
1627 "/home/session/test"
1628 );
1629 }
1630
1631 #[test]
1632 fn eval_positional_param() {
1633 let mut scope = Scope::new();
1634 scope.set_positional("my_tool", vec!["hello".into(), "world".into()]);
1635
1636 let expr = Expr::Positional(0);
1638 let result = eval_expr(&expr, &mut scope).unwrap();
1639 assert_eq!(result, Value::String("my_tool".into()));
1640
1641 let expr = Expr::Positional(1);
1643 let result = eval_expr(&expr, &mut scope).unwrap();
1644 assert_eq!(result, Value::String("hello".into()));
1645
1646 let expr = Expr::Positional(2);
1648 let result = eval_expr(&expr, &mut scope).unwrap();
1649 assert_eq!(result, Value::String("world".into()));
1650
1651 let expr = Expr::Positional(3);
1653 let result = eval_expr(&expr, &mut scope).unwrap();
1654 assert_eq!(result, Value::String("".into()));
1655 }
1656
1657 #[test]
1658 fn eval_all_args() {
1659 let mut scope = Scope::new();
1660 scope.set_positional("test", vec!["a".into(), "b".into(), "c".into()]);
1661
1662 let expr = Expr::AllArgs;
1663 let result = eval_expr(&expr, &mut scope).unwrap();
1664
1665 assert_eq!(result, Value::String("a b c".into()));
1667 }
1668
1669 #[test]
1670 fn eval_arg_count() {
1671 let mut scope = Scope::new();
1672 scope.set_positional("test", vec!["x".into(), "y".into()]);
1673
1674 let expr = Expr::ArgCount;
1675 let result = eval_expr(&expr, &mut scope).unwrap();
1676 assert_eq!(result, Value::Int(2));
1677 }
1678
1679 #[test]
1680 fn eval_arg_count_empty() {
1681 let mut scope = Scope::new();
1682
1683 let expr = Expr::ArgCount;
1684 let result = eval_expr(&expr, &mut scope).unwrap();
1685 assert_eq!(result, Value::Int(0));
1686 }
1687
1688 #[test]
1689 fn eval_var_length_string() {
1690 let mut scope = Scope::new();
1691 scope.set("NAME", Value::String("hello".into()));
1692
1693 let expr = Expr::VarLength(VarPath::simple("NAME"));
1694 let result = eval_expr(&expr, &mut scope).unwrap();
1695 assert_eq!(result, Value::Int(5));
1696 }
1697
1698 #[test]
1699 fn eval_var_length_empty_string() {
1700 let mut scope = Scope::new();
1701 scope.set("EMPTY", Value::String("".into()));
1702
1703 let expr = Expr::VarLength(VarPath::simple("EMPTY"));
1704 let result = eval_expr(&expr, &mut scope).unwrap();
1705 assert_eq!(result, Value::Int(0));
1706 }
1707
1708 #[test]
1709 fn eval_var_length_unset() {
1710 let mut scope = Scope::new();
1711
1712 let expr = Expr::VarLength(VarPath::simple("MISSING"));
1714 let result = eval_expr(&expr, &mut scope).unwrap();
1715 assert_eq!(result, Value::Int(0));
1716 }
1717
1718 #[test]
1719 fn eval_var_length_int() {
1720 let mut scope = Scope::new();
1721 scope.set("NUM", Value::Int(12345));
1722
1723 let expr = Expr::VarLength(VarPath::simple("NUM"));
1725 let result = eval_expr(&expr, &mut scope).unwrap();
1726 assert_eq!(result, Value::Int(5)); }
1728
1729 #[test]
1730 fn eval_var_with_default_set() {
1731 let mut scope = Scope::new();
1732 scope.set("NAME", Value::String("Alice".into()));
1733
1734 let expr = Expr::VarWithDefault {
1736 path: VarPath::simple("NAME"),
1737 default: vec![StringPart::Literal("default".into())],
1738 };
1739 let result = eval_expr(&expr, &mut scope).unwrap();
1740 assert_eq!(result, Value::String("Alice".into()));
1741 }
1742
1743 #[test]
1744 fn eval_var_with_default_unset() {
1745 let mut scope = Scope::new();
1746
1747 let expr = Expr::VarWithDefault {
1749 path: VarPath::simple("MISSING"),
1750 default: vec![StringPart::Literal("fallback".into())],
1751 };
1752 let result = eval_expr(&expr, &mut scope).unwrap();
1753 assert_eq!(result, Value::String("fallback".into()));
1754 }
1755
1756 #[test]
1757 fn eval_var_with_default_empty() {
1758 let mut scope = Scope::new();
1759 scope.set("EMPTY", Value::String("".into()));
1760
1761 let expr = Expr::VarWithDefault {
1763 path: VarPath::simple("EMPTY"),
1764 default: vec![StringPart::Literal("not empty".into())],
1765 };
1766 let result = eval_expr(&expr, &mut scope).unwrap();
1767 assert_eq!(result, Value::String("not empty".into()));
1768 }
1769
1770 #[test]
1771 fn eval_var_with_default_non_string() {
1772 let mut scope = Scope::new();
1773 scope.set("NUM", Value::Int(42));
1774
1775 let expr = Expr::VarWithDefault {
1777 path: VarPath::simple("NUM"),
1778 default: vec![StringPart::Literal("default".into())],
1779 };
1780 let result = eval_expr(&expr, &mut scope).unwrap();
1781 assert_eq!(result, Value::Int(42));
1782 }
1783
1784 #[test]
1785 fn eval_unset_variable_is_empty() {
1786 let mut scope = Scope::new();
1787 let parts = vec![
1788 StringPart::Literal("prefix:".into()),
1789 StringPart::Var(VarPath::simple("UNSET")),
1790 StringPart::Literal(":suffix".into()),
1791 ];
1792 let expr = Expr::Interpolated(parts);
1793 let result = eval_expr(&expr, &mut scope).unwrap();
1794 assert_eq!(result, Value::String("prefix::suffix".into()));
1795 }
1796
1797 #[test]
1798 fn eval_unset_variable_multiple() {
1799 let mut scope = Scope::new();
1800 scope.set("SET", Value::String("hello".into()));
1801 let parts = vec![
1802 StringPart::Var(VarPath::simple("UNSET1")),
1803 StringPart::Literal("-".into()),
1804 StringPart::Var(VarPath::simple("SET")),
1805 StringPart::Literal("-".into()),
1806 StringPart::Var(VarPath::simple("UNSET2")),
1807 ];
1808 let expr = Expr::Interpolated(parts);
1809 let result = eval_expr(&expr, &mut scope).unwrap();
1810 assert_eq!(result, Value::String("-hello-".into()));
1811 }
1812
1813 #[test]
1816 fn values_equal_scalars_still_work() {
1817 assert_eq!(
1818 values_equal(&Value::String("x".into()), &Value::String("x".into())),
1819 Ok(true)
1820 );
1821 assert_eq!(
1823 values_equal(&Value::String("42".into()), &Value::Int(42)),
1824 Ok(true)
1825 );
1826 }
1827
1828 #[test]
1829 fn values_equal_collection_vs_scalar_is_loud() {
1830 let list = Value::Json(serde_json::json!(["a", "b"]));
1831 let record = Value::Json(serde_json::json!({"k": 1}));
1832 assert!(
1833 matches!(values_equal(&list, &Value::String("banana".into())), Err(EvalError::Unsupported(_))),
1834 "list vs scalar must be a loud error, never silently false"
1835 );
1836 assert!(matches!(
1838 values_equal(&Value::String("x".into()), &record),
1839 Err(EvalError::Unsupported(_))
1840 ));
1841 }
1842
1843 #[test]
1844 fn values_equal_collection_vs_collection_is_structural() {
1845 let a = Value::Json(serde_json::json!({"a": 1, "b": 2}));
1847 let b = Value::Json(serde_json::json!({"b": 2, "a": 1}));
1848 assert_eq!(values_equal(&a, &b), Ok(true));
1849 }
1850
1851 #[test]
1852 fn value_length_of_bytes_is_byte_count() {
1853 assert_eq!(value_length(&Value::Bytes(vec![1, 2, 3])), 3);
1854 }
1855
1856 #[test]
1857 fn structured_export_error_flags_collections_passes_scalars() {
1858 let scalars = vec![
1860 ("A".to_string(), Value::String("x".into())),
1861 ("B".to_string(), Value::Int(1)),
1862 ];
1863 assert!(structured_export_error(&scalars).is_none());
1864 let with_record = vec![(
1866 "CFG".to_string(),
1867 Value::Json(serde_json::json!({"port": 8080})),
1868 )];
1869 let msg = structured_export_error(&with_record).expect("record must be refused");
1870 assert!(msg.contains("CFG") && msg.contains("tojson"), "got: {msg}");
1871 let with_list = vec![("XS".to_string(), Value::Json(serde_json::json!([1, 2])))];
1873 assert!(structured_export_error(&with_list).is_some());
1874 }
1875
1876 #[test]
1877 fn defaults_on_emptiness_matches_decision_a() {
1878 assert!(value_defaults_on_emptiness(&Value::Null));
1881 assert!(value_defaults_on_emptiness(&Value::Json(serde_json::Value::Null)));
1882 assert!(value_defaults_on_emptiness(&Value::String(String::new())));
1883 assert!(!value_defaults_on_emptiness(&Value::Bool(false)));
1884 assert!(!value_defaults_on_emptiness(&Value::Int(0)));
1885 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!([]))));
1886 assert!(!value_defaults_on_emptiness(&Value::Json(serde_json::json!({}))));
1887 assert!(!value_defaults_on_emptiness(&Value::String("x".into())));
1888 }
1889
1890 #[test]
1891 fn subscripted_length_and_default_resolve_the_path() {
1892 let mut scope = Scope::new();
1895 scope.set("u", Value::Json(serde_json::json!({"tags": ["a", "b"]})));
1896 let len = eval_expr(
1897 &Expr::VarLength(crate::parser::parse_varpath("${u[tags]}")),
1898 &mut scope,
1899 )
1900 .unwrap();
1901 assert_eq!(len, Value::Int(2));
1902
1903 scope.set("cfg", Value::Json(serde_json::json!({"port": 9000})));
1904 let val = eval_expr(
1906 &Expr::VarWithDefault {
1907 path: crate::parser::parse_varpath("${cfg[port]}"),
1908 default: vec![StringPart::Literal("8080".into())],
1909 },
1910 &mut scope,
1911 )
1912 .unwrap();
1913 assert_eq!(value_to_string(&val), "9000");
1914
1915 let missing = eval_expr(
1917 &Expr::VarWithDefault {
1918 path: crate::parser::parse_varpath("${cfg[nope]}"),
1919 default: vec![StringPart::Literal("8080".into())],
1920 },
1921 &mut scope,
1922 )
1923 .unwrap();
1924 assert_eq!(value_to_string(&missing), "8080");
1925
1926 let err = eval_expr(
1928 &Expr::VarWithDefault {
1929 path: crate::parser::parse_varpath("${cfg[0]}"),
1930 default: vec![StringPart::Literal("x".into())],
1931 },
1932 &mut scope,
1933 )
1934 .unwrap_err();
1935 assert!(matches!(err, EvalError::InvalidPath(_)), "got: {err}");
1936 }
1937}