@tishlang/tish-format 1.0.13 → 2.0.1

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 (108) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +10 -2
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/Cargo.toml +1 -1
  66. package/crates/tish_fmt/src/lib.rs +61 -5
  67. package/crates/tish_lexer/src/lib.rs +397 -9
  68. package/crates/tish_lexer/src/token.rs +7 -0
  69. package/crates/tish_lint/src/lib.rs +2 -10
  70. package/crates/tish_lsp/src/import_goto.rs +2 -0
  71. package/crates/tish_lsp/src/main.rs +439 -26
  72. package/crates/tish_native/src/build.rs +55 -1
  73. package/crates/tish_opt/src/lib.rs +126 -23
  74. package/crates/tish_parser/src/lib.rs +55 -1
  75. package/crates/tish_parser/src/parser.rs +456 -34
  76. package/crates/tish_pg/src/lib.rs +3 -3
  77. package/crates/tish_resolve/src/lib.rs +99 -59
  78. package/crates/tish_runtime/Cargo.toml +4 -0
  79. package/crates/tish_runtime/src/http.rs +66 -17
  80. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  81. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  82. package/crates/tish_runtime/src/lib.rs +299 -44
  83. package/crates/tish_runtime/src/promise.rs +328 -18
  84. package/crates/tish_runtime/src/timers.rs +13 -7
  85. package/crates/tish_runtime/src/tty.rs +226 -0
  86. package/crates/tish_runtime/src/ws.rs +35 -18
  87. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  88. package/crates/tish_ui/src/jsx.rs +10 -0
  89. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  90. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  91. package/crates/tish_vm/Cargo.toml +14 -1
  92. package/crates/tish_vm/src/jit.rs +1050 -0
  93. package/crates/tish_vm/src/lib.rs +2 -0
  94. package/crates/tish_vm/src/vm.rs +1546 -202
  95. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  96. package/crates/tish_wasm/src/lib.rs +6 -2
  97. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  98. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  99. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  100. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  101. package/justfile +8 -0
  102. package/package.json +2 -2
  103. package/platform/darwin-arm64/tish-fmt +0 -0
  104. package/platform/darwin-x64/tish-fmt +0 -0
  105. package/platform/linux-arm64/tish-fmt +0 -0
  106. package/platform/linux-x64/tish-fmt +0 -0
  107. package/platform/win32-x64/tish-fmt.exe +0 -0
  108. package/README.md +0 -138
@@ -4,6 +4,8 @@ use std::sync::atomic::{AtomicU64, Ordering};
4
4
  use std::sync::Arc;
5
5
 
6
6
  use ahash::AHashMap;
7
+ use indexmap::IndexMap;
8
+ use smallvec::SmallVec;
7
9
 
8
10
  use crate::vmref::VmRef;
9
11
 
@@ -63,10 +65,63 @@ use fancy_regex::Regex;
63
65
  /// `Rc<dyn Fn>` for zero-overhead single-threaded execution (wasm / wasi /
64
66
  /// interpreter / cranelift / llvm VMs and any Rust native build without
65
67
  /// `http`).
68
+ /// A callable value's behaviour. Replaces the former `Arc<dyn Fn(&[Value]) -> Value>`:
69
+ /// the trait lets a *bytecode-VM* closure additionally expose its compiled chunk (via the
70
+ /// `as_any` downcast), so the VM's `Call` opcode can run tish→tish calls on an explicit
71
+ /// frame stack (task #39, the frame-VM) instead of recursively re-entering `run_chunk` —
72
+ /// while native builtins use the blanket [`FnCallable`] adapter and keep plain `Fn`
73
+ /// behaviour. `Send + Sync` is conditional on `send-values`, exactly like `NativeFn` was.
66
74
  #[cfg(feature = "send-values")]
67
- pub type NativeFn = Arc<dyn Fn(&[Value]) -> Value + Send + Sync>;
75
+ pub trait Callable: Send + Sync {
76
+ fn call(&self, args: &[Value]) -> Value;
77
+ /// Downcast hook for the VM frame path; native adapters return themselves (downcast fails).
78
+ fn as_any(&self) -> &dyn std::any::Any;
79
+ }
80
+ #[cfg(not(feature = "send-values"))]
81
+ pub trait Callable {
82
+ fn call(&self, args: &[Value]) -> Value;
83
+ fn as_any(&self) -> &dyn std::any::Any;
84
+ }
85
+
86
+ /// Adapter wrapping a plain `Fn` closure (every native builtin) as a [`Callable`].
87
+ pub struct FnCallable<F>(pub F);
88
+ #[cfg(feature = "send-values")]
89
+ impl<F: Fn(&[Value]) -> Value + Send + Sync + 'static> Callable for FnCallable<F> {
90
+ #[inline]
91
+ fn call(&self, args: &[Value]) -> Value {
92
+ (self.0)(args)
93
+ }
94
+ fn as_any(&self) -> &dyn std::any::Any {
95
+ self
96
+ }
97
+ }
98
+ #[cfg(not(feature = "send-values"))]
99
+ impl<F: Fn(&[Value]) -> Value + 'static> Callable for FnCallable<F> {
100
+ #[inline]
101
+ fn call(&self, args: &[Value]) -> Value {
102
+ (self.0)(args)
103
+ }
104
+ fn as_any(&self) -> &dyn std::any::Any {
105
+ self
106
+ }
107
+ }
108
+
109
+ #[cfg(feature = "send-values")]
110
+ pub type NativeFn = Arc<dyn Callable>;
68
111
  #[cfg(not(feature = "send-values"))]
