@tishlang/tish-format 2.12.0 → 2.35.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 (88) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  4. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  5. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  6. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/integration_test.rs +80 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +84 -0
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +724 -72
  13. package/crates/tish_builtins/src/collections.rs +149 -40
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +263 -23
  18. package/crates/tish_bytecode/src/chunk.rs +7 -0
  19. package/crates/tish_bytecode/src/compiler.rs +611 -63
  20. package/crates/tish_bytecode/src/lib.rs +1 -1
  21. package/crates/tish_bytecode/src/opcode.rs +156 -2
  22. package/crates/tish_bytecode/src/serialize.rs +2 -0
  23. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  24. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  25. package/crates/tish_compile/src/codegen.rs +18219 -5283
  26. package/crates/tish_compile/src/infer.rs +1562 -30
  27. package/crates/tish_compile/src/lib.rs +29 -4
  28. package/crates/tish_compile/src/resolve.rs +151 -13
  29. package/crates/tish_compile/src/types.rs +224 -28
  30. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  31. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  32. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  33. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  34. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  35. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  36. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  37. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  38. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  39. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  40. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  41. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  42. package/crates/tish_compile_js/src/codegen.rs +94 -3
  43. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  44. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
  45. package/crates/tish_core/Cargo.toml +4 -0
  46. package/crates/tish_core/src/json.rs +109 -6
  47. package/crates/tish_core/src/lib.rs +222 -2
  48. package/crates/tish_core/src/shape.rs +4 -2
  49. package/crates/tish_core/src/uri.rs +45 -0
  50. package/crates/tish_core/src/value.rs +571 -35
  51. package/crates/tish_core/src/vmref.rs +14 -0
  52. package/crates/tish_eval/Cargo.toml +2 -1
  53. package/crates/tish_eval/src/eval.rs +1328 -101
  54. package/crates/tish_eval/src/natives.rs +283 -18
  55. package/crates/tish_eval/src/regex.rs +47 -1
  56. package/crates/tish_eval/src/value.rs +76 -21
  57. package/crates/tish_ffi/src/lib.rs +11 -1
  58. package/crates/tish_ffi/tests/double_free.rs +35 -0
  59. package/crates/tish_fmt/Cargo.toml +1 -1
  60. package/crates/tish_fmt/src/lib.rs +94 -1
  61. package/crates/tish_lexer/src/lib.rs +76 -0
  62. package/crates/tish_lexer/src/token.rs +4 -0
  63. package/crates/tish_lint/src/lib.rs +126 -0
  64. package/crates/tish_lsp/README.md +2 -1
  65. package/crates/tish_lsp/src/main.rs +378 -28
  66. package/crates/tish_native/src/build.rs +41 -0
  67. package/crates/tish_opt/src/lib.rs +20 -1
  68. package/crates/tish_parser/Cargo.toml +4 -0
  69. package/crates/tish_parser/src/lib.rs +68 -0
  70. package/crates/tish_parser/src/parser.rs +479 -28
  71. package/crates/tish_resolve/src/lib.rs +75 -5
  72. package/crates/tish_runtime/Cargo.toml +10 -1
  73. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  74. package/crates/tish_runtime/src/http.rs +28 -10
  75. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  76. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  77. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  78. package/crates/tish_runtime/src/lib.rs +651 -51
  79. package/crates/tish_runtime/src/timers.rs +49 -2
  80. package/crates/tish_ui/src/jsx.rs +21 -3
  81. package/crates/tish_vm/src/jit.rs +2514 -117
  82. package/crates/tish_vm/src/vm.rs +1479 -224
  83. package/package.json +1 -1
  84. package/platform/darwin-arm64/tish-fmt +0 -0
  85. package/platform/darwin-x64/tish-fmt +0 -0
  86. package/platform/linux-arm64/tish-fmt +0 -0
  87. package/platform/linux-x64/tish-fmt +0 -0
  88. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -54,38 +54,65 @@ impl PartialEq for Key {
54
54
  }
55
55
  impl Eq for Key {}
56
56
 
