@tishlang/tish 1.13.1 → 2.0.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.
- package/Cargo.toml +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +43 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -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
|
|
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
|
|
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(
|
|
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:
|
|
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:
|
|
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::
|
|
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 =
|
|
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.
|
|
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
|
+
|