@tishlang/tish 2.2.5 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/bin/tish +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +5 -1
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/cli_help.rs +12 -0
  5. package/crates/tish/src/main.rs +69 -11
  6. package/crates/tish_ast/src/ast.rs +12 -5
  7. package/crates/tish_build_utils/src/lib.rs +37 -0
  8. package/crates/tish_builtins/src/array.rs +82 -1
  9. package/crates/tish_builtins/src/globals.rs +50 -16
  10. package/crates/tish_builtins/src/math.rs +63 -9
  11. package/crates/tish_builtins/src/number.rs +20 -1
  12. package/crates/tish_builtins/src/string.rs +68 -4
  13. package/crates/tish_builtins/src/typedarrays.rs +23 -16
  14. package/crates/tish_bytecode/src/compiler.rs +94 -28
  15. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
  16. package/crates/tish_compile/src/check.rs +11 -10
  17. package/crates/tish_compile/src/codegen.rs +1386 -42
  18. package/crates/tish_compile/src/infer.rs +16 -16
  19. package/crates/tish_compile/src/lib.rs +38 -0
  20. package/crates/tish_compile/src/resolve.rs +3 -2
  21. package/crates/tish_compile/src/types.rs +26 -9
  22. package/crates/tish_compile_js/src/codegen.rs +55 -4
  23. package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
  24. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  25. package/crates/tish_core/Cargo.toml +2 -0
  26. package/crates/tish_core/src/json.rs +135 -34
  27. package/crates/tish_core/src/lib.rs +23 -1
  28. package/crates/tish_core/src/value.rs +64 -11
  29. package/crates/tish_eval/src/eval.rs +144 -197
  30. package/crates/tish_eval/src/natives.rs +45 -45
  31. package/crates/tish_eval/src/regex.rs +35 -27
  32. package/crates/tish_eval/src/value.rs +8 -0
  33. package/crates/tish_fmt/src/lib.rs +46 -2
  34. package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
  35. package/crates/tish_lint/src/lib.rs +197 -21
  36. package/crates/tish_lsp/Cargo.toml +7 -1
  37. package/crates/tish_lsp/src/import_goto.rs +52 -7
  38. package/crates/tish_lsp/src/main.rs +913 -145
  39. package/crates/tish_opt/src/lib.rs +5 -3
  40. package/crates/tish_parser/src/lib.rs +23 -5
  41. package/crates/tish_parser/src/parser.rs +35 -35
  42. package/crates/tish_resolve/src/lib.rs +567 -17
  43. package/crates/tish_runtime/src/lib.rs +58 -18
  44. package/crates/tish_ui/src/jsx.rs +2 -2
  45. package/crates/tish_vm/src/jit.rs +212 -10
  46. package/crates/tish_vm/src/vm.rs +39 -18
  47. package/crates/tish_wasm/src/lib.rs +116 -22
  48. package/package.json +1 -1
  49. package/platform/darwin-arm64/tish +0 -0
  50. package/platform/darwin-x64/tish +0 -0
  51. package/platform/linux-arm64/tish +0 -0
  52. package/platform/linux-x64/tish +0 -0
  53. package/platform/win32-x64/tish.exe +0 -0