57
+ /// The single source of truth for hashing a key `Value`. Both [`Key`] (owned, the stored key) and
58
+ /// [`KeyRef`] (borrowed, the lookup probe) delegate here so an owned key and a borrowed probe of an
59
+ /// equal value hash identically — the invariant that makes the zero-clone borrow lookup sound.
60
+ #[inline]
61
+ fn hash_key_value<H: Hasher>(v: &Value, state: &mut H) {
62
+ match v {
63
+ Value::Number(n) => {
64
+ 0u8.hash(state);
65
+ // Canonicalize so `Eq` partners hash together: `+0` and `-0` share a key, and every
66
+ // NaN is one key.
67
+ let bits = if *n == 0.0 {
68
+ 0u64
69
+ } else if n.is_nan() {
70
+ 0x7ff8_0000_0000_0000
71
+ } else {
72
+ n.to_bits()
73
+ };
74
+ bits.hash(state);
75
+ }
76
+ Value::String(s) => {
77
+ 1u8.hash(state);
78
+ s.as_str().hash(state);
79
+ }
80
+ Value::Bool(b) => {
81
+ 2u8.hash(state);
82
+ b.hash(state);
83
+ }
84
+ Value::Null => 3u8.hash(state),
85
+ // Reference / identity values: per-variant tag only; `ptr_eq` in `Eq` does the rest.
86
+ Value::Symbol(_) => 4u8.hash(state),
87
+ Value::Array(_) => 5u8.hash(state),
88
+ Value::NumberArray(_) => 6u8.hash(state),
89
+ Value::Object(_) => 7u8.hash(state),
90
+ _ => 8u8.hash(state),
91
+ }
92
+ }
93
+
57
94
  impl Hash for Key {
58
95
  fn hash<H: Hasher>(&self, state: &mut H) {
59
- match &self.0 {
60
- Value::Number(n) => {
61
- 0u8.hash(state);
62
- // Canonicalize so `Eq` partners hash together: `+0` and `-0` share a key, and every
63
- // NaN is one key.
64
- let bits = if *n == 0.0 {
65
- 0u64
66
- } else if n.is_nan() {
67
- 0x7ff8_0000_0000_0000
68
- } else {
69
- n.to_bits()
70
- };
71
- bits.hash(state);
72
- }
73
- Value::String(s) => {
74
- 1u8.hash(state);
75
- s.as_str().hash(state);
76
- }
77
- Value::Bool(b) => {
78
- 2u8.hash(state);
79
- b.hash(state);
80
- }
81
- Value::Null => 3u8.hash(state),
82
- // Reference / identity values: per-variant tag only; `ptr_eq` in `Eq` does the rest.
83
- Value::Symbol(_) => 4u8.hash(state),
84
- Value::Array(_) => 5u8.hash(state),
85
- Value::NumberArray(_) => 6u8.hash(state),
86
- Value::Object(_) => 7u8.hash(state),
87
- _ => 8u8.hash(state),
88
- }
96
+ hash_key_value(&self.0, state);
97
+ }
98
+ }
99
+
100
+ /// Borrowed look-up probe for [`Key`]: wraps `&Value` so `get`/`contains_key`/`shift_remove` can
101
+ /// query the store **without cloning the key into an owned `Key`** (no `Value` clone, no `Arc` bump).
102
+ /// It hashes via [`hash_key_value`] (identical to `Key`) and compares via [`same_value_zero`]
103
+ /// (identical to `Key`'s `Eq`), so it is `Equivalent<Key>` — the lookup is a drop-in for `&Key`.
104
+ /// Only `set`/`add` (which must *store* the key) still build an owned `Key`.
105
+ struct KeyRef<'a>(&'a Value);
106
+
107
+ impl Hash for KeyRef<'_> {
108
+ fn hash<H: Hasher>(&self, state: &mut H) {
109
+ hash_key_value(self.0, state);
110
+ }
111
+ }
112
+
113
+ impl indexmap::Equivalent<Key> for KeyRef<'_> {
114
+ fn equivalent(&self, key: &Key) -> bool {
115
+ same_value_zero(self.0, &key.0)
89
116
  }
90
117
  }
91
118
 
@@ -119,6 +146,57 @@ pub fn size_probe_len(op: &dyn TishOpaque) -> Option<f64> {
119
146
  .map(|p| p.0.borrow().len() as f64)
120
147
  }
121
148
 