69
- pub type NativeFn = std::rc::Rc<dyn Fn(&[Value]) -> Value>;
112
+ pub type NativeFn = std::rc::Rc<dyn Callable>;
113
+
114
+ /// Build a raw [`NativeFn`] from a plain closure (wraps it in [`FnCallable`]). For sites that
115
+ /// need a `NativeFn` handle directly rather than a `Value::Function` (e.g. HTTP/promise/timer
116
+ /// internals that store the callable). The `Value::Function` variant is built via [`Value::native`].
117
+ #[cfg(feature = "send-values")]
118
+ pub fn native_fn<F: Fn(&[Value]) -> Value + Send + Sync + 'static>(f: F) -> NativeFn {
119
+ Arc::new(FnCallable(f))
120
+ }
121
+ #[cfg(not(feature = "send-values"))]
122
+ pub fn native_fn<F: Fn(&[Value]) -> Value + 'static>(f: F) -> NativeFn {
123
+ std::rc::Rc::new(FnCallable(f))
124
+ }
70
125
 
71
126
  /// Trait for opaque Rust types exposed to Tish (e.g. Polars DataFrame).
72
127
  /// Implementors provide method dispatch so Tish can call methods on the value.
@@ -106,6 +161,17 @@ pub trait TishOpaque {
106
161
  /// Implemented by the runtime for native compile; interpreter uses its own Promise.
107
162
  pub trait TishPromise: Send + Sync {
108
163
  fn block_until_settled(&self) -> std::result::Result<Value, Value>;
164
+ /// Try to settle WITHOUT blocking. Returns `Some(result)` if the promise was already
165
+ /// settled before this call; returns `None` if it is still pending (a background thread
166
+ /// / I/O task has not completed yet). Default: always pending — implementors of async
167
+ /// promises (fetch, spawn) leave this as `None`; `ImmediateSettledPromise` overrides it.
168
+ ///
169
+ /// Used by `race`/`any`/`allSettled` to handle already-settled promises in input-order
170
+ /// (deterministic, JS-compatible) before falling back to concurrent thread waiting for
171
+ /// genuinely-pending ones.
172
+ fn try_settle(&self) -> Option<std::result::Result<Value, Value>> {
173
+ None
174
+ }
109
175
  }
110
176
 
111
177
  /// JavaScript RegExp flags
@@ -278,13 +344,20 @@ impl TishRegExp {
278
344
  /// **Thread safety**: `Value: Send + Sync`. Mutable payloads live inside
279
345
  /// [`VmRef`], a `Send + Sync` `Arc<Mutex<T>>` wrapper that preserves the
280
346
  /// `RefCell`-style borrow API. Functions are `Arc<dyn Fn + Send + Sync>`.
281
- #[derive(Clone)]
347
+ #[derive(Clone, Default)]
282
348
  pub enum Value {
283
349
  Number(f64),
284
- String(Arc<str>),
350
+ String(arcstr::ArcStr),
285
351
  Bool(bool),
352
+ #[default]
286
353
  Null,
287
354
  Array(VmRef<Vec<Value>>),
355
+ /// Packed f64 array — `TISH_PACKED_ARRAYS` mode only. All elements are f64; a non-numeric
356
+ /// push/set/op materializes to `Value::Array` first. Eliminates per-element boxing and
357
+ /// enables direct `sort_unstable_by` without an unbox pass. Created by all-numeric array
358
+ /// literals, `new Array(n)` (zero-filled), and from numeric HOF results. Never created
359
+ /// when `packed_arrays_enabled()` is false — callers check before constructing.
360
+ NumberArray(VmRef<Vec<f64>>),
288
361
  Object(VmRef<ObjectData>),
289
362
  /// ECMAScript-style primitive symbol (identity by `Arc`).
290
363
  Symbol(Arc<TishSymbol>),
@@ -297,18 +370,335 @@ pub enum Value {
297
370
  Opaque(Arc<dyn TishOpaque>),
298
371
  }
299
372
 
373
+ // Size guard. `Value` is 24 bytes: `String` is thin (`ArcStr`, 8B), but `Function`/`Promise`/`Opaque`
374
+ // are fat `Arc<dyn …>` (data+vtable, 16B) ⇒ 16B payload + discriminant = 24.
375
+ //
376
+ // NOTE — shrinking to 16B was tried and REVERTED (see docs/perf.md "Value-shrink"): thinning the
377
+ // three `Arc<dyn>` variants to `Arc<Box<dyn>>` (8B) DID make `Value` 16B and stayed green on all 6
378
+ // backends, but it REGRESSED numeric dispatch ~8–10% (measured A/B interleaved, both in- AND
379
+ // out-of-cache). tish is dispatch-bound, NOT memory-bandwidth-bound on `Value` size; the
380
+ // boxing/enum-layout change pessimized the hot `Number` path. Smaller `Value` ≠ faster here. Do not
381
+ // re-attempt the box trick; only a dispatch-level change (e.g. NaN-box's branch-free tag test, not
382
+ // its size) could pay off. Gated to 64-bit: wasm32 (wasi) has 32-bit pointers, so size differs there.
383
+ #[cfg(target_pointer_width = "64")]
384
+ const _: () = assert!(std::mem::size_of::<Value>() == 24);
385
+
386
+ /// Number of properties kept inline (no heap hashmap) before promoting to a map.
387
+ const PROPMAP_INLINE: usize = 8;
388
+
389
+ /// String-keyed property storage for objects.
390
+ ///
391
+ /// Small objects (the overwhelming common case — `{ id, name, active }`) keep
392
+ /// their entries inline with linear-scan lookup: no separate hashmap allocation
393
+ /// and good cache locality, which beats hashing for a handful of keys. Objects
394
+ /// that grow past [`PROPMAP_INLINE`] keys promote to an insertion-ordered
395
+ /// `IndexMap` so large objects (e.g. `JSON.parse` output) keep O(1) lookup and
396
+ /// never hit O(n²). Iteration is always **insertion order**, matching JS/Node.
397
+ ///
398
+ /// Exposes the `AHashMap`-compatible surface (`get`/`insert`/`iter`/…) the rest
399
+ /// of the runtime already uses, so it is a drop-in for the old `ObjectMap` field.
400
+ #[derive(Clone, Debug, Default)]
401
+ pub struct PropMap {
402
+ inline: SmallVec<[(Arc<str>, Value); PROPMAP_INLINE]>,
403
+ map: Option<Box<IndexMap<Arc<str>, Value, ahash::RandomState>>>,
404
+ /// Hidden-class identity for this object's ordered key-set (JSC Structure). `EMPTY_SHAPE` (0)
405
+ /// for `{}`. Maintained by `insert` (new key → `shape::transition`) and reset to `DICT_SHAPE` by
406
+ /// `remove`. Lets the VM's inline caches compare a `u32` instead of hashing a key. INVARIANT: a
407
+ /// non-empty PropMap never has `EMPTY_SHAPE` (every key-add transitions away from it) — the IC
408
+ /// relies on this, so all key-adds must go through `insert` (the only mutation path; fields private).
409
+ shape: crate::shape::ShapeId,
410
+ }
411
+
412
+ impl PropMap {
413
+ #[inline]
414
+ pub fn new() -> Self {
415
+ Self::default()
416
+ }
417
+
418
+ pub fn with_capacity(n: usize) -> Self {
419
+ if n > PROPMAP_INLINE {
420
+ Self {
421
+ inline: SmallVec::new(),
422
+ map: Some(Box::new(IndexMap::with_capacity_and_hasher(
423
+ n,
424
+ ahash::RandomState::default(),
425
+ ))),
426
+ shape: crate::shape::EMPTY_SHAPE,
427
+ }
428
+ } else {
429
+ Self::default()
430
+ }
431
+ }
432
+
433
+ /// The hidden-class id for this object's current key-set (for the VM's inline caches).
434
+ #[inline]
435
+ pub fn shape(&self) -> crate::shape::ShapeId {
436
+ self.shape
437
+ }
438
+
439
+ /// Value at slot `i` (insertion order). For the inline-cache hit path: once a `(shape, index)`
440
+ /// is cached, a shape match means the property is at this stable index.
441
+ #[inline]
442
+ pub fn value_at_index(&self, i: usize) -> Option<&Value> {
443
+ match &self.map {
444
+ Some(m) => m.get_index(i).map(|(_, v)| v),
445
+ None => self.inline.get(i).map(|(_, v)| v),
446
+ }
447
+ }
448
+
449
+ /// Mutable value at slot `i` (insertion order) — for the SetMember inline-cache update path.
450
+ #[inline]
451
+ pub fn value_at_index_mut(&mut self, i: usize) -> Option<&mut Value> {
452
+ match &mut self.map {
453
+ Some(m) => m.get_index_mut(i).map(|(_, v)| v),
454
+ None => self.inline.get_mut(i).map(|(_, v)| v),
455
+ }
456
+ }
457
+
458
+ /// Like `get`, but also returns the property's slot index — used to *fill* an inline cache on a miss.
459
+ #[inline]
460
+ pub fn get_with_index(&self, key: &str) -> Option<(&Value, usize)> {
461
+ match &self.map {
462
+ Some(m) => m.get_full(key).map(|(i, _, v)| (v, i)),
463
+ None => self
464
+ .inline
465
+ .iter()
466
+ .position(|(k, _)| k.as_ref() == key)
467
+ .map(|i| (&self.inline[i].1, i)),
468
+ }
469
+ }
470
+
471
+ #[inline]
472
+ pub fn len(&self) -> usize {
473
+ match &self.map {
474
+ Some(m) => m.len(),
475
+ None => self.inline.len(),
476
+ }
477
+ }
478
+
479
+ #[inline]
480
+ pub fn is_empty(&self) -> bool {
481
+ self.len() == 0
482
+ }
483
+
484
+ #[inline]
485
+ pub fn get(&self, key: &str) -> Option<&Value> {
486
+ match &self.map {
487
+ Some(m) => m.get(key),
488
+ None => self
489
+ .inline
490
+ .iter()
491
+ .find(|(k, _)| k.as_ref() == key)
492
+ .map(|(_, v)| v),
493
+ }
494
+ }
495
+
496
+ #[inline]
497
+ pub fn get_mut(&mut self, key: &str) -> Option<&mut Value> {
498
+ match &mut self.map {
499
+ Some(m) => m.get_mut(key),
500
+ None => self
501
+ .inline
502
+ .iter_mut()
503
+ .find(|(k, _)| k.as_ref() == key)
504
+ .map(|(_, v)| v),
505
+ }
506
+ }
507
+
508
+ #[inline]
509
+ pub fn contains_key(&self, key: &str) -> bool {
510
+ self.get(key).is_some()
511
+ }
512
+
513
+ pub fn insert(&mut self, key: Arc<str>, val: Value) -> Option<Value> {
514
+ if let Some(m) = &mut self.map {
515
+ // Map path (>PROPMAP_INLINE keys). A new key transitions the shape; an update doesn't.
516
+ let kc = Arc::clone(&key);
517
+ let prev = m.insert(key, val);
518
+ if prev.is_none() {
519
+ self.shape = crate::shape::transition(self.shape, &kc);
520
+ }
521
+ return prev;
522
+ }
523
+ if let Some(slot) = self.inline.iter_mut().find(|(k, _)| k.as_ref() == key.as_ref()) {
524
+ // Update existing key → value changes, layout (shape) does not.
525
+ return Some(std::mem::replace(&mut slot.1, val));
526
+ }
527
+ // New key (inline storage) → transition the shape away from the current one.
528
+ self.shape = crate::shape::transition(self.shape, &key);
529
+ if self.inline.len() >= PROPMAP_INLINE {
530
+ // Promote inline storage to an insertion-ordered map (keys + their order are preserved,
531
+ // so the shape stays valid).
532
+ let mut m: IndexMap<Arc<str>, Value, ahash::RandomState> =
533
+ IndexMap::with_capacity_and_hasher(self.inline.len() + 1, ahash::RandomState::default());
534
+ for (k, v) in self.inline.drain(..) {
535
+ m.insert(k, v);
536
+ }
537
+ m.insert(key, val);
538
+ self.map = Some(Box::new(m));
539
+ return None;
540
+ }
541
+ self.inline.push((key, val));
542
+ None
543
+ }
544
+
545
+ pub fn remove(&mut self, key: &str) -> Option<Value> {
546
+ let removed = match &mut self.map {
547
+ // shift_remove preserves insertion order (vs swap_remove).
548
+ Some(m) => m.shift_remove(key),
549
+ None => self
550
+ .inline
551
+ .iter()
552
+ .position(|(k, _)| k.as_ref() == key)
553
+ .map(|pos| self.inline.remove(pos).1),
554
+ };
555
+ if removed.is_some() {
556
+ // Deleting shifts slot indices → this object opts out of shape-based inline caches.
557
+ self.shape = crate::shape::DICT_SHAPE;
558
+ }
559
+ removed
560
+ }
561
+
562
+ // Iterators return concrete enum types (not `Box<dyn>`) so iteration never
563
+ // heap-allocates — critical for the per-request JSON stringify hot path
564
+ // (`json.rs` iterates `strings.keys()` on every response object).
565
+ #[inline]
566
+ pub fn iter(&self) -> PropMapIter<'_> {
567
+ match &self.map {
568
+ Some(m) => PropMapIter::Map(m.iter()),
569
+ None => PropMapIter::Inline(self.inline.iter()),
570
+ }
571
+ }
572
+
573
+ #[inline]
574
+ pub fn keys(&self) -> PropMapKeys<'_> {
575
+ match &self.map {
576
+ Some(m) => PropMapKeys::Map(m.keys()),
577
+ None => PropMapKeys::Inline(self.inline.iter()),
578
+ }
579
+ }
580
+
581
+ #[inline]
582
+ pub fn values(&self) -> PropMapValues<'_> {
583
+ match &self.map {
584
+ Some(m) => PropMapValues::Map(m.values()),
585
+ None => PropMapValues::Inline(self.inline.iter()),
586
+ }
587
+ }
588
+
589
+ pub fn reserve(&mut self, additional: usize) {
590
+ if let Some(m) = &mut self.map {
591
+ m.reserve(additional);
592
+ }
593
+ }
594
+ }
595
+
596
+ impl FromIterator<(Arc<str>, Value)> for PropMap {
597
+ fn from_iter<I: IntoIterator<Item = (Arc<str>, Value)>>(iter: I) -> Self {
598
+ let mut pm = PropMap::default();
599
+ for (k, v) in iter {
600
+ pm.insert(k, v);
601
+ }
602
+ pm
603
+ }
604
+ }
605
+
606
+ impl Extend<(Arc<str>, Value)> for PropMap {
607
+ fn extend<I: IntoIterator<Item = (Arc<str>, Value)>>(&mut self, iter: I) {
608
+ for (k, v) in iter {
609
+ self.insert(k, v);
610
+ }
611
+ }
612
+ }
613
+
614
+ impl IntoIterator for PropMap {
615
+ type Item = (Arc<str>, Value);
616
+ type IntoIter = PropMapIntoIter;
617
+ fn into_iter(self) -> Self::IntoIter {
618
+ match self.map {
619
+ Some(m) => PropMapIntoIter::Map(m.into_iter()),
620
+ None => PropMapIntoIter::Inline(self.inline.into_iter()),
621
+ }
622
+ }
623
+ }
624
+
625
+ /// Zero-allocation borrowing iterator over [`PropMap`] entries (insertion order).
626
+ pub enum PropMapIter<'a> {
627
+ Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
628
+ Map(indexmap::map::Iter<'a, Arc<str>, Value>),
629
+ }
630
+ impl<'a> Iterator for PropMapIter<'a> {
631
+ type Item = (&'a Arc<str>, &'a Value);
632
+ #[inline]
633
+ fn next(&mut self) -> Option<Self::Item> {
634
+ match self {
635
+ PropMapIter::Inline(it) => it.next().map(|(k, v)| (k, v)),
636
+ PropMapIter::Map(it) => it.next(),
637
+ }
638
+ }
639
+ }
640
+
641
+ /// Zero-allocation key iterator over [`PropMap`] (insertion order).
642
+ pub enum PropMapKeys<'a> {
643
+ Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
644
+ Map(indexmap::map::Keys<'a, Arc<str>, Value>),
645
+ }
646
+ impl<'a> Iterator for PropMapKeys<'a> {
647
+ type Item = &'a Arc<str>;
648
+ #[inline]
649
+ fn next(&mut self) -> Option<Self::Item> {
650
+ match self {
651
+ PropMapKeys::Inline(it) => it.next().map(|(k, _)| k),
652
+ PropMapKeys::Map(it) => it.next(),
653
+ }
654
+ }
655
+ }
656
+
657
+ /// Zero-allocation value iterator over [`PropMap`] (insertion order).
658
+ pub enum PropMapValues<'a> {
659
+ Inline(std::slice::Iter<'a, (Arc<str>, Value)>),
660
+ Map(indexmap::map::Values<'a, Arc<str>, Value>),
661
+ }
662
+ impl<'a> Iterator for PropMapValues<'a> {
663
+ type Item = &'a Value;
664
+ #[inline]
665
+ fn next(&mut self) -> Option<Self::Item> {
666
+ match self {
667
+ PropMapValues::Inline(it) => it.next().map(|(_, v)| v),
668
+ PropMapValues::Map(it) => it.next(),
669
+ }
670
+ }
671
+ }
672
+
673
+ /// Owning iterator over [`PropMap`] entries (insertion order).
674
+ #[allow(clippy::large_enum_variant)] // `Inline` is intentionally unboxed to keep PropMap iteration allocation-free
675
+ pub enum PropMapIntoIter {
676
+ Inline(smallvec::IntoIter<[(Arc<str>, Value); PROPMAP_INLINE]>),
677
+ Map(indexmap::map::IntoIter<Arc<str>, Value>),
678
+ }
679
+ impl Iterator for PropMapIntoIter {
680
+ type Item = (Arc<str>, Value);
681
+ #[inline]
682
+ fn next(&mut self) -> Option<Self::Item> {
683
+ match self {
684
+ PropMapIntoIter::Inline(it) => it.next(),
685
+ PropMapIntoIter::Map(it) => it.next(),
686
+ }
687
+ }
688
+ }
689
+
300
690
  /// Ordinary object: string-keyed properties plus optional symbol-keyed side map.
301
691
  #[derive(Clone, Debug, Default)]
302
692
  pub struct ObjectData {
303
- pub strings: ObjectMap,
693
+ pub strings: PropMap,
304
694
  pub symbols: Option<AHashMap<u64, Value>>,
305
695
  }
306
696
 
307
697
  impl ObjectData {
308
698
  #[inline]
309
- pub fn from_strings(strings: ObjectMap) -> Self {
699
+ pub fn from_strings<I: IntoIterator<Item = (Arc<str>, Value)>>(strings: I) -> Self {
310
700
  Self {
311
- strings,
701
+ strings: strings.into_iter().collect(),
312
702
  symbols: None,
313
703
  }
314
704
  }
@@ -355,7 +745,7 @@ pub fn object_set(obj: &Value, key: &Value, val: Value) -> Result<(), String> {
355
745
  Ok(())
356
746
  }
357
747
  Value::String(k) => {
358
- b.strings.insert(Arc::clone(k), val);
748
+ b.strings.insert(Arc::from(k.as_str()), val);
359
749
  Ok(())
360
750
  }
361
751
  _ => Err(format!(
@@ -382,10 +772,72 @@ pub fn object_has(obj: &Value, key: &Value) -> bool {
382
772
  }
383
773
  }
384
774
 
775
+ /// Drain a JS-style iterator object — one with a callable `next()` that returns
776
+ /// `{ value, done }` — into a `Vec`, calling `next()` until `done` is truthy. Returns
777
+ /// `None` when `obj` is not such an object (no callable `next`), so callers fall back
778
+ /// to their array/string handling. This is what makes a `Map`/`Set` iterator (the result
779
+ /// of `.values()`/`.keys()`/`.entries()`) usable in `for…of`, spread, and `Array.from`.
780
+ /// A missing/absent `done` is treated as truthy so a malformed object can't spin forever;
781
+ /// the native iterators always set `done`, so well-formed iteration is exact.
782
+ pub fn drain_iterator(obj: &Value) -> Option<Vec<Value>> {
783
+ if !matches!(obj, Value::Object(_)) {
784
+ return None;
785
+ }
786
+ // Fast path: tish's own `Map`/`Set` iterators expose `__drain__`, which returns the remaining
787
+ // items as one array (respecting the current position) and exhausts the iterator — so `for…of`
788
+ // and spread don't pay the per-element `{ value, done }` allocation of the generic `next()` loop.
789
+ if let Some(Value::Function(drain)) = object_get(obj, &Value::String("__drain__".into())) {
790
+ if let Value::Array(arr) = drain.call(&[]) {
791
+ return Some(arr.borrow().clone());
792
+ }
793
+ }
794
+ let Value::Function(next) = object_get(obj, &Value::String("next".into()))? else {
795
+ return None;
796
+ };
797
+ let done_key = Value::String("done".into());
798
+ let value_key = Value::String("value".into());
799
+ let mut out = Vec::new();
800
+ loop {
801
+ let res = next.call(&[]);
802
+ let done = object_get(&res, &done_key)
803
+ .map(|v| v.is_truthy())
804
+ .unwrap_or(true);
805
+ if done {
806
+ break;
807
+ }
808
+ out.push(object_get(&res, &value_key).unwrap_or(Value::Null));
809
+ }
810
+ Some(out)
811
+ }
812
+
813
+ /// JS `ToInt32`: NaN and ±Infinity map to `0`; every other value is truncated toward zero and
814
+ /// reduced modulo 2³². `f64 as i64` is exact for the finite `< 2⁶³` magnitudes real bitwise code
815
+ /// produces (then `as i32` truncates the low 32 bits = the modulo); the `is_finite` guard is what
816
+ /// makes `Infinity`/`-Infinity` correct — `f64 as i64` *saturates* (`+∞ → i64::MAX → -1`), which is
817
+ /// NOT the JS result. One always-predicted branch, so the hot path (finite hash values) is unaffected.
818
+ #[inline]
819
+ pub fn to_int32(x: f64) -> i32 {
820
+ if x.is_finite() {
821
+ x as i64 as i32
822
+ } else {
823
+ 0
824
+ }
825
+ }
826
+
827
+ /// JS `ToUint32`: as [`to_int32`] but reinterpreted unsigned (NaN/±Infinity → `0`).
828
+ #[inline]
829
+ pub fn to_uint32(x: f64) -> u32 {
830
+ if x.is_finite() {
831
+ x as i64 as u32
832
+ } else {
833
+ 0
834
+ }
835
+ }
836
+
385
837
  /// Invoke a callable [`Value`]: [`Value::Function`], or an object exposing `__call` (e.g. `Symbol`).
386
838
  pub fn value_call(callee: &Value, args: &[Value]) -> Value {
387
839
  match callee {
388
- Value::Function(f) => f(args),
840
+ Value::Function(f) => f.call(args),
389
841
  Value::Object(o) => {
390
842
  let inner = o.borrow().strings.get("__call").cloned();
391
843
  if let Some(inner) = inner {
@@ -407,7 +859,7 @@ pub fn value_call(callee: &Value, args: &[Value]) -> Value {
407
859
  pub fn merge_object_data(left: &VmRef<ObjectData>, right: &VmRef<ObjectData>) -> ObjectData {
408
860
  let l = left.borrow();
409
861
  let r = right.borrow();
410
- let mut strings = ObjectMap::with_capacity(l.strings.len() + r.strings.len());
862
+ let mut strings = PropMap::with_capacity(l.strings.len() + r.strings.len());
411
863
  strings.extend(l.strings.iter().map(|(k, v)| (Arc::clone(k), v.clone())));
412
864
  strings.extend(r.strings.iter().map(|(k, v)| (Arc::clone(k), v.clone())));
413
865
  let mut symbols: Option<AHashMap<u64, Value>> = None;
@@ -429,10 +881,11 @@ impl std::fmt::Debug for Value {
429
881
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
430
882
  match self {
431
883
  Value::Number(n) => write!(f, "Number({})", n),
432
- Value::String(s) => write!(f, "String({:?})", s.as_ref()),
884
+ Value::String(s) => write!(f, "String({:?})", s.as_str()),
433
885
  Value::Bool(b) => write!(f, "Bool({})", b),
434
886
  Value::Null => write!(f, "Null"),
435
887
  Value::Array(arr) => write!(f, "Array({:?})", arr.borrow()),
888
+ Value::NumberArray(arr) => write!(f, "NumberArray({:?})", arr.borrow()),
436
889
  Value::Object(obj) => write!(f, "Object({:?})", obj.borrow()),
437
890
  Value::Symbol(s) => write!(f, "Symbol({})", s.id),
438
891
  Value::Function(_) => write!(f, "Function"),
@@ -449,21 +902,81 @@ impl std::fmt::Debug for Value {
449
902
  }
450
903
  }
451
904
 
905
+ /// Format an `f64` exactly like JavaScript's `Number.prototype.toString` (radix 10) — the
906
+ /// algorithm behind `console.log(n)` and `String(n)`. Rust's default `{}` never uses
907
+ /// exponential form and so prints `6.022e23` as `602200000000000000000000`; JS switches to
908
+ /// exponential when the decimal point lands past digit 21 or before digit −6.
909
+ ///
910
+ /// We take the shortest round-tripping digits from Rust's `{:e}` (a Ryū/Grisu-class shortest
911
+ /// formatter, matching V8's digit choice) and lay them out per the ECMAScript rule: plain
912
+ /// 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).
915
+ pub fn js_number_to_string(value: f64) -> String {
916
+ if value.is_nan() {
917
+ return "NaN".to_string();
918
+ }
919
+ if value == f64::INFINITY {
920
+ return "Infinity".to_string();
921
+ }
922
+ if value == f64::NEG_INFINITY {
923
+ return "-Infinity".to_string();
924
+ }
925
+ if value == 0.0 {
926
+ return if value.is_sign_negative() { "-0" } else { "0" }.to_string();
927
+ }
928
+
929
+ let negative = value < 0.0;
930
+ // Shortest round-trip digits + base-10 exponent, e.g. "6.022e23" → ("6022", 23).
931
+ let sci = format!("{:e}", value.abs());
932
+ let (mantissa, exp_str) = sci
933
+ .split_once('e')
934
+ .expect("LowerExp formatting always contains 'e'");
935
+ let exp: i32 = exp_str
936
+ .parse()
937
+ .expect("LowerExp exponent is a valid integer");
938
+ let digits: String = mantissa.chars().filter(|&c| c != '.').collect();
939
+ let k = digits.len() as i32; // significant digit count (≤ 17 for an f64)
940
+ let point = exp + 1; // ECMAScript's `n`: value = digits × 10^(point − k)
941
+
942
+ let mut out = String::new();
943
+ if negative {
944
+ out.push('-');
945
+ }
946
+ if k <= point && point <= 21 {
947
+ // Integer, zero-padded: digits then (point − k) trailing zeros.
948
+ out.push_str(&digits);
949
+ out.push_str(&"0".repeat((point - k) as usize));
950
+ } else if 0 < point && point <= 21 {
951
+ // Decimal point inside the digit string.
952
+ out.push_str(&digits[..point as usize]);
953
+ out.push('.');
954
+ out.push_str(&digits[point as usize..]);
955
+ } else if -6 < point && point <= 0 {
956
+ // Leading-zero fraction: "0." then (−point) zeros then the digits.
957
+ out.push_str("0.");
958
+ out.push_str(&"0".repeat((-point) as usize));
959
+ out.push_str(&digits);
960
+ } else {
961
+ // Exponential: first digit, optional `.rest`, then `e±E`.
962
+ let e = point - 1;
963
+ out.push_str(&digits[..1]);
964
+ if k > 1 {
965
+ out.push('.');
966
+ out.push_str(&digits[1..]);
967
+ }
968
+ out.push('e');
969
+ out.push(if e >= 0 { '+' } else { '-' });
970
+ out.push_str(&e.abs().to_string());
971
+ }
972
+ out
973
+ }
974
+
452
975
  impl Value {
453
976
  /// Convert value to display string (for console output).
454
977
  pub fn to_display_string(&self) -> String {
455
978
  match self {
456
- Value::Number(n) => {
457
- if n.is_nan() {
458
- "NaN".to_string()
459
- } else if *n == f64::INFINITY {
460
- "Infinity".to_string()
461
- } else if *n == f64::NEG_INFINITY {
462
- "-Infinity".to_string()
463
- } else {
464
- n.to_string()
465
- }
466
- }
979
+ Value::Number(n) => js_number_to_string(*n),
467
980
  Value::String(s) => s.to_string(),
468
981
  Value::Bool(b) => b.to_string(),
469
982
  Value::Null => "null".to_string(),
@@ -472,6 +985,14 @@ impl Value {
472
985
  arr.borrow().iter().map(|v| v.to_display_string()).collect();
473
986
  format!("[{}]", inner.join(", "))
474
987
  }
988
+ Value::NumberArray(arr) => {
989
+ let inner: Vec<String> = arr
990
+ .borrow()
991
+ .iter()
992
+ .map(|&n| if n.is_nan() { "null".to_string() } else { Value::Number(n).to_display_string() })
993
+ .collect();
994
+ format!("[{}]", inner.join(", "))
995
+ }
475
996
  Value::Object(obj) => {
476
997
  let inner: Vec<String> = obj
477
998
  .borrow()
@@ -499,6 +1020,37 @@ impl Value {
499
1020
  }
500
1021
  }
501
1022
 
1023
+ /// JavaScript `ToString` coercion (the value's `.toString()`), as used by `Array.prototype.join`,
1024
+ /// string concatenation, and template literals — distinct from [`Self::to_display_string`], which
1025
+ /// is the *inspect/console* form (arrays bracketed, strings quoted in some contexts). The key
1026
+ /// JS-conformance differences from display: a nested **array** stringifies to its own
1027
+ /// comma-joined `toString` (recursively, always `,` regardless of the outer separator), an
1028
+ /// **object** becomes `"[object Object]"`, and primitives render as their plain value. `null`
1029
+ /// renders as `"null"` here (matching `String(null)`); `join` itself maps `null`/`undefined`
1030
+ /// elements to `""` *before* calling this, per the spec.
1031
+ pub fn to_js_string(&self) -> String {
1032
+ match self {
1033
+ Value::Array(arr) => arr
1034
+ .borrow()
1035
+ .iter()
1036
+ .map(|v| match v {
1037
+ Value::Null => String::new(),
1038
+ other => other.to_js_string(),
1039
+ })
1040
+ .collect::<Vec<_>>()
1041
+ .join(","),
1042
+ Value::NumberArray(arr) => arr
1043
+ .borrow()
1044
+ .iter()
1045
+ .map(|n| Value::Number(*n).to_js_string())
1046
+ .collect::<Vec<_>>()
1047
+ .join(","),
1048
+ Value::Object(_) => "[object Object]".to_string(),
1049
+ // Primitives (and the remaining cases) coincide with the display form.
1050
+ _ => self.to_display_string(),
1051
+ }
1052
+ }
1053
+
502
1054
  /// Check if value is truthy (for conditionals).
503
1055
  pub fn is_truthy(&self) -> bool {
504
1056
  match self {
@@ -524,6 +1076,7 @@ impl Value {
524
1076
  (Value::Bool(a), Value::Bool(b)) => a == b,
525
1077
  (Value::Null, Value::Null) => true,
526
1078
  (Value::Array(a), Value::Array(b)) => VmRef::ptr_eq(a, b),
1079
+ (Value::NumberArray(a), Value::NumberArray(b)) => VmRef::ptr_eq(a, b),
527
1080
  (Value::Object(a), Value::Object(b)) => VmRef::ptr_eq(a, b),
528
1081
  #[cfg(feature = "send-values")]
529
1082
  (Value::Function(a), Value::Function(b)) => Arc::ptr_eq(a, b),
@@ -549,7 +1102,7 @@ impl Value {
549
1102
  where
550
1103
  F: Fn(&[Value]) -> Value + Send + Sync + 'static,
551
1104
  {
552
- Value::Function(Arc::new(f))
1105
+ Value::Function(Arc::new(FnCallable(f)))
553
1106
  }
554
1107
 
555
1108
  #[cfg(not(feature = "send-values"))]
@@ -557,7 +1110,7 @@ impl Value {
557
1110
  where
558
1111
  F: Fn(&[Value]) -> Value + 'static,
559
1112
  {
560
- Value::Function(std::rc::Rc::new(f))
1113
+ Value::Function(std::rc::Rc::new(FnCallable(f)))
561
1114
  }
562
1115
 
563
1116
  /// Create a new array Value from a Vec.
@@ -570,11 +1123,66 @@ impl Value {
570
1123
  Value::Object(VmRef::new(ObjectData::from_strings(map)))
571
1124
  }
572
1125
 
1126
+ /// Create an object directly from key/value pairs, building the `PropMap`
1127
+ /// in one pass with **no intermediate `AHashMap`**. Used by the Rust
1128
+ /// backend's object-literal codegen and any hot path that knows its pairs,
1129
+ /// so small objects (the common case) cost a single inline allocation.
1130
+ pub fn object_from_pairs<const N: usize>(pairs: [(Arc<str>, Value); N]) -> Self {
1131
+ let mut strings = PropMap::with_capacity(N);
1132
+ for (k, v) in pairs {
1133
+ strings.insert(k, v);
1134
+ }
1135
+ Value::Object(VmRef::new(ObjectData {
1136
+ strings,
1137
+ symbols: None,
1138
+ }))
1139
+ }
1140
+
573
1141
  /// Create an empty array Value.
574
1142
  pub fn empty_array() -> Self {
575
1143
  Value::Array(VmRef::new(Vec::new()))
576
1144
  }
577
1145
 
1146
+ // -------------------------------------------------------------------------
1147
+ // Packed f64 array support (TISH_PACKED_ARRAYS)
1148
+ // -------------------------------------------------------------------------
1149
+
1150
+ /// 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.
1152
+ /// The flag is intentionally backwards from the slot/JIT flags (those were default-on) to
1153
+ /// keep the default binary behaviour byte-identical while we validate coverage.
1154
+ #[inline]
1155
+ pub fn packed_arrays_enabled() -> bool {
1156
+ std::env::var("TISH_PACKED_ARRAYS").map(|v| v == "1").unwrap_or(false)
1157
+ }
1158
+
1159
+ /// Wrap a `Vec<f64>` as a `Value::NumberArray`. Only call when `packed_arrays_enabled()`.
1160
+ #[inline]
1161
+ pub fn number_array(items: Vec<f64>) -> Self {
1162
+ Value::NumberArray(VmRef::new(items))
1163
+ }
1164
+
1165
+ /// Materialize a `Value::NumberArray` into a boxed `Value::Array`.
1166
+ /// Called on the deopt path: any operation that doesn't have a packed fast path
1167
+ /// (non-numeric push, getIndex-beyond-bounds, spread into non-numeric context, etc.)
1168
+ /// converts once and continues on the generic path. The original `NumberArray` VmRef
1169
+ /// is consumed; callers replace the `Value` in whatever container held it.
1170
+ #[inline]
1171
+ pub fn materialize_number_array(arr: &VmRef<Vec<f64>>) -> Value {
1172
+ let nums = arr.borrow();
1173
+ Value::Array(VmRef::new(nums.iter().map(|&n| Value::Number(n)).collect()))
1174
+ }
1175
+
1176
+ /// If `self` is a `NumberArray`, materialise and return `Value::Array`; otherwise
1177
+ /// return `self` unchanged. Convenience deopt for callers that pattern-match on `Array`.
1178
+ #[inline]
1179
+ pub fn coerce_number_array(self) -> Value {
1180
+ match self {
1181
+ Value::NumberArray(ref arr) => Value::materialize_number_array(arr),
1182
+ other => other,
1183
+ }
1184
+ }
1185
+
578
1186
  /// Create an empty object Value.
579
1187
  pub fn empty_object() -> Self {
580
1188
  Value::Object(VmRef::new(ObjectData::default()))
@@ -595,7 +1203,7 @@ impl Value {
595
1203
  Value::String(_) => "string",
596
1204
  Value::Bool(_) => "boolean",
597
1205
  Value::Null => "null",
598
- Value::Array(_) => "object",
1206
+ Value::Array(_) | Value::NumberArray(_) => "object",
599
1207
  Value::Object(_) => "object",
600
1208
  Value::Function(_) => "function",
601
1209
  #[cfg(feature = "regex")]
@@ -694,3 +1302,49 @@ impl Value {
694
1302
  }
695
1303
  }
696
1304
  }
1305
+
1306
+ #[cfg(test)]
1307
+ mod number_to_string_tests {
1308
+ use super::js_number_to_string;
1309
+
1310
+ #[test]
1311
+ fn matches_javascript_number_tostring() {
1312
+ // (value, expected) — every `expected` is what Node's `String(value)` produces.
1313
+ let cases: &[(f64, &str)] = &[
1314
+ (0.0, "0"),
1315
+ (-0.0, "-0"),
1316
+ (123.0, "123"),
1317
+ (123.456, "123.456"),
1318
+ (0.5, "0.5"),
1319
+ (-123.456, "-123.456"),
1320
+ (100000.0, "100000"),
1321
+ // Decimal/exponential boundary on the large side: 1e21 flips to exponential.
1322
+ (1e20, "100000000000000000000"),
1323
+ (1e21, "1e+21"),
1324
+ (21e18, "21000000000000000000"),
1325
+ // Small side: 1e-6 is decimal, 1e-7 is exponential.
1326
+ (1e-6, "0.000001"),
1327
+ (1e-7, "1e-7"),
1328
+ (9.5e-7, "9.5e-7"),
1329
+ // Exponential with a multi-digit mantissa.
1330
+ (6.022e23, "6.022e+23"),
1331
+ (1.2345678901234568e21, "1.2345678901234568e+21"),
1332
+ (1e100, "1e+100"),
1333
+ (-1e21, "-1e+21"),
1334
+ // Subnormal min and normal max.
1335
+ (5e-324, "5e-324"),
1336
+ (1.7976931348623157e308, "1.7976931348623157e+308"),
1337
+ // Shortest round-trip mantissa (not full precision).
1338
+ (0.1, "0.1"),
1339
+ (0.1 + 0.2, "0.30000000000000004"),
1340
+ // Non-finite.
1341
+ (f64::INFINITY, "Infinity"),
1342
+ (f64::NEG_INFINITY, "-Infinity"),
1343
+ (f64::NAN, "NaN"),
1344
+ ];
1345
+ for &(value, expected) in cases {
1346
+ assert_eq!(js_number_to_string(value), expected, "for {value:?}");
1347
+ }
1348
+ }
1349
+ }
1350
+