@@ -144,6 +144,11 @@ impl Evaluator {
144
144
  math.insert("round".into(), Value::Native(natives::math_round));
145
145
  math.insert("random".into(), Value::Native(natives::math_random));
146
146
  math.insert("pow".into(), Value::Native(natives::math_pow));
147
+ math.insert("hypot".into(), Value::Native(natives::math_hypot));
148
+ math.insert("asin".into(), Value::Native(natives::math_asin));
149
+ math.insert("acos".into(), Value::Native(natives::math_acos));
150
+ math.insert("atan".into(), Value::Native(natives::math_atan));
151
+ math.insert("atan2".into(), Value::Native(natives::math_atan2));
147
152
  math.insert("sin".into(), Value::Native(natives::math_sin));
148
153
  math.insert("cos".into(), Value::Native(natives::math_cos));
149
154
  math.insert("tan".into(), Value::Native(natives::math_tan));
@@ -180,6 +185,26 @@ impl Evaluator {
180
185
  true,
181
186
  );
182
187
 
188
+ // Bare `process` global (node-compatible), mirroring the VM. `process.argv` reads the
189
+ // configurable argv so `tish run <file> [args...]` reaches the script. #88
190
+ #[cfg(feature = "process")]
191
+ {
192
+ let mut process_obj = PropMap::default();
193
+ process_obj.insert("exit".into(), Value::Native(natives::process_exit));
194
+ process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
195
+ process_obj.insert("exec".into(), Value::Native(natives::process_exec));
196
+ let argv: Vec<Value> = tishlang_core::process_argv()
197
+ .into_iter()
198
+ .map(|s| Value::String(s.into()))
199
+ .collect();
200
+ process_obj.insert("argv".into(), Value::Array(Rc::new(RefCell::new(argv))));
201
+ let env_obj: PropMap = std::env::vars()
202
+ .map(|(k, v)| (Arc::from(k.as_str()), Value::String(v.into())))
203
+ .collect();
204
+ process_obj.insert("env".into(), Value::object(env_obj));
205
+ s.set("process".into(), Value::object(process_obj), true);
206
+ }
207
+
183
208
  let mut object = PropMap::with_capacity(5);
184
209
  object.insert("keys".into(), Value::Native(Self::object_keys));
185
210
  object.insert("values".into(), Value::Native(Self::object_values));
@@ -1032,8 +1057,10 @@ impl Evaluator {
1032
1057
  exports.insert("exit".into(), Value::Native(natives::process_exit));
1033
1058
  exports.insert("cwd".into(), Value::Native(natives::process_cwd));
1034
1059
  exports.insert("exec".into(), Value::Native(natives::process_exec));
1035
- let argv: Vec<Value> =
1036
- std::env::args().map(|s| Value::String(s.into())).collect();
1060
+ let argv: Vec<Value> = tishlang_core::process_argv()
1061
+ .into_iter()
1062
+ .map(|s| Value::String(s.into()))
1063
+ .collect();
1037
1064
  exports.insert(
1038
1065
  "argv".into(),
1039
1066
  Value::Array(Rc::new(RefCell::new(argv.clone()))),
@@ -1106,11 +1133,35 @@ impl Evaluator {
1106
1133
  op,
1107
1134
  right,
1108
1135
  ..
1109
- } => {
1110
- let l = self.eval_expr(left)?;
1111
- let r = self.eval_expr(right)?;
1112
- self.eval_binop(&l, *op, &r).map_err(EvalError::Error)
1113
- }
1136
+ } => match op {
1137
+ // Short-circuit + value-returning && / || (JS, #240): evaluate the left; if it
1138
+ // already decides the result, return IT without evaluating (and running the side
1139
+ // effects of) the right. The result is always an operand value, never a coerced
1140
+ // boolean. The generic path below eagerly evaluated both operands and `eval_binop`
1141
+ // coerced And/Or to `Bool`, so `five() && 7` was `true` and `false && f()` still
1142
+ // ran `f()`.
1143
+ BinOp::And => {
1144
+ let l = self.eval_expr(left)?;
1145
+ if l.is_truthy() {
1146
+ self.eval_expr(right)
1147
+ } else {
1148
+ Ok(l)
1149
+ }
1150
+ }
1151
+ BinOp::Or => {
1152
+ let l = self.eval_expr(left)?;
1153
+ if l.is_truthy() {
1154
+ Ok(l)
1155
+ } else {
1156
+ self.eval_expr(right)
1157
+ }
1158
+ }
1159
+ _ => {
1160
+ let l = self.eval_expr(left)?;
1161
+ let r = self.eval_expr(right)?;
1162
+ self.eval_binop(&l, *op, &r).map_err(EvalError::Error)
1163
+ }
1164
+ },
1114
1165
  Expr::Unary { op, operand, .. } => {
1115
1166
  let o = self.eval_expr(operand)?;
1116
1167
  self.eval_unary(*op, &o).map_err(EvalError::Error)
@@ -1173,7 +1224,11 @@ impl Evaluator {
1173
1224
  _ => 0,
1174
1225
  };
1175
1226
  for v in arr_borrow.iter().skip(start) {
1176
- if v.strict_eq(&search) {
1227
+ // SameValueZero: NaN matches NaN (JS Array.includes, unlike
1228
+ // indexOf). #247
1229
+ if v.strict_eq(&search)
1230
+ || matches!((v, &search), (Value::Number(a), Value::Number(b)) if a.is_nan() && b.is_nan())
1231
+ {
1177
1232
  return Ok(Value::Bool(true));
1178
1233
  }
1179
1234
  }
@@ -1496,6 +1551,54 @@ impl Evaluator {
1496
1551
  }
1497
1552
  return Ok(Value::Number(-1.0));
1498
1553
  }
1554
+ "findLast" => {
1555
+ // Like find, from the end (#247). Callback gets (value, original index).
1556
+ let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
1557
+ let arr_borrow = arr.borrow();
1558
+ let scoped = self.create_callback_scope(&callback);
1559
+ for i in (0..arr_borrow.len()).rev() {
1560
+ let v = arr_borrow[i].clone();
1561
+ let args = [v.clone(), Value::Number(i as f64)];
1562
+ let found = match &scoped {
1563
+ Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
1564
+ None => self.call_func(&callback, &args)?,
1565
+ };
1566
+ if found.is_truthy() {
1567
+ return Ok(v);
1568
+ }
1569
+ }
1570
+ return Ok(Value::Null);
1571
+ }
1572
+ "findLastIndex" => {
1573
+ let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
1574
+ let arr_borrow = arr.borrow();
1575
+ let scoped = self.create_callback_scope(&callback);
1576
+ for i in (0..arr_borrow.len()).rev() {
1577
+ let args = [arr_borrow[i].clone(), Value::Number(i as f64)];
1578
+ let found = match &scoped {
1579
+ Some((scope, params, body)) => self.call_with_scope(scope, params, body, &args)?,
1580
+ None => self.call_func(&callback, &args)?,
1581
+ };
1582
+ if found.is_truthy() {
1583
+ return Ok(Value::Number(i as f64));
1584
+ }
1585
+ }
1586
+ return Ok(Value::Number(-1.0));
1587
+ }
1588
+ "at" => {
1589
+ // Negative index counts from the end; out of range → null (#247).
1590
+ let i = match arg_vals.first() {
1591
+ Some(Value::Number(n)) => *n as i64,
1592
+ _ => 0,
1593
+ };
1594
+ let arr_borrow = arr.borrow();
1595
+ let len = arr_borrow.len() as i64;
1596
+ let idx = if i < 0 { len + i } else { i };
1597
+ if idx >= 0 && idx < len {
1598
+ return Ok(arr_borrow[idx as usize].clone());
1599
+ }
1600
+ return Ok(Value::Null);
1601
+ }
1499
1602
  "forEach" => {
1500
1603
  let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
1501
1604
  let arr_borrow = arr.borrow();
@@ -1788,6 +1891,20 @@ impl Evaluator {
1788
1891
  .map(|c| Value::String(c.to_string().into()))
1789
1892
  .unwrap_or(Value::String("".into())));
1790
1893
  }
1894
+ "at" => {
1895
+ // Negative index from end; out of range → null (#247).
1896
+ let i = match arg_vals.first() {
1897
+ Some(Value::Number(n)) => *n as i64,
1898
+ _ => 0,
1899
+ };
1900
+ let chars: Vec<char> = s.chars().collect();
1901
+ let len = chars.len() as i64;
1902
+ let idx = if i < 0 { len + i } else { i };
1903
+ if idx >= 0 && idx < len {
1904
+ return Ok(Value::String(chars[idx as usize].to_string().into()));
1905
+ }
1906
+ return Ok(Value::Null);
1907
+ }
1791
1908
  "charCodeAt" => {
1792
1909
  let idx = match arg_vals.first() {
1793
1910
  Some(Value::Number(n)) => *n as usize,
@@ -1868,7 +1985,8 @@ impl Evaluator {
1868
1985
  })
1869
1986
  .unwrap_or(0)
1870
1987
  .clamp(0, 20); // ECMA-262: 0–20
1871
- let formatted = format!("{:.*}", digits as usize, n);
1988
+ // Shared half-away-from-zero rounding so interp matches vm/native/node (#247).
1989
+ let formatted = tishlang_builtins::number::to_fixed_str(*n, digits as usize);
1872
1990
  return Ok(Value::String(formatted.into()));
1873
1991
  }
1874
1992
  if method_name.as_ref() == "toString" {
@@ -2011,7 +2129,7 @@ impl Evaluator {
2011
2129
  let mut data = EvalObjectData::default();
2012
2130
  for prop in props {
2013
2131
  match prop {
2014
- tishlang_ast::ObjectProp::KeyValue(k, v) => {
2132
+ tishlang_ast::ObjectProp::KeyValue(k, v, _) => {
2015
2133
  data
2016
2134
  .strings
2017
2135
  .insert(Arc::clone(k), self.eval_expr(v)?);
@@ -3546,188 +3664,16 @@ impl Evaluator {
3546
3664
  }
3547
3665
 
3548
3666
  fn json_parse(s: &str) -> Value {
3549
- let s = s.trim();
3550
- if s.is_empty() {
3551
- return Value::Null;
3552
- }
3553
- match Self::json_parse_str(s) {
3554
- Ok(v) => v,
3555
- Err(()) => Value::Null,
3556
- }
3557
- }
3558
-
3559
- fn json_parse_str(s: &str) -> Result<Value, ()> {
3560
- let s = s.trim();
3561
- if s.is_empty() {
3562
- return Err(());
3563
- }
3564
- if s == "null" {
3565
- return Ok(Value::Null);
3566
- }
3567
- if s == "true" {
3568
- return Ok(Value::Bool(true));
3569
- }
3570
- if s == "false" {
3571
- return Ok(Value::Bool(false));
3572
- }
3573
- if s.starts_with('"') {
3574
- return Self::json_parse_string_full(s);
3575
- }
3576
- if s.starts_with('[') {
3577
- return Self::json_parse_array(s);
3578
- }
3579
- if s.starts_with('{') {
3580
- return Self::json_parse_object(s);
3581
- }
3582
- if let Ok(n) = s.parse::<f64>() {
3583
- return Ok(Value::Number(n));
3584
- }
3585
- Err(())
3586
- }
3587
-
3588
- fn json_parse_string(s: &str) -> Result<(Value, &str), ()> {
3589
- let s = &s[1..];
3590
- let mut out = String::new();
3591
- let mut i = 0;
3592
- let chars: Vec<char> = s.chars().collect();
3593
- while i < chars.len() {
3594
- if chars[i] == '"' {
3595
- let rest_start = s.chars().take(i + 1).map(|c| c.len_utf8()).sum::<usize>();
3596
- return Ok((Value::String(out.into()), &s[rest_start..]));
3597
- }
3598
- if chars[i] == '\\' {
3599
- i += 1;
3600
- if i >= chars.len() {
3601
- return Err(());
3602
- }
3603
- match chars[i] {
3604
- '"' => out.push('"'),
3605
- '\\' => out.push('\\'),
3606
- 'n' => out.push('\n'),
3607
- 'r' => out.push('\r'),
3608
- 't' => out.push('\t'),
3609
- _ => return Err(()),
3610
- }
3611
- } else {
3612
- out.push(chars[i]);
3613
- }
3614
- i += 1;
3615
- }
3616
- Err(())
3617
- }
3618
-
3619
- fn json_parse_string_full(s: &str) -> Result<Value, ()> {
3620
- Self::json_parse_string(s).map(|(v, _)| v)
3621
- }
3622
-
3623
- fn json_parse_array(s: &str) -> Result<Value, ()> {
3624
- let s = s[1..].trim_start();
3625
- if s.starts_with(']') {
3626
- return Ok(Value::Array(Rc::new(RefCell::new(vec![]))));
3627
- }
3628
- let mut vals = Vec::new();
3629
- let mut rest = s;
3630
- loop {
3631
- let (v, next) = Self::json_parse_one(rest)?;
3632
- vals.push(v);
3633
- rest = next.trim_start();
3634
- if rest.starts_with(']') {
3635
- break;
3636
- }
3637
- if !rest.starts_with(',') {
3638
- return Err(());
3639
- }
3640
- rest = rest[1..].trim_start();
3641
- }
3642
- Ok(Value::Array(Rc::new(RefCell::new(vals))))
3643
- }
3644
-
3645
- fn json_parse_object(s: &str) -> Result<Value, ()> {
3646
- let s = s[1..].trim_start();
3647
- if s.starts_with('}') {
3648
- return Ok(Value::object(PropMap::default()));
3649
- }
3650
- let mut map = PropMap::default();
3651
- let mut rest = s;
3652
- loop {
3653
- if !rest.starts_with('"') {
3654
- return Err(());
3655
- }
3656
- let (key_val, next) = Self::json_parse_string(rest)?;
3657
- let key = match &key_val {
3658
- Value::String(k) => Arc::clone(k),
3659
- _ => return Err(()),
3660
- };
3661
- rest = next.trim_start();
3662
- if !rest.starts_with(':') {
3663
- return Err(());
3664
- }
3665
- rest = rest[1..].trim_start();
3666
- let (val, next) = Self::json_parse_one(rest)?;
3667
- map.insert(key, val);
3668
- rest = next.trim_start();
3669
- if rest.starts_with('}') {
3670
- break;
3671
- }
3672
- if !rest.starts_with(',') {
3673
- return Err(());
3674
- }
3675
- rest = rest[1..].trim_start();
3676
- }
3677
- Ok(Value::object(map))
3678
- }
3679
-
3680
- fn json_parse_one(s: &str) -> Result<(Value, &str), ()> {
3681
- let s = s.trim();
3682
- if s.is_empty() {
3683
- return Err(());
3684
- }
3685
- if s.starts_with('"') {
3686
- let (v, rest) = Self::json_parse_string(s)?;
3687
- Ok((v, rest))
3688
- } else if s.starts_with('[') {
3689
- let mut depth = 0;
3690
- for (i, c) in s.char_indices() {
3691
- if c == '[' {
3692
- depth += 1;
3693
- } else if c == ']' {
3694
- depth -= 1;
3695
- if depth == 0 {
3696
- let v = Self::json_parse_array(&s[..=i])?;
3697
- return Ok((v, &s[i + c.len_utf8()..]));
3698
- }
3699
- }
3700
- }
3701
- Err(())
3702
- } else if s.starts_with('{') {
3703
- let mut depth = 0;
3704
- for (i, c) in s.char_indices() {
3705
- if c == '{' {
3706
- depth += 1;
3707
- } else if c == '}' {
3708
- depth -= 1;
3709
- if depth == 0 {
3710
- let v = Self::json_parse_object(&s[..=i])?;
3711
- return Ok((v, &s[i + c.len_utf8()..]));
3712
- }
3713
- }
3714
- }
3715
- Err(())
3716
- } else if let Some(rest) = s.strip_prefix("null") {
3717
- Ok((Value::Null, rest))
3718
- } else if let Some(rest) = s.strip_prefix("true") {
3719
- Ok((Value::Bool(true), rest))
3720
- } else if let Some(rest) = s.strip_prefix("false") {
3721
- Ok((Value::Bool(false), rest))
3722
- } else {
3723
- let end = s
3724
- .find(|c: char| {
3725
- !c.is_ascii_digit() && c != '-' && c != '+' && c != '.' && c != 'e' && c != 'E'
3726
- })
3727
- .unwrap_or(s.len());
3728
- let num_str = &s[..end];
3729
- let n: f64 = num_str.parse().map_err(|_| ())?;
3730
- Ok((Value::Number(n), &s[end..]))
3667
+ // Delegate to the shared, spec-correct parser in `tishlang_core` and convert into
3668
+ // interpreter Values — one source of truth with the VM/native/cranelift/wasi backends.
3669
+ // The previous hand-rolled parser depth-counted `{}`/`[]` brackets WITHOUT skipping string
3670
+ // contents, so a nested value whose string held `}`/`]`/`{`/`[` (e.g. `{"a":{"s":"}"}}`)
3671
+ // mis-sliced and the whole parse failed to `null` where Node/VM succeed; it also re-scanned
3672
+ // each nested value O(n^2). The core parser is string-aware and builds insertion-ordered
3673
+ // PropMaps. Invalid input still yields `null` (unchanged interpreter behavior).
3674
+ match tishlang_core::json_parse(s) {
3675
+ Ok(core) => crate::value_convert::core_to_eval(core),
3676
+ Err(_) => Value::Null,
3731
3677
  }
3732
3678
  }
3733
3679
 
@@ -3736,11 +3682,12 @@ impl Evaluator {
3736
3682
  Value::Null => "null".to_string(),
3737
3683
  Value::Bool(b) => b.to_string(),
3738
3684
  Value::Number(n) => {
3739
- if n.is_finite() {
3740
- n.to_string()
3741
- } else {
3742
- "null".to_string()
3743
- }
3685
+ // Format numbers exactly like the VM/native path (shared helper) so JSON output
3686
+ // matches across backends and Node — Rust's `{}` Display diverges (e.g. `1e21`,
3687
+ // `1e-7`, `-0`). #180.
3688
+ let mut s = String::new();
3689
+ tishlang_core::write_json_number(&mut s, *n);
3690
+ s
3744
3691
  }
3745
3692
  Value::String(s) => format!(
3746
3693
  "\"{}\"",
@@ -56,26 +56,14 @@ fn get_log_level() -> u8 {
56
56
 
57
57
  pub fn parse_int(args: &[Value]) -> Result<Value, String> {
58
58
  let s = args.first().map(|v| v.to_string()).unwrap_or_default();
59
- let s = s.trim();
60
- let radix = args
61
- .get(1)
62
- .and_then(|v| match v {
63
- Value::Number(n) => Some(*n as i32),
64
- _ => None,
65
- })
66
- .unwrap_or(10);
67
- let n = if (2..=36).contains(&radix) {
68
- let prefix: String = s
69
- .chars()
70
- .take_while(|c| *c == '-' || *c == '+' || c.is_digit(radix as u32))
71
- .collect();
72
- i64::from_str_radix(&prefix, radix as u32)
73
- .ok()
74
- .map(|n| n as f64)
75
- } else {
76
- None
77
- };
78
- Ok(Value::Number(n.unwrap_or(f64::NAN)))
59
+ let radix = args.get(1).and_then(|v| match v {
60
+ Value::Number(n) => Some(*n as i32),
61
+ _ => None,
62
+ });
63
+ // Shared semantics (0x stripping, sign, auto-radix) see js_parse_int (#247).
64
+ Ok(Value::Number(tishlang_builtins::globals::js_parse_int(
65
+ &s, radix,
66
+ )))
79
67
  }
80
68
 
81
69
  pub fn parse_float(args: &[Value]) -> Result<Value, String> {
@@ -168,32 +156,44 @@ pub fn math_sqrt(args: &[Value]) -> Result<Value, String> {
168
156
  ))
169
157
  }
170
158
 
159
+ // The JS-specific min/max/round semantics live in the shared `tishlang_builtins::math` f64 helpers
160
+ // (#247) — the interpreter uses a different `Value` type, so it extracts its own args and calls those
161
+ // helpers, keeping interp/vm/native bit-identical without duplicating the rules.
171
162
  pub fn math_min(args: &[Value]) -> Result<Value, String> {
172
- let nums: Vec<f64> = args
173
- .iter()
174
- .filter_map(|v| match v {
175
- Value::Number(n) => Some(*n),
176
- _ => None,
177
- })
178
- .collect();
179
- let n = nums.into_iter().fold(f64::INFINITY, f64::min);
180
- Ok(Value::Number(if n == f64::INFINITY { f64::NAN } else { n }))
163
+ let nums: Vec<f64> = args.iter().map(get_num).collect();
164
+ Ok(Value::Number(tishlang_builtins::math::min_f64(&nums)))
181
165
  }
182
166
 
183
167
  pub fn math_max(args: &[Value]) -> Result<Value, String> {
184
- let nums: Vec<f64> = args
185
- .iter()
186
- .filter_map(|v| match v {
187
- Value::Number(n) => Some(*n),
188
- _ => None,
189
- })
190
- .collect();
191
- let n = nums.into_iter().fold(f64::NEG_INFINITY, f64::max);
192
- Ok(Value::Number(if n == f64::NEG_INFINITY {
193
- f64::NAN
194
- } else {
195
- n
196
- }))
168
+ let nums: Vec<f64> = args.iter().map(get_num).collect();
169
+ Ok(Value::Number(tishlang_builtins::math::max_f64(&nums)))
170
+ }
171
+
172
+ // hypot/asin/acos/atan/atan2 existed on the vm's Math but not the interpreter's — a direct interp≠vm
173
+ // gap (#247). These are 1:1 with Rust f64 methods (same as the builtins), so compute directly; hypot
174
+ // defaults missing args to 0.0 to match `tishlang_builtins::math::hypot`.
175
+ pub fn math_hypot(args: &[Value]) -> Result<Value, String> {
176
+ let x = args.first().map(get_num).unwrap_or(0.0);
177
+ let y = args.get(1).map(get_num).unwrap_or(0.0);
178
+ Ok(Value::Number(x.hypot(y)))
179
+ }
180
+
181
+ pub fn math_asin(args: &[Value]) -> Result<Value, String> {
182
+ Ok(Value::Number(get_num(args.first().unwrap_or(&Value::Null)).asin()))
183
+ }
184
+
185
+ pub fn math_acos(args: &[Value]) -> Result<Value, String> {
186
+ Ok(Value::Number(get_num(args.first().unwrap_or(&Value::Null)).acos()))
187
+ }
188
+
189
+ pub fn math_atan(args: &[Value]) -> Result<Value, String> {
190
+ Ok(Value::Number(get_num(args.first().unwrap_or(&Value::Null)).atan()))
191
+ }
192
+
193
+ pub fn math_atan2(args: &[Value]) -> Result<Value, String> {
194
+ let y = get_num(args.first().unwrap_or(&Value::Null));
195
+ let x = get_num(args.get(1).unwrap_or(&Value::Null));
196
+ Ok(Value::Number(y.atan2(x)))
197
197
  }
198
198
 
199
199
  pub fn math_floor(args: &[Value]) -> Result<Value, String> {
@@ -209,9 +209,9 @@ pub fn math_ceil(args: &[Value]) -> Result<Value, String> {
209
209
  }
210
210
 
211
211
  pub fn math_round(args: &[Value]) -> Result<Value, String> {
212
- Ok(Value::Number(
213
- get_num(args.first().unwrap_or(&Value::Null)).round(),
214
- ))
212
+ Ok(Value::Number(tishlang_builtins::math::round_f64(get_num(
213
+ args.first().unwrap_or(&Value::Null),
214
+ ))))
215
215
  }
216
216
 
217
217
  pub fn math_random(_args: &[Value]) -> Result<Value, String> {
@@ -254,46 +254,54 @@ pub fn string_split(input: &str, separator: &Value, limit: Option<usize>) -> Val
254
254
  return Value::Array(Rc::new(RefCell::new(Vec::new())));
255
255
  }
256
256
 
257
- match separator {
257
+ // JS semantics in every branch: split the string fully, then truncate the result to `limit`.
258
+ // (Rust's `splitn` keeps the unsplit remainder in the last slot — that's Python-style maxsplit,
259
+ // not JS — so we must split fully and `truncate`. `truncate(usize::MAX)` is a no-op for the
260
+ // no-limit case.) This mirrors `tish_builtins::string::split_limit` /
261
+ // `tishlang_runtime::string_split_regex` so interp, vm, and the native backends agree.
262
+ let mut result: Vec<Value> = match separator {
258
263
  Value::RegExp(re) => {
259
264
  let re = re.borrow();
260
- let mut result = Vec::new();
265
+ let mut parts = Vec::new();
261
266
  let mut last_end = 0;
262
-
263
267
  for mat in re.regex.find_iter(input) {
264
268
  match mat {
265
269
  Ok(m) => {
266
- if result.len() >= max - 1 {
267
- break;
268
- }
269
- result.push(Value::String(input[last_end..m.start()].into()));
270
+ parts.push(Value::String(input[last_end..m.start()].into()));
270
271
  last_end = m.end();
271
272
  }
272
273
  Err(_) => break,
273
274
  }
274
275
  }
275
-
276
- if result.len() < max {
277
- result.push(Value::String(input[last_end..].into()));
278
- }
279
-
280
- Value::Array(Rc::new(RefCell::new(result)))
281
- }
282
- Value::String(sep) => {
283
- let parts: Vec<Value> = input
284
- .splitn(max, sep.as_ref())
285
- .map(|s| Value::String(s.into()))
286
- .collect();
287
- Value::Array(Rc::new(RefCell::new(parts)))
276
+ parts.push(Value::String(input[last_end..].into()));
277
+ parts
288
278
  }
289
- Value::Null => Value::Array(Rc::new(RefCell::new(vec![Value::String(input.into())]))),
279
+ // JS `split("")` → the string's chars with no surrounding empties (`"".split("")` → `[]`),
280
+ // unlike Rust's `str::split("")`. Mirrors `tish_builtins::string::split_limit`. #247
281
+ Value::String(sep) if sep.is_empty() => input
282
+ .chars()
283
+ .map(|c| Value::String(c.to_string().into()))
284
+ .collect(),
285
+ Value::String(sep) => input
286
+ .split(sep.as_ref())
287
+ .map(|s| Value::String(s.into()))
288
+ .collect(),
289
+ Value::Null => vec![Value::String(input.into())],
290
290
  _ => {
291
291
  let sep_str = separator.to_string();
292
- let parts: Vec<Value> = input
293
- .splitn(max, &sep_str)
294
- .map(|s| Value::String(s.into()))
295
- .collect();
296
- Value::Array(Rc::new(RefCell::new(parts)))
292
+ if sep_str.is_empty() {
293
+ input
294
+ .chars()
295
+ .map(|c| Value::String(c.to_string().into()))
296
+ .collect()
297
+ } else {
298
+ input
299
+ .split(&sep_str)
300
+ .map(|s| Value::String(s.into()))
301
+ .collect()
302
+ }
297
303
  }
298
- }
304
+ };
305
+ result.truncate(max);
306
+ Value::Array(Rc::new(RefCell::new(result)))
299
307
  }
@@ -197,6 +197,10 @@ impl std::fmt::Debug for Value {
197
197
  impl std::fmt::Display for Value {
198
198
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
199
199
  match self {
200
+ // Inspect form (`console.log`): keeps the sign of negative zero, unlike the ECMAScript
201
+ // ToString used by `to_js_string`. Matches Node's console output (`console.log(-0)` →
202
+ // `-0`). `to_js_string` has an explicit `Number` arm so ToString still drops it. (#247)
203
+ Value::Number(n) if *n == 0.0 && n.is_sign_negative() => write!(f, "-0"),
200
204
  // Match JS `Number.prototype.toString` (exponential past digit 21 / before −6),
201
205
  // shared with the VM/native path via `tishlang_core`.
202
206
  Value::Number(n) => write!(f, "{}", tishlang_core::js_number_to_string(*n)),
@@ -279,6 +283,10 @@ impl Value {
279
283
  .collect::<Vec<_>>()
280
284
  .join(","),
281
285
  Value::Object(_) => "[object Object]".to_string(),
286
+ // ECMAScript ToString of a number drops `-0`'s sign (`String(-0) === "0"`), distinct
287
+ // from the inspect `Display` above which keeps it. Explicit arm so the `_` fallback to
288
+ // `to_string()` (inspect) is not used for numbers. (#247)
289
+ Value::Number(n) => tishlang_core::js_number_to_string(*n),
282
290
  _ => self.to_string(),
283
291
  }
284
292
  }