149
+ fn map_store(map: &Value) -> Option<Store> {
150
+ if let Value::Object(o) = map {
151
+ if let Some(Value::Opaque(op)) = o.borrow().strings.get(SIZE_SLOT) {
152
+ if let Some(probe) = op.as_ref().as_any().downcast_ref::<SizeProbe>() {
153
+ return Some(probe.0.clone());
154
+ }
155
+ }
156
+ }
157
+ None
158
+ }
159
+
160
+ /// Direct `Map.prototype.has` — skips bound-method `get_prop` + `value_call` (k-nucleotide hot path).
161
+ /// Looks up via a borrowed [`KeyRef`]: no key clone, no owned `Key` allocation.
162
+ pub fn map_has(map: &Value, key: &Value) -> Value {
163
+ match map_store(map) {
164
+ Some(s) => Value::Bool(s.borrow().contains_key(&KeyRef(key))),
165
+ None => Value::Bool(false),
166
+ }
167
+ }
168
+
169
+ /// Direct `Map.prototype.get`. Looks up via a borrowed [`KeyRef`] (no key clone / no owned `Key`).
170
+ pub fn map_get(map: &Value, key: &Value) -> Value {
171
+ match map_store(map) {
172
+ Some(s) => s.borrow().get(&KeyRef(key)).cloned().unwrap_or(Value::Null),
173
+ None => Value::Null,
174
+ }
175
+ }
176
+
177
+ /// Direct `Map.prototype.set` (mutates in place; returns `undefined` = `Null`).
178
+ ///
179
+ /// Takes the key/val by reference and clones on insert (the map owns its entries). Passing by
180
+ /// reference lets the native emitter BORROW the caller's place instead of moving it, so
181
+ /// `m.set(k, v); …use k…` no longer produces `E0382 use of moved value` in the generated Rust (#442).
182
+ pub fn map_set(map: &Value, key: &Value, val: &Value) -> Value {
183
+ if let Some(s) = map_store(map) {
184
+ s.borrow_mut().insert(Key(key.clone()), val.clone());
185
+ }
186
+ Value::Null
187
+ }
188
+
189
+ /// Direct `Map.prototype.values` — snapshot iterator (same as the bound native method).
190
+ pub fn map_values(map: &Value) -> Value {
191
+ match map_store(map) {
192
+ Some(s) => {
193
+ let out: Vec<Value> = s.borrow().values().cloned().collect();
194
+ crate::iterator::array_iterator(out)
195
+ }
196
+ None => crate::iterator::array_iterator(vec![]),
197
+ }
198
+ }
199
+
122
200
  /// Wrap a backing store as the hidden [`SIZE_SLOT`] opaque. `Value::Opaque`'s payload is always
123
201
  /// `Arc<dyn TishOpaque>`, so on the single-threaded build (`Rc`-based `VmRef`) clippy's
124
202
  /// `arc_with_non_send_sync` fires spuriously — the `Arc` is mandated by the API, not a thread choice.
@@ -196,8 +274,8 @@ pub fn set_instance(initial: &[Value]) -> Value {
196
274
  m.insert(
197
275
  Arc::from("has"),
198
276
  Value::native(move |args: &[Value]| {
199
- let v = args.first().cloned().unwrap_or(Value::Null);
200
- Value::Bool(s.borrow().contains_key(&Key(v)))
277
+ let v = args.first().unwrap_or(&Value::Null);
278
+ Value::Bool(s.borrow().contains_key(&KeyRef(v)))
201
279
  }),
202
280
  );
203
281
  }
@@ -206,9 +284,9 @@ pub fn set_instance(initial: &[Value]) -> Value {
206
284
  m.insert(
207
285
  Arc::from("delete"),
208
286
  Value::native(move |args: &[Value]| {
209
- let v = args.first().cloned().unwrap_or(Value::Null);
287
+ let v = args.first().unwrap_or(&Value::Null);
210
288
  // `shift_remove` preserves iteration order (vs `swap_remove`).
211
- Value::Bool(s.borrow_mut().shift_remove(&Key(v)).is_some())
289
+ Value::Bool(s.borrow_mut().shift_remove(&KeyRef(v)).is_some())
212
290
  }),
213
291
  );
214
292
  }
@@ -246,6 +324,20 @@ pub fn set_instance(initial: &[Value]) -> Value {
246
324
  }),
247
325
  );
248
326
  }
327
+ // `__drain__`: the default-iteration array a Set yields (its elements). `drain_iterator`
328
+ // (and the interp/native equivalents) check `__drain__` FIRST, so exposing it here makes a
329
+ // Set directly iterable — `[...set]`, `for (x of set)`, `Array.from(set)` — across all three
330
+ // backends with no changes to any spread/for-of site. (#440)
331
+ {
332
+ let s = store.clone();
333
+ m.insert(
334
+ Arc::from("__drain__"),
335
+ Value::native(move |_args: &[Value]| {
336
+ let out: Vec<Value> = s.borrow().keys().map(|k| k.0.clone()).collect();
337
+ Value::Array(VmRef::new(out))
338
+ }),
339
+ );
340
+ }
249
341
 
250
342
  Value::object(m)
251
343
  }
