@tishlang/tish 2.2.7 → 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 (52) 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 +6 -2
  7. package/crates/tish_builtins/src/array.rs +82 -1
  8. package/crates/tish_builtins/src/globals.rs +50 -16
  9. package/crates/tish_builtins/src/math.rs +63 -9
  10. package/crates/tish_builtins/src/number.rs +20 -1
  11. package/crates/tish_builtins/src/string.rs +68 -4
  12. package/crates/tish_builtins/src/typedarrays.rs +23 -16
  13. package/crates/tish_bytecode/src/compiler.rs +94 -28
  14. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +19 -0
  15. package/crates/tish_compile/src/check.rs +1 -1
  16. package/crates/tish_compile/src/codegen.rs +1386 -42
  17. package/crates/tish_compile/src/infer.rs +4 -4
  18. package/crates/tish_compile/src/lib.rs +38 -0
  19. package/crates/tish_compile/src/resolve.rs +3 -2
  20. package/crates/tish_compile/src/types.rs +17 -0
  21. package/crates/tish_compile_js/src/codegen.rs +55 -4
  22. package/crates/tish_compile_js/src/tests_jsx.rs +44 -0
  23. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  24. package/crates/tish_core/Cargo.toml +2 -0
  25. package/crates/tish_core/src/json.rs +135 -34
  26. package/crates/tish_core/src/lib.rs +23 -1
  27. package/crates/tish_core/src/value.rs +64 -11
  28. package/crates/tish_eval/src/eval.rs +144 -197
  29. package/crates/tish_eval/src/natives.rs +45 -45
  30. package/crates/tish_eval/src/regex.rs +35 -27
  31. package/crates/tish_eval/src/value.rs +8 -0
  32. package/crates/tish_fmt/src/lib.rs +45 -1
  33. package/crates/tish_lint/src/bin/tish-lint.rs +17 -8
  34. package/crates/tish_lint/src/lib.rs +197 -21
  35. package/crates/tish_lsp/Cargo.toml +6 -0
  36. package/crates/tish_lsp/src/import_goto.rs +52 -7
  37. package/crates/tish_lsp/src/main.rs +856 -140
  38. package/crates/tish_opt/src/lib.rs +5 -3
  39. package/crates/tish_parser/src/lib.rs +23 -5
  40. package/crates/tish_parser/src/parser.rs +15 -26
  41. package/crates/tish_resolve/src/lib.rs +188 -18
  42. package/crates/tish_runtime/src/lib.rs +58 -18
  43. package/crates/tish_ui/src/jsx.rs +2 -2
  44. package/crates/tish_vm/src/jit.rs +212 -10
  45. package/crates/tish_vm/src/vm.rs +39 -18
  46. package/crates/tish_wasm/src/lib.rs +116 -22
  47. package/package.json +1 -1
  48. package/platform/darwin-arm64/tish +0 -0
  49. package/platform/darwin-x64/tish +0 -0
  50. package/platform/linux-arm64/tish +0 -0
  51. package/platform/linux-x64/tish +0 -0
  52. package/platform/win32-x64/tish.exe +0 -0
@@ -834,6 +834,31 @@ pub fn to_uint32(x: f64) -> u32 {
834
834
  }
835
835
  }
836
836
 