@@ -299,8 +391,8 @@ pub fn map_instance(pairs: &[Value]) -> Value {
299
391
  m.insert(
300
392
  Arc::from("get"),
301
393
  Value::native(move |args: &[Value]| {
302
- let key = args.first().cloned().unwrap_or(Value::Null);
303
- s.borrow().get(&Key(key)).cloned().unwrap_or(Value::Null)
394
+ let key = args.first().unwrap_or(&Value::Null);
395
+ s.borrow().get(&KeyRef(key)).cloned().unwrap_or(Value::Null)
304
396
  }),
305
397
  );
306
398
  }
@@ -309,8 +401,8 @@ pub fn map_instance(pairs: &[Value]) -> Value {
309
401
  m.insert(
310
402
  Arc::from("has"),
311
403
  Value::native(move |args: &[Value]| {
312
- let key = args.first().cloned().unwrap_or(Value::Null);
313
- Value::Bool(s.borrow().contains_key(&Key(key)))
404
+ let key = args.first().unwrap_or(&Value::Null);
405
+ Value::Bool(s.borrow().contains_key(&KeyRef(key)))
314
406
  }),
315
407
  );
316
408
  }
@@ -319,8 +411,8 @@ pub fn map_instance(pairs: &[Value]) -> Value {
319
411
  m.insert(
320
412
  Arc::from("delete"),
321
413
  Value::native(move |args: &[Value]| {
322
- let key = args.first().cloned().unwrap_or(Value::Null);
323
- Value::Bool(s.borrow_mut().shift_remove(&Key(key)).is_some())
414
+ let key = args.first().unwrap_or(&Value::Null);
415
+ Value::Bool(s.borrow_mut().shift_remove(&KeyRef(key)).is_some())
324
416
  }),
325
417
  );
326
418
  }
@@ -368,6 +460,23 @@ pub fn map_instance(pairs: &[Value]) -> Value {
368
460
  }),
369
461
  );
370
462
  }
463
+ // `__drain__`: a Map's default iteration yields `[key, value]` entries. Exposed so the shared
464
+ // `drain_iterator` (checked first) makes a Map directly iterable — `[...map]`, `for (x of map)`,
465
+ // `Array.from(map)` — across interp/vm/native with no spread/for-of site changes. (#440)
466
+ {
467
+ let s = store.clone();
468
+ m.insert(
469
+ Arc::from("__drain__"),
470
+ Value::native(move |_args: &[Value]| {
471
+ let pairs: Vec<Value> = s
472
+ .borrow()
473
+ .iter()
474
+ .map(|(k, v)| Value::Array(VmRef::new(vec![k.0.clone(), v.clone()])))
475
+ .collect();
476
+ Value::Array(VmRef::new(pairs))
477
+ }),
478
+ );
479
+ }
371
480
 
372
481
  Value::object(m)
373
482
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  use std::sync::Arc;
7
7
  use tishlang_core::VmRef;
8
- use tishlang_core::{percent_decode, percent_encode, ObjectMap, Value};
8
+ use tishlang_core::{percent_decode, percent_decode_component, percent_encode, percent_encode_component, ObjectMap, Value};
9
9
 
10
10
  /// Boolean(value) - coerce to bool
11
11
  pub fn boolean(args: &[Value]) -> Value {
@@ -13,7 +13,7 @@ pub fn boolean(args: &[Value]) -> Value {
13
13
  Value::Bool(v.is_truthy())
14
14
  }
15
15
 
16
- /// decodeURI(str)
16
+ /// decodeURI(str) — preserves the URI reserved delimiters (the inverse of encodeURI).
17
17
  pub fn decode_uri(args: &[Value]) -> Value {
18
18
  let s = args
19
19
  .first()
@@ -31,19 +31,34 @@ pub fn encode_uri(args: &[Value]) -> Value {
31
31
  Value::String(percent_encode(&s).into())
32
32
  }
33
33
 
34
- /// isFinite(value)
34
+ /// encodeURIComponent(str) — stricter than encodeURI (also escapes the URI reserved delimiters).
35
+ pub fn encode_uri_component(args: &[Value]) -> Value {
36
+ let s = args
37
+ .first()
38
+ .map(Value::to_display_string)
39
+ .unwrap_or_default();
40
+ Value::String(percent_encode_component(&s).into())
41
+ }
42
+
43
+ /// decodeURIComponent(str) — decodes ALL `%XX` escapes (including the reserved delimiters).
44
+ pub fn decode_uri_component(args: &[Value]) -> Value {
45
+ let s = args
46
+ .first()
47
+ .map(Value::to_display_string)
48
+ .unwrap_or_default();
49
+ Value::String(percent_decode_component(&s).unwrap_or(s).into())
50
+ }
51
+
52
+ /// isFinite(value) — coerces via ToNumber (like `Number()`), then tests finiteness. `isFinite("3")`
53
+ /// is `true`, `isFinite("x")`/`isFinite()` is `false`. (The non-coercing form is `Number.isFinite`.)
35
54
  pub fn is_finite(args: &[Value]) -> Value {
36
- Value::Bool(
37
- args.first()
38
- .is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite())),
39
- )
55
+ Value::Bool(args.first().map_or(f64::NAN, to_number).is_finite())
40
56
  }
41
57
 
42
- /// isNaN(value)
58
+ /// isNaN(value) — coerces via ToNumber, then tests NaN. `isNaN("3")` is `false`; an absent arg
59
+ /// (undefined) coerces to NaN → `true`. (The non-coercing form is `Number.isNaN`.)
43
60
  pub fn is_nan(args: &[Value]) -> Value {
44
- Value::Bool(args.first().is_none_or(|v| {
45
- matches!(v, Value::Number(n) if n.is_nan()) || !matches!(v, Value::Number(_))
46
- }))
61
+ Value::Bool(args.first().map_or(f64::NAN, to_number).is_nan())
47
62
  }
48
63
 
49
64
  /// Array.isArray(value)
@@ -51,6 +66,142 @@ pub fn array_is_array(args: &[Value]) -> Value {
51
66
  Value::Bool(matches!(args.first(), Some(Value::Array(_)) | Some(Value::NumberArray(_))))
52
67
  }
53
68
 
69
+ /// `Array.of(...items)` — build an array from the args (unlike `Array(n)`, a single number is an
70
+ /// element, not a length: `Array.of(7)` is `[7]`).
71
+ pub fn array_of(args: &[Value]) -> Value {
72
+ Value::Array(VmRef::new(args.to_vec()))
73
+ }
74
+
75
+ /// `Array.from(source, mapFn?)` — build an array from an iterable (array, string, Set/Map, or any
76
+ /// `__drain__`/`next` iterator — via the shared iterator protocol) or an array-like `{ length }`
77
+ /// object, optionally mapping each element through `mapFn(item, index)`.
78
+ pub fn array_from(args: &[Value]) -> Value {
79
+ let source = args.first().cloned().unwrap_or(Value::Null);
80
+ let items: Vec<Value> = match &source {
81
+ Value::Array(a) => a.borrow().clone(),
82
+ Value::NumberArray(a) => a.borrow().iter().map(|n| Value::Number(*n)).collect(),
83
+ // `drain_iterator` covers String (chars), Set/Map, and any iterator object.
84
+ other => {
85
+ if let Some(d) = tishlang_core::drain_iterator(other) {
86
+ d
87
+ } else if matches!(other, Value::Object(_)) {
88
+ let len = tishlang_core::object_get(other, &Value::String("length".into()))
89
+ .and_then(|v| match v {
90
+ Value::Number(n) if n.is_finite() && n >= 0.0 => Some(n as usize),
91
+ _ => None,
92
+ })
93
+ .unwrap_or(0);
94
+ (0..len)
95
+ .map(|i| {
96
+ tishlang_core::object_get(other, &Value::String(i.to_string().into()))
97
+ .unwrap_or(Value::Null)
98
+ })
99
+ .collect()
100
+ } else {
101
+ Vec::new()
102
+ }
103
+ }
104
+ };
105
+ let out: Vec<Value> = match args.get(1) {
106
+ Some(Value::Function(f)) => items
107
+ .into_iter()
108
+ .enumerate()
109
+ .map(|(i, x)| f.call(&[x, Value::Number(i as f64)]))
110
+ .collect(),
111
+ _ => items,
112
+ };
113
+ Value::Array(VmRef::new(out))
114
+ }
115
+
116
+ /// `structuredClone(value)` — a recursive deep copy of arrays, objects, and primitives, with cycles
117
+ /// preserved (a repeated reference clones to the same new node). Functions and exotic types (Date,
118
+ /// RegExp, Map, Set) are shared/shallow — a pragmatic subset covering plain data structures.
119
+ pub fn structured_clone(args: &[Value]) -> Value {
120
+ let mut seen: Vec<(*const (), Value)> = Vec::new();
121
+ deep_clone(args.first().unwrap_or(&Value::Null), &mut seen)
122
+ }
123
+
124
+ fn deep_clone(v: &Value, seen: &mut Vec<(*const (), Value)>) -> Value {
125
+ match v {
126
+ Value::Array(a) => {
127
+ let ptr = a.as_ptr();
128
+ if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
129
+ return c.clone();
130
+ }
131
+ let new: VmRef<Vec<Value>> = VmRef::new(Vec::new());
132
+ seen.push((ptr, Value::Array(new.clone())));
133
+ let src = a.borrow().clone();
134
+ let cloned: Vec<Value> = src.iter().map(|e| deep_clone(e, seen)).collect();
135
+ *new.borrow_mut() = cloned;
136
+ Value::Array(new)
137
+ }
138
+ Value::NumberArray(a) => Value::NumberArray(VmRef::new(a.borrow().clone())),
139
+ Value::Object(o) => {
140
+ let ptr = o.as_ptr();
141
+ if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
142
+ return c.clone();
143
+ }
144
+ let entries: Vec<(Arc<str>, Value)> = o
145
+ .borrow()
146
+ .strings
147
+ .iter()
148
+ .map(|(k, val)| (k.clone(), val.clone()))
149
+ .collect();
150
+ let new_obj = Value::object(ObjectMap::default());
151
+ // register the (empty) new object before recursing so cycles resolve back to it
152
+ if let Value::Object(no) = &new_obj {
153
+ seen.push((ptr, new_obj.clone()));
154
+ for (k, val) in entries {
155
+ let cloned = deep_clone(&val, seen);
156
+ no.borrow_mut().strings.insert(k, cloned);
157
+ }
158
+ }
159
+ new_obj
160
+ }
161
+ other => other.clone(),
162
+ }
163
+ }
164
+
165
+ /// `Object.hasOwn(obj, key)` (ES2022) — true if `key` is an OWN property of `obj`. For arrays an
166
+ /// in-range integer index or `"length"` counts; for plain objects, a key present in the string map.
167
+ pub fn object_has_own(args: &[Value]) -> Value {
168
+ let key = match args.get(1) {
169
+ Some(Value::String(s)) => s.to_string(),
170
+ Some(Value::Number(n)) => tishlang_core::js_number_to_string(*n),
171
+ Some(v) => v.to_display_string(),
172
+ None => return Value::Bool(false),
173
+ };
174
+ match args.first() {
175
+ Some(Value::Object(obj)) => Value::Bool(obj.borrow().strings.contains_key(key.as_str())),
176
+ Some(Value::Array(arr)) => Value::Bool(
177
+ key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
178
+ ),
179
+ Some(Value::NumberArray(arr)) => Value::Bool(
180
+ key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
181
+ ),
182
+ _ => Value::Bool(false),
183
+ }
184
+ }
185
+
186
+ /// `Object.is(a, b)` — SameValue: like `===`, but NaN equals NaN and `+0` is NOT equal to `-0`.
187
+ pub fn object_is(args: &[Value]) -> Value {
188
+ let a = args.first().unwrap_or(&Value::Null);
189
+ let b = args.get(1).unwrap_or(&Value::Null);
190
+ let same = match (a, b) {
191
+ (Value::Number(x), Value::Number(y)) => {
192
+ if x.is_nan() && y.is_nan() {
193
+ true
194
+ } else if *x == 0.0 && *y == 0.0 {
195
+ x.is_sign_negative() == y.is_sign_negative()
196
+ } else {
197
+ x == y
198
+ }
199
+ }
200
+ _ => a.strict_eq(b),
201
+ };
202
+ Value::Bool(same)
203
+ }
204
+
54
205
  /// String(value) — convert value to string (JS String constructor as function).