837
+ /// `ToNumber` for the native boxed runtime: a `Value::Number` passes through, every other variant
838
+ /// coerces to `NaN`. This is the same `as_number().unwrap_or(NaN)` convention used by the
839
+ /// interpreter (`binop_number` / `to_int32`), the VM (`eval_binop`), and `ops::add`/`sub`/`mul`/`div`
840
+ /// — so the native backend stays bit-for-bit in lock-step with them at runtime (a string/bool/null
841
+ /// operand is `NaN`, hence `0` for bitwise). The compiler constant-folds literal cases like
842
+ /// `"5" | 0 === 5` separately, so this only governs runtime (non-constant) operands.
843
+ #[inline]
844
+ pub fn to_number_value(v: &Value) -> f64 {
845
+ v.as_number().unwrap_or(f64::NAN)
846
+ }
847
+
848
+ /// `ToInt32` of an arbitrary [`Value`] (coerce via [`to_number_value`], then [`to_int32`]). Designed
849
+ /// to compose in generated code: unlike a `let Value::Number(a) = &(..) else { panic!() }` block,
850
+ /// it binds no name, so nested bitwise/shift operands can never shadow each other.
851
+ #[inline]
852
+ pub fn to_int32_value(v: &Value) -> i32 {
853
+ to_int32(to_number_value(v))
854
+ }
855
+
856
+ /// `ToUint32` companion to [`to_int32_value`].
857
+ #[inline]
858
+ pub fn to_uint32_value(v: &Value) -> u32 {
859
+ to_uint32(to_number_value(v))
860
+ }
861
+
837
862
  /// Invoke a callable [`Value`]: [`Value::Function`], or an object exposing `__call` (e.g. `Symbol`).