55
206
  /// Uses JS `ToString` (arrays comma-join recursively, objects → "[object Object]"),
56
207
  /// not the inspect/display form.
@@ -64,8 +215,14 @@ pub fn string_convert(args: &[Value]) -> Value {
64
215
  /// otherwise NaN); arrays/objects go via their string form (so `Number([5])` → 5,
65
216
  /// `Number([])` → 0, objects → NaN).
66
217
  pub fn number_convert(args: &[Value]) -> Value {
67
- let v = args.first().unwrap_or(&Value::Null);
68
- let n = match v {
218
+ Value::Number(to_number(args.first().unwrap_or(&Value::Null)))
219
+ }
220
+
221
+ /// JS `ToNumber(v)`: Number → itself; Bool → 1/0; Null → 0; String → parsed (trimmed, `0x`/`0b`/`0o`
222
+ /// and `Infinity`; `""` → 0, else NaN); anything else via its string form. The single ToNumber path
223
+ /// shared by `Number()`, `isNaN`/`isFinite`, etc.
224
+ pub fn to_number(v: &Value) -> f64 {
225
+ match v {
69
226
  Value::Number(n) => *n,
70
227
  Value::Bool(b) => {
71
228
  if *b {
@@ -77,8 +234,29 @@ pub fn number_convert(args: &[Value]) -> Value {
77
234
  Value::Null => 0.0,
78
235
  Value::String(s) => parse_numeric_string(s),
79
236
  other => parse_numeric_string(&other.to_js_string()),
80
- };
81
- Value::Number(n)
237
+ }
238
+ }
239
+
240
+ /// `Number.isInteger(v)` — true iff `v` IS a finite Number with no fractional part. No coercion
241
+ /// (`Number.isInteger("3")` is `false`, unlike a bare `isNaN`-style coercion).
242
+ pub fn number_is_integer(args: &[Value]) -> Value {
243
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0))
244
+ }
245
+
246
+ /// `Number.isSafeInteger(v)` — `isInteger` AND exactly representable (|v| ≤ 2^53 − 1).
247
+ pub fn number_is_safe_integer(args: &[Value]) -> Value {
248
+ Value::Bool(matches!(args.first(),
249
+ Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0 && n.abs() <= 9_007_199_254_740_991.0))
250
+ }
251
+
252
+ /// `Number.isNaN(v)` — true iff `v` IS the number NaN. No coercion (unlike the global `isNaN`).
253
+ pub fn number_is_nan(args: &[Value]) -> Value {
254
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_nan()))
255
+ }
256
+
257
+ /// `Number.isFinite(v)` — true iff `v` is a finite Number. No coercion (unlike the global `isFinite`).
258
+ pub fn number_is_finite(args: &[Value]) -> Value {
259
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite()))
82
260
  }
83
261
 
84
262
  /// Parse a string as JS `Number` does: trimmed; `""` → 0; `0x`/`0o`/`0b` radix prefixes;
@@ -120,16 +298,28 @@ pub fn string_from_char_code(args: &[Value]) -> Value {
120
298
 
121
299
  /// Object.keys(obj)
122
300
  pub fn object_keys(args: &[Value]) -> Value {
123
- if let Some(Value::Object(obj)) = args.first() {
124
- let obj_borrow = obj.borrow();
125
- let keys: Vec<Value> = obj_borrow
126
- .strings
127
- .keys()
128
- .map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
129
- .collect();
130
- Value::Array(VmRef::new(keys))
131
- } else {
132
- Value::Array(VmRef::new(Vec::new()))
301
+ match args.first() {
302
+ Some(Value::Object(obj)) => {
303
+ let obj_borrow = obj.borrow();
304
+ let keys: Vec<Value> = obj_borrow
305
+ .strings
306
+ .keys()
307
+ .map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
308
+ .collect();
309
+ Value::Array(VmRef::new(keys))
310
+ }
311
+ // `Object.keys(array)` yields the index strings "0".."len-1" (an array's own enumerable keys
312
+ // are its indices, per JS). Previously returned `[]` — a node divergence, and the reason
313
+ // `for (k in array)` enumerated nothing on the vm/native backends (they lower for-in to
314
+ // for-of over `Object.keys`).
315
+ Some(Value::Array(arr)) => {
316
+ let len = arr.borrow().len();
317
+ let keys: Vec<Value> = (0..len)
318
+ .map(|i| Value::String(tishlang_core::ArcStr::from(i.to_string().as_str())))
319
+ .collect();
320
+ Value::Array(VmRef::new(keys))
321
+ }
322
+ _ => Value::Array(VmRef::new(Vec::new())),
133
323
  }
134
324
  }
135
325
 
@@ -1,7 +1,7 @@
1
1
  //! Math builtin functions.
2
2
 
3
3
  use crate::helpers::extract_num;
4
- use tishlang_core::Value;
4
+ use tishlang_core::{to_int32, to_uint32, Value};
5
5
 
6
6
  macro_rules! math_unary {
7
7
  ($name:ident, $op:ident) => {
@@ -28,6 +28,20 @@ math_unary!(log2, log2);
28
28
  math_unary!(exp, exp);
29
29
  math_unary!(trunc, trunc);
30
30
  math_unary!(cbrt, cbrt);
31
+ math_unary!(expm1, exp_m1);
32
+ math_unary!(log1p, ln_1p);
33
+
34
+ /// `Math.clz32(x)` — count leading zero bits of `ToUint32(x)` (0..=32).
35
+ pub fn clz32(args: &[Value]) -> Value {
36
+ let n = to_uint32(extract_num(args.first()).unwrap_or(0.0));
37
+ Value::Number(n.leading_zeros() as f64)
38
+ }
39
+
40
+ /// `Math.fround(x)` — round `x` to the nearest 32-bit float, back to f64.
41
+ pub fn fround(args: &[Value]) -> Value {
42
+ let x = extract_num(args.first()).unwrap_or(f64::NAN);
43
+ Value::Number(x as f32 as f64)
44
+ }
31
45
 
32
46
  // --- f64-domain semantics (single source of truth) -------------------------------------------------
33
47
  // These hold the JS-specific Math rules so every backend agrees: the vm/native paths call the
@@ -119,7 +133,9 @@ pub fn sign(args: &[Value]) -> Value {
119
133
  } else if n < 0.0 {
120
134
  -1.0
121
135
  } else {
122
- 0.0
136
+ // ±0 → return the input unchanged so `Math.sign(-0)` is `-0` (per ES);
137
+ // `n < 0.0` is false for -0, so it lands here — bare `0.0` would drop the sign.
138
+ n
123
139
  })
124
140
  }
125
141
 
@@ -129,15 +145,57 @@ pub fn atan2(args: &[Value]) -> Value {
129
145
  Value::Number(y.atan2(x))
130
146
  }
131
147
 
148
+ /// ECMAScript `Math.hypot(...args)` core: sqrt of the sum of squares, VARIADIC and
149
+ /// NaN/Infinity-correct. Infinity takes precedence over NaN (any ±∞ arg → +∞, even alongside a NaN);
150
+ /// otherwise any NaN → NaN; the finite case is scaled by the max magnitude to avoid intermediate
151
+ /// overflow/underflow (matches V8). `Math.hypot()` (no args) → 0. Shared so interp/vm/native agree
152
+ /// (#247 — the interp/native path was 2-arg only, so `Math.hypot(3,4,12)` gave 5 not 13, and the vm
153
+ /// dropped non-number args instead of propagating NaN).
154
+ pub fn hypot_f64(nums: &[f64]) -> f64 {
155
+ let mut any_nan = false;
156
+ let mut max = 0.0_f64;
157
+ for &n in nums {
158
+ if n.is_infinite() {
159
+ return f64::INFINITY;
160
+ }
161
+ if n.is_nan() {
162
+ any_nan = true;
163
+ }
164
+ let a = n.abs();
165
+ if a > max {
166
+ max = a;
167
+ }
168
+ }
169
+ if any_nan {
170
+ return f64::NAN;
171
+ }
172
+ if max == 0.0 {
173
+ return 0.0;
174
+ }
175
+ let mut sum = 0.0_f64;
176
+ for &n in nums {
177
+ let r = n / max;
178
+ sum += r * r;
179
+ }
180
+ max * sum.sqrt()
181
+ }
182
+
132
183
  pub fn hypot(args: &[Value]) -> Value {
133
- let x = extract_num(args.first()).unwrap_or(0.0);
134
- let y = extract_num(args.get(1)).unwrap_or(0.0);
135
- Value::Number(x.hypot(y))
184
+ let nums: Vec<f64> = args
185
+ .iter()
186
+ .map(|v| extract_num(Some(v)).unwrap_or(f64::NAN))
187
+ .collect();
188
+ Value::Number(hypot_f64(&nums))
136
189
  }
137
190
 
138
191
  /// ES6 `Math.imul`: 32-bit integer multiply (used by xmur3 PRNG in juke-cards).
192
+ ///
193
+ /// Operands go through JS `ToInt32` (modulo 2³², NaN/±Infinity → 0), NOT a saturating `as i32`
194
+ /// cast: the latter clamps any argument ≥ 2³¹ (e.g. a full uint32 hash state) to `i32::MAX` and
195
+ /// diverges from ECMAScript / V8 (`Math.imul(3e9, 1)` = -1294967296, not 2147483647). The typed
196
+ /// native path inlines the same ToInt32 + wrapping multiply, so all backends agree.
139
197
  pub fn imul(args: &[Value]) -> Value {
140
- let a = extract_num(args.first()).unwrap_or(0.0) as i32;
141
- let b = extract_num(args.get(1)).unwrap_or(0.0) as i32;
198
+ let a = to_int32(extract_num(args.first()).unwrap_or(0.0));
199
+ let b = to_int32(extract_num(args.get(1)).unwrap_or(0.0));
142
200
  Value::Number(a.wrapping_mul(b) as f64)
143
201
  }