838
863
  pub fn value_call(callee: &Value, args: &[Value]) -> Value {
839
864
  match callee {
@@ -910,20 +935,36 @@ impl std::fmt::Debug for Value {
910
935
  /// We take the shortest round-tripping digits from Rust's `{:e}` (a Ryū/Grisu-class shortest
911
936
  /// formatter, matching V8's digit choice) and lay them out per the ECMAScript rule: plain
912
937
  /// decimal when the point position `n` is in `(-6, 21]`, otherwise `d[.ddd]e±E` with `E = n-1`
913
- /// (sign always shown, no leading zeros in the exponent). `-0` renders as `"-0"` (matching
914
- /// `console.log` and tish's existing behavior).
938
+ /// (sign always shown, no leading zeros in the exponent). This is the ECMAScript `Number::toString`
939
+ /// used by `.toString()`, `String(n)`, `+` concatenation, and template literals, so `-0` renders as
940
+ /// `"0"` (the sign is dropped — `(-0).toString() === "0"`). The *inspect* form (`console.log` of a
941
+ /// bare number / array element) keeps `"-0"`; that distinction lives in [`Value::to_display_string`].
915
942
  pub fn js_number_to_string(value: f64) -> String {
943
+ let mut out = String::new();
944
+ js_number_to_string_into(&mut out, value);
945
+ out
946
+ }
947
+
948
+ /// Append the ECMAScript `Number::toString` of `value` to `out` — identical result to
949
+ /// [`js_number_to_string`] but with no intermediate `String` allocation. The JSON.stringify
950
+ /// hot path appends millions of numbers into a single buffer, so the alloc-free form matters.
951
+ pub fn js_number_to_string_into(out: &mut String, value: f64) {
916
952
  if value.is_nan() {
917
- return "NaN".to_string();
953
+ out.push_str("NaN");
954
+ return;
918
955
  }
919
956
  if value == f64::INFINITY {
920
- return "Infinity".to_string();
957
+ out.push_str("Infinity");
958
+ return;
921
959
  }
922
960
  if value == f64::NEG_INFINITY {
923
- return "-Infinity".to_string();
961
+ out.push_str("-Infinity");
962
+ return;
924
963
  }
925
964
  if value == 0.0 {
926
- return if value.is_sign_negative() { "-0" } else { "0" }.to_string();
965
+ // ECMAScript `Number::toString`: both `+0` and `-0` stringify to `"0"`.
966
+ out.push('0');
967
+ return;
927
968
  }
928
969
 
929
970
  let negative = value < 0.0;
@@ -939,7 +980,6 @@ pub fn js_number_to_string(value: f64) -> String {
939
980
  let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
940
981
  let point = exp + 1; // ECMAScript's `n`: value = digits × 10^(point − k)
941
982
 
942
- let mut out = String::new();
943
983
  if negative {
944
984
  out.push('-');
945
985
  }
@@ -969,13 +1009,15 @@ pub fn js_number_to_string(value: f64) -> String {
969
1009
  out.push(if e >= 0 { '+' } else { '-' });
970
1010
  out.push_str(&e.abs().to_string());
971
1011
  }
972
- out
973
1012
  }
974
1013
 
975
1014
  impl Value {
976
1015
  /// Convert value to display string (for console output).
977
1016
  pub fn to_display_string(&self) -> String {
978
1017
  match self {
1018
+ // Inspect form keeps the sign of negative zero (`console.log(-0)` → `-0`), unlike the
1019
+ // ECMAScript ToString used by `to_js_string`. See `js_number_to_string`. (#247)
1020
+ Value::Number(n) if *n == 0.0 && n.is_sign_negative() => "-0".to_string(),
979
1021
  Value::Number(n) => js_number_to_string(*n),
980
1022
  Value::String(s) => s.to_string(),
981
1023
  Value::Bool(b) => b.to_string(),
@@ -1046,6 +1088,9 @@ impl Value {
1046
1088
  .collect::<Vec<_>>()
1047
1089
  .join(","),
1048
1090
  Value::Object(_) => "[object Object]".to_string(),
1091
+ // ECMAScript ToString of a number (drops `-0`'s sign), distinct from the inspect form
1092
+ // that `to_display_string` would give for `-0`. (#247)
1093
+ Value::Number(n) => js_number_to_string(*n),
1049
1094
  // Primitives (and the remaining cases) coincide with the display form.
1050
1095
  _ => self.to_display_string(),
1051
1096
  }
@@ -1148,12 +1193,18 @@ impl Value {
1148
1193
  // -------------------------------------------------------------------------
1149
1194
 
1150
1195
  /// Whether packed f64 arrays are enabled this run. Default: **off** (`TISH_PACKED_ARRAYS=1`
1151
- /// opts in). Checked at every creation site so flag changes take effect per-process.
1196
+ /// opts in). Read once per process and cached the VM calls this once per executed array
1197
+ /// literal, and a `std::env::var` there is a libc env lock plus a `String` allocation per
1198
+ /// `NewArray` for a flag that never changes after startup (#166). Set the variable before the
1199
+ /// process starts (as the CI sweep does); mid-process toggling is not observed by design.
1152
1200
  /// The flag is intentionally backwards from the slot/JIT flags (those were default-on) to
1153
1201
  /// keep the default binary behaviour byte-identical while we validate coverage.
1154
1202
  #[inline]
1155
1203
  pub fn packed_arrays_enabled() -> bool {
1156
- std::env::var("TISH_PACKED_ARRAYS").map(|v| v == "1").unwrap_or(false)
1204
+ static PACKED_ARRAYS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1205
+ *PACKED_ARRAYS.get_or_init(|| {
1206
+ std::env::var("TISH_PACKED_ARRAYS").map(|v| v == "1").unwrap_or(false)
1207
+ })
1157
1208
  }
1158
1209
 
1159
1210
  /// Wrap a `Vec<f64>` as a `Value::NumberArray`. Only call when `packed_arrays_enabled()`.
@@ -1310,9 +1361,11 @@ mod number_to_string_tests {
1310
1361
  #[test]
1311
1362
  fn matches_javascript_number_tostring() {
1312
1363
  // (value, expected) — every `expected` is what Node's `String(value)` produces.
1364
+ // `String(-0) === "0"`: ToString drops the sign of negative zero (the inspect form keeps
1365
+ // it, but that path is `Value::to_display_string`, not this function). (#247)
1313
1366
  let cases: &[(f64, &str)] = &[
1314
1367
  (0.0, "0"),
1315
- (-0.0, "-0"),
1368
+ (-0.0, "0"),
1316
1369
  (123.0, "123"),
1317
1370
  (123.456, "123.456"),
1318
1371
  (0.5, "0.5"),
@@ -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> {