@tishlang/tish-format 1.0.12 → 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.
- package/Cargo.toml +51 -0
- package/LICENSE +13 -0
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/Cargo.toml +11 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +55 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +611 -0
- package/crates/js_to_tish/src/transform/stmt.rs +503 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +62 -0
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cargo_native_registry.rs +32 -0
- package/crates/tish/src/cli_help.rs +576 -0
- package/crates/tish/src/main.rs +853 -0
- package/crates/tish/src/repl_completion.rs +199 -0
- package/crates/tish/tests/cargo_example_compile.rs +67 -0
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
- package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
- package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
- package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -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 +1406 -0
- package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
- package/crates/tish/tests/shortcircuit.rs +65 -0
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/Cargo.toml +9 -0
- package/crates/tish_ast/src/ast.rs +649 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +11 -0
- package/crates/tish_build_utils/src/lib.rs +577 -0
- package/crates/tish_builtins/Cargo.toml +22 -0
- package/crates/tish_builtins/src/array.rs +803 -0
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +199 -0
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +293 -0
- package/crates/tish_builtins/src/helpers.rs +35 -0
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +21 -0
- package/crates/tish_builtins/src/math.rs +89 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +646 -0
- package/crates/tish_builtins/src/symbol.rs +83 -0
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/Cargo.toml +17 -0
- package/crates/tish_bytecode/src/chunk.rs +164 -0
- package/crates/tish_bytecode/src/compiler.rs +2604 -0
- package/crates/tish_bytecode/src/encoding.rs +102 -0
- package/crates/tish_bytecode/src/lib.rs +20 -0
- package/crates/tish_bytecode/src/opcode.rs +185 -0
- package/crates/tish_bytecode/src/peephole.rs +189 -0
- package/crates/tish_bytecode/src/serialize.rs +193 -0
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +27 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +7317 -0
- package/crates/tish_compile/src/infer.rs +1681 -0
- package/crates/tish_compile/src/lib.rs +206 -0
- package/crates/tish_compile/src/resolve.rs +1951 -0
- package/crates/tish_compile/src/types.rs +605 -0
- package/crates/tish_compile_js/Cargo.toml +18 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +938 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/lib.rs +26 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
- package/crates/tish_compiler_wasm/Cargo.toml +21 -0
- package/crates/tish_compiler_wasm/src/lib.rs +57 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
- package/crates/tish_core/Cargo.toml +32 -0
- package/crates/tish_core/src/console_style.rs +170 -0
- package/crates/tish_core/src/json.rs +430 -0
- package/crates/tish_core/src/lib.rs +20 -0
- package/crates/tish_core/src/macros.rs +36 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/uri.rs +118 -0
- package/crates/tish_core/src/value.rs +1350 -0
- package/crates/tish_core/src/vmref.rs +183 -0
- package/crates/tish_cranelift/Cargo.toml +19 -0
- package/crates/tish_cranelift/src/lib.rs +43 -0
- package/crates/tish_cranelift/src/link.rs +130 -0
- package/crates/tish_cranelift/src/lower.rs +85 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
- package/crates/tish_eval/Cargo.toml +51 -0
- package/crates/tish_eval/src/eval.rs +4265 -0
- package/crates/tish_eval/src/http.rs +191 -0
- package/crates/tish_eval/src/lib.rs +99 -0
- package/crates/tish_eval/src/natives.rs +551 -0
- package/crates/tish_eval/src/promise.rs +179 -0
- package/crates/tish_eval/src/regex.rs +299 -0
- package/crates/tish_eval/src/timers.rs +120 -0
- package/crates/tish_eval/src/value.rs +336 -0
- package/crates/tish_eval/src/value_convert.rs +117 -0
- 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/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +2157 -0
- package/crates/tish_jsx_web/Cargo.toml +9 -0
- package/crates/tish_jsx_web/README.md +5 -0
- package/crates/tish_jsx_web/src/lib.rs +2 -0
- package/crates/tish_lexer/Cargo.toml +9 -0
- package/crates/tish_lexer/src/lib.rs +1104 -0
- package/crates/tish_lexer/src/token.rs +170 -0
- package/crates/tish_lint/Cargo.toml +18 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
- package/crates/tish_lint/src/lib.rs +281 -0
- package/crates/tish_llvm/Cargo.toml +13 -0
- package/crates/tish_llvm/src/lib.rs +115 -0
- package/crates/tish_lsp/Cargo.toml +25 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/builtin_goto.rs +362 -0
- package/crates/tish_lsp/src/import_goto.rs +564 -0
- package/crates/tish_lsp/src/main.rs +1459 -0
- package/crates/tish_native/Cargo.toml +16 -0
- package/crates/tish_native/src/build.rs +481 -0
- package/crates/tish_native/src/config.rs +48 -0
- package/crates/tish_native/src/lib.rs +416 -0
- package/crates/tish_opt/Cargo.toml +13 -0
- package/crates/tish_opt/src/lib.rs +1046 -0
- package/crates/tish_parser/Cargo.toml +11 -0
- package/crates/tish_parser/src/lib.rs +386 -0
- package/crates/tish_parser/src/parser.rs +2726 -0
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +955 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3601 -0
- package/crates/tish_resolve/src/pos.rs +141 -0
- package/crates/tish_runtime/Cargo.toml +100 -0
- package/crates/tish_runtime/src/http.rs +1347 -0
- package/crates/tish_runtime/src/http_fetch.rs +492 -0
- package/crates/tish_runtime/src/http_hyper.rs +441 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +1447 -0
- package/crates/tish_runtime/src/native_promise.rs +15 -0
- package/crates/tish_runtime/src/promise.rs +558 -0
- package/crates/tish_runtime/src/promise_io.rs +38 -0
- package/crates/tish_runtime/src/timers.rs +172 -0
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +778 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
- package/crates/tish_ui/Cargo.toml +17 -0
- package/crates/tish_ui/src/jsx.rs +692 -0
- package/crates/tish_ui/src/lib.rs +20 -0
- package/crates/tish_ui/src/runtime/hooks.rs +573 -0
- package/crates/tish_ui/src/runtime/mod.rs +183 -0
- package/crates/tish_vm/Cargo.toml +60 -0
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +41 -0
- package/crates/tish_vm/src/vm.rs +3536 -0
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
- package/crates/tish_wasm/Cargo.toml +15 -0
- package/crates/tish_wasm/src/lib.rs +428 -0
- package/crates/tish_wasm_runtime/Cargo.toml +37 -0
- package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
- package/crates/tish_wasm_runtime/src/lib.rs +42 -0
- package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
- package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
- package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
- package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
- package/justfile +276 -0
- package/package.json +2 -2
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -0,0 +1,1350 @@
|
|
|
1
|
+
//! Unified Value type for Tish runtime values.
|
|
2
|
+
|
|
3
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
4
|
+
use std::sync::Arc;
|
|
5
|
+
|
|
6
|
+
use ahash::AHashMap;
|
|
7
|
+
use indexmap::IndexMap;
|
|
8
|
+
use smallvec::SmallVec;
|
|
9
|
+
|
|
10
|
+
use crate::vmref::VmRef;
|
|
11
|
+
|
|
12
|
+
/// Property map for objects and other `Arc<str>` → `Value` tables (VM globals, scopes).
|
|
13
|
+
/// Uses a faster hasher than `std::collections::HashMap` for string-heavy workloads.
|
|
14
|
+
pub type ObjectMap = AHashMap<Arc<str>, Value>;
|
|
15
|
+
|
|
16
|
+
static NEXT_SYMBOL_ID: AtomicU64 = AtomicU64::new(1);
|
|
17
|
+
|
|
18
|
+
fn next_symbol_id() -> u64 {
|
|
19
|
+
NEXT_SYMBOL_ID.fetch_add(1, Ordering::Relaxed)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Allocate a unique symbol id (for `Symbol()` and first-time `Symbol.for` entries).
|
|
23
|
+
#[inline]
|
|
24
|
+
pub fn alloc_symbol_id() -> u64 {
|
|
25
|
+
next_symbol_id()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Primitive Symbol (ECMAScript-style): identity is `Arc` pointer equality.
|
|
29
|
+
#[derive(Debug)]
|
|
30
|
+
pub struct TishSymbol {
|
|
31
|
+
pub id: u64,
|
|
32
|
+
pub description: Option<Arc<str>>,
|
|
33
|
+
/// Set when created via `Symbol.for(key)` (global registry).
|
|
34
|
+
pub registry_key: Option<Arc<str>>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl TishSymbol {
|
|
38
|
+
/// Unique symbol (`Symbol("desc")`).
|
|
39
|
+
pub fn new_unique(description: Option<Arc<str>>) -> Arc<Self> {
|
|
40
|
+
Arc::new(Self {
|
|
41
|
+
id: next_symbol_id(),
|
|
42
|
+
description,
|
|
43
|
+
registry_key: None,
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Registry symbol (`Symbol.for`): stable `id` for this registry key.
|
|
48
|
+
pub fn new_registry(id: u64, registry_key: Arc<str>, description: Option<Arc<str>>) -> Arc<Self> {
|
|
49
|
+
Arc::new(Self {
|
|
50
|
+
id,
|
|
51
|
+
description,
|
|
52
|
+
registry_key: Some(registry_key),
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[cfg(feature = "regex")]
|
|
58
|
+
use fancy_regex::Regex;
|
|
59
|
+
|
|
60
|
+
/// Native function signature.
|
|
61
|
+
///
|
|
62
|
+
/// When the `send-values` feature is enabled this is
|
|
63
|
+
/// `Arc<dyn Fn + Send + Sync>`, so handler closures can be dispatched across
|
|
64
|
+
/// HTTP worker threads (`tishlang_runtime::http::serve`). Otherwise it stays
|
|
65
|
+
/// `Rc<dyn Fn>` for zero-overhead single-threaded execution (wasm / wasi /
|
|
66
|
+
/// interpreter / cranelift / llvm VMs and any Rust native build without
|
|
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.
|
|
74
|
+
#[cfg(feature = "send-values")]
|
|
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>;
|
|
111
|
+
#[cfg(not(feature = "send-values"))]
|
|
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
|
+
}
|
|
125
|
+
|
|
126
|
+
/// Trait for opaque Rust types exposed to Tish (e.g. Polars DataFrame).
|
|
127
|
+
/// Implementors provide method dispatch so Tish can call methods on the value.
|
|
128
|
+
///
|
|
129
|
+
/// The `Send + Sync` supertrait bound is conditional on the `send-values`
|
|
130
|
+
/// feature. When `send-values` is off (single-threaded VMs: wasm browser /
|
|
131
|
+
/// wasi / interpreter / cranelift), `NativeFn` is already `Rc<dyn Fn>`, so
|
|
132
|
+
/// `Value` is `!Send` anyway — dropping the bound here loses nothing and lets
|
|
133
|
+
/// `!Send` opaques like `JsHandle(wasm_bindgen::JsValue)` be stored in a
|
|
134
|
+
/// `Value::Opaque` on the browser runtime.
|
|
135
|
+
#[cfg(feature = "send-values")]
|
|
136
|
+
pub trait TishOpaque: Send + Sync {
|
|
137
|
+
/// Display name for the type (e.g. "DataFrame").
|
|
138
|
+
fn type_name(&self) -> &'static str;
|
|
139
|
+
|
|
140
|
+
/// Get a method by name. Returns a native function if the method exists.
|
|
141
|
+
fn get_method(&self, name: &str) -> Option<NativeFn>;
|
|
142
|
+
|
|
143
|
+
/// For downcasting `Arc<dyn TishOpaque>` in native crates (e.g. Polars → `DataFrame`).
|
|
144
|
+
fn as_any(&self) -> &dyn std::any::Any;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/// Single-threaded variant (no `Send + Sync` bound); see the `send-values` doc above.
|
|
148
|
+
#[cfg(not(feature = "send-values"))]
|
|
149
|
+
pub trait TishOpaque {
|
|
150
|
+
/// Display name for the type (e.g. "DataFrame").
|
|
151
|
+
fn type_name(&self) -> &'static str;
|
|
152
|
+
|
|
153
|
+
/// Get a method by name. Returns a native function if the method exists.
|
|
154
|
+
fn get_method(&self, name: &str) -> Option<NativeFn>;
|
|
155
|
+
|
|
156
|
+
/// For downcasting `Arc<dyn TishOpaque>` in native crates (e.g. Polars → `DataFrame`).
|
|
157
|
+
fn as_any(&self) -> &dyn std::any::Any;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Trait for Promise-like values that can be awaited (block until settled).
|
|
161
|
+
/// Implemented by the runtime for native compile; interpreter uses its own Promise.
|
|
162
|
+
pub trait TishPromise: Send + Sync {
|
|
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
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// JavaScript RegExp flags
|
|
178
|
+
#[cfg(feature = "regex")]
|
|
179
|
+
#[derive(Debug, Clone, Default)]
|
|
180
|
+
pub struct RegExpFlags {
|
|
181
|
+
pub global: bool,
|
|
182
|
+
pub ignore_case: bool,
|
|
183
|
+
pub multiline: bool,
|
|
184
|
+
pub dot_all: bool,
|
|
185
|
+
pub unicode: bool,
|
|
186
|
+
pub sticky: bool,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
#[cfg(feature = "regex")]
|
|
190
|
+
impl RegExpFlags {
|
|
191
|
+
pub fn from_string(flags: &str) -> Result<Self, String> {
|
|
192
|
+
let mut result = Self::default();
|
|
193
|
+
for c in flags.chars() {
|
|
194
|
+
match c {
|
|
195
|
+
'g' => {
|
|
196
|
+
if result.global {
|
|
197
|
+
return Err(format!("duplicate flag '{}'", c));
|
|
198
|
+
}
|
|
199
|
+
result.global = true;
|
|
200
|
+
}
|
|
201
|
+
'i' => {
|
|
202
|
+
if result.ignore_case {
|
|
203
|
+
return Err(format!("duplicate flag '{}'", c));
|
|
204
|
+
}
|
|
205
|
+
result.ignore_case = true;
|
|
206
|
+
}
|
|
207
|
+
'm' => {
|
|
208
|
+
if result.multiline {
|
|
209
|
+
return Err(format!("duplicate flag '{}'", c));
|
|
210
|
+
}
|
|
211
|
+
result.multiline = true;
|
|
212
|
+
}
|
|
213
|
+
's' => {
|
|
214
|
+
if result.dot_all {
|
|
215
|
+
return Err(format!("duplicate flag '{}'", c));
|
|
216
|
+
}
|
|
217
|
+
result.dot_all = true;
|
|
218
|
+
}
|
|
219
|
+
'u' => {
|
|
220
|
+
if result.unicode {
|
|
221
|
+
return Err(format!("duplicate flag '{}'", c));
|
|
222
|
+
}
|
|
223
|
+
result.unicode = true;
|
|
224
|
+
}
|
|
225
|
+
'y' => {
|
|
226
|
+
if result.sticky {
|
|
227
|
+
return Err(format!("duplicate flag '{}'", c));
|
|
228
|
+
}
|
|
229
|
+
result.sticky = true;
|
|
230
|
+
}
|
|
231
|
+
_ => return Err(format!("unknown flag '{}'", c)),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
Ok(result)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
#[cfg(feature = "regex")]
|
|
239
|
+
impl std::fmt::Display for RegExpFlags {
|
|
240
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
241
|
+
if self.global {
|
|
242
|
+
f.write_str("g")?;
|
|
243
|
+
}
|
|
244
|
+
if self.ignore_case {
|
|
245
|
+
f.write_str("i")?;
|
|
246
|
+
}
|
|
247
|
+
if self.multiline {
|
|
248
|
+
f.write_str("m")?;
|
|
249
|
+
}
|
|
250
|
+
if self.dot_all {
|
|
251
|
+
f.write_str("s")?;
|
|
252
|
+
}
|
|
253
|
+
if self.unicode {
|
|
254
|
+
f.write_str("u")?;
|
|
255
|
+
}
|
|
256
|
+
if self.sticky {
|
|
257
|
+
f.write_str("y")?;
|
|
258
|
+
}
|
|
259
|
+
Ok(())
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// Tish RegExp object
|
|
264
|
+
#[cfg(feature = "regex")]
|
|
265
|
+
#[derive(Debug, Clone)]
|
|
266
|
+
pub struct TishRegExp {
|
|
267
|
+
pub source: String,
|
|
268
|
+
pub flags: RegExpFlags,
|
|
269
|
+
pub regex: Arc<Regex>,
|
|
270
|
+
pub last_index: usize,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[cfg(feature = "regex")]
|
|
274
|
+
impl TishRegExp {
|
|
275
|
+
pub fn new(pattern: &str, flags_str: &str) -> Result<Self, String> {
|
|
276
|
+
let flags = RegExpFlags::from_string(flags_str)?;
|
|
277
|
+
let mut regex_pattern = pattern.to_string();
|
|
278
|
+
|
|
279
|
+
if flags.ignore_case || flags.multiline || flags.dot_all {
|
|
280
|
+
let mut flag_prefix = String::from("(?");
|
|
281
|
+
if flags.ignore_case {
|
|
282
|
+
flag_prefix.push('i');
|
|
283
|
+
}
|
|
284
|
+
if flags.multiline {
|
|
285
|
+
flag_prefix.push('m');
|
|
286
|
+
}
|
|
287
|
+
if flags.dot_all {
|
|
288
|
+
flag_prefix.push('s');
|
|
289
|
+
}
|
|
290
|
+
flag_prefix.push(')');
|
|
291
|
+
regex_pattern = format!("{}{}", flag_prefix, regex_pattern);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let regex =
|
|
295
|
+
Regex::new(®ex_pattern).map_err(|e| format!("Invalid regular expression: {}", e))?;
|
|
296
|
+
|
|
297
|
+
Ok(Self {
|
|
298
|
+
source: pattern.to_string(),
|
|
299
|
+
flags,
|
|
300
|
+
regex: Arc::new(regex),
|
|
301
|
+
last_index: 0,
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
pub fn flags_string(&self) -> String {
|
|
306
|
+
self.flags.to_string()
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
pub fn test(&mut self, input: &str) -> bool {
|
|
310
|
+
if self.flags.global || self.flags.sticky {
|
|
311
|
+
let start = self.last_index;
|
|
312
|
+
if start > input.chars().count() {
|
|
313
|
+
self.last_index = 0;
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let byte_start: usize = input.chars().take(start).map(|c| c.len_utf8()).sum();
|
|
318
|
+
let search_str = &input[byte_start..];
|
|
319
|
+
|
|
320
|
+
match self.regex.find(search_str) {
|
|
321
|
+
Ok(Some(m)) => {
|
|
322
|
+
if self.flags.sticky && m.start() != 0 {
|
|
323
|
+
self.last_index = 0;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
let match_end_chars = input[byte_start..byte_start + m.end()].chars().count();
|
|
327
|
+
self.last_index = start + match_end_chars;
|
|
328
|
+
true
|
|
329
|
+
}
|
|
330
|
+
_ => {
|
|
331
|
+
self.last_index = 0;
|
|
332
|
+
false
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
self.regex.is_match(input).unwrap_or(false)
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Runtime value for Tish programs.
|
|
342
|
+
/// Used by both interpreter and compiled code.
|
|
343
|
+
///
|
|
344
|
+
/// **Thread safety**: `Value: Send + Sync`. Mutable payloads live inside
|
|
345
|
+
/// [`VmRef`], a `Send + Sync` `Arc<Mutex<T>>` wrapper that preserves the
|
|
346
|
+
/// `RefCell`-style borrow API. Functions are `Arc<dyn Fn + Send + Sync>`.
|
|
347
|
+
#[derive(Clone, Default)]
|
|
348
|
+
pub enum Value {
|
|
349
|
+
Number(f64),
|
|
350
|
+
String(arcstr::ArcStr),
|
|
351
|
+
Bool(bool),
|
|
352
|
+
#[default]
|
|
353
|
+
Null,
|
|
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>>),
|
|
361
|
+
Object(VmRef<ObjectData>),
|
|
362
|
+
/// ECMAScript-style primitive symbol (identity by `Arc`).
|
|
363
|
+
Symbol(Arc<TishSymbol>),
|
|
364
|
+
Function(NativeFn),
|
|
365
|
+
#[cfg(feature = "regex")]
|
|
366
|
+
RegExp(VmRef<TishRegExp>),
|
|
367
|
+
/// Promise (for native compile). Interpreter uses tishlang_eval::Value::Promise.
|
|
368
|
+
Promise(Arc<dyn TishPromise>),
|
|
369
|
+
/// Opaque handle to a native Rust type (e.g. Polars DataFrame).
|
|
370
|
+
Opaque(Arc<dyn TishOpaque>),
|
|
371
|
+
}
|
|
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
|
+
|
|
690
|
+
/// Ordinary object: string-keyed properties plus optional symbol-keyed side map.
|
|
691
|
+
#[derive(Clone, Debug, Default)]
|
|
692
|
+
pub struct ObjectData {
|
|
693
|
+
pub strings: PropMap,
|
|
694
|
+
pub symbols: Option<AHashMap<u64, Value>>,
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
impl ObjectData {
|
|
698
|
+
#[inline]
|
|
699
|
+
pub fn from_strings<I: IntoIterator<Item = (Arc<str>, Value)>>(strings: I) -> Self {
|
|
700
|
+
Self {
|
|
701
|
+
strings: strings.into_iter().collect(),
|
|
702
|
+
symbols: None,
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
#[inline]
|
|
707
|
+
pub fn len_entries(&self) -> usize {
|
|
708
|
+
self.strings.len() + self.symbols.as_ref().map(|s| s.len()).unwrap_or(0)
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/// Read a property from an object value.
|
|
713
|
+
pub fn object_get(obj: &Value, key: &Value) -> Option<Value> {
|
|
714
|
+
let Value::Object(od) = obj else {
|
|
715
|
+
return None;
|
|
716
|
+
};
|
|
717
|
+
let b = od.borrow();
|
|
718
|
+
match key {
|
|
719
|
+
Value::Symbol(s) => b.symbols.as_ref()?.get(&s.id).cloned(),
|
|
720
|
+
Value::Number(n) => {
|
|
721
|
+
let k: Arc<str> = n.to_string().into();
|
|
722
|
+
b.strings.get(&k).cloned()
|
|
723
|
+
}
|
|
724
|
+
Value::String(k) => b.strings.get(k.as_ref()).cloned(),
|
|
725
|
+
_ => None,
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/// Set a property on an object.
|
|
730
|
+
pub fn object_set(obj: &Value, key: &Value, val: Value) -> Result<(), String> {
|
|
731
|
+
let Value::Object(od) = obj else {
|
|
732
|
+
return Err(format!("Cannot set property on {}", obj.type_name()));
|
|
733
|
+
};
|
|
734
|
+
let mut b = od.borrow_mut();
|
|
735
|
+
match key {
|
|
736
|
+
Value::Symbol(s) => {
|
|
737
|
+
if b.symbols.is_none() {
|
|
738
|
+
b.symbols = Some(AHashMap::default());
|
|
739
|
+
}
|
|
740
|
+
b.symbols.as_mut().unwrap().insert(s.id, val);
|
|
741
|
+
Ok(())
|
|
742
|
+
}
|
|
743
|
+
Value::Number(n) => {
|
|
744
|
+
b.strings.insert(n.to_string().into(), val);
|
|
745
|
+
Ok(())
|
|
746
|
+
}
|
|
747
|
+
Value::String(k) => {
|
|
748
|
+
b.strings.insert(Arc::from(k.as_str()), val);
|
|
749
|
+
Ok(())
|
|
750
|
+
}
|
|
751
|
+
_ => Err(format!(
|
|
752
|
+
"Object key must be string, number, or symbol, got {}",
|
|
753
|
+
key.type_name()
|
|
754
|
+
)),
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/// `key in obj` for objects.
|
|
759
|
+
pub fn object_has(obj: &Value, key: &Value) -> bool {
|
|
760
|
+
let Value::Object(od) = obj else {
|
|
761
|
+
return false;
|
|
762
|
+
};
|
|
763
|
+
let b = od.borrow();
|
|
764
|
+
match key {
|
|
765
|
+
Value::Symbol(s) => b.symbols.as_ref().is_some_and(|m| m.contains_key(&s.id)),
|
|
766
|
+
Value::Number(n) => {
|
|
767
|
+
let k: Arc<str> = n.to_string().into();
|
|
768
|
+
b.strings.contains_key(&k)
|
|
769
|
+
}
|
|
770
|
+
Value::String(k) => b.strings.contains_key(k.as_ref()),
|
|
771
|
+
_ => false,
|
|
772
|
+
}
|
|
773
|
+
}
|
|
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
|
+
|
|
837
|
+
/// Invoke a callable [`Value`]: [`Value::Function`], or an object exposing `__call` (e.g. `Symbol`).
|
|
838
|
+
pub fn value_call(callee: &Value, args: &[Value]) -> Value {
|
|
839
|
+
match callee {
|
|
840
|
+
Value::Function(f) => f.call(args),
|
|
841
|
+
Value::Object(o) => {
|
|
842
|
+
let inner = o.borrow().strings.get("__call").cloned();
|
|
843
|
+
if let Some(inner) = inner {
|
|
844
|
+
return value_call(&inner, args);
|
|
845
|
+
}
|
|
846
|
+
panic!(
|
|
847
|
+
"Not a function: tried to call {:?} as a function (e.g. method on Null when read failed)",
|
|
848
|
+
callee
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
_ => panic!(
|
|
852
|
+
"Not a function: tried to call {:?} as a function (e.g. method on Null when read failed)",
|
|
853
|
+
callee
|
|
854
|
+
),
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/// Merge two object payloads (spread / VM MergeObject).
|
|
859
|
+
pub fn merge_object_data(left: &VmRef<ObjectData>, right: &VmRef<ObjectData>) -> ObjectData {
|
|
860
|
+
let l = left.borrow();
|
|
861
|
+
let r = right.borrow();
|
|
862
|
+
let mut strings = PropMap::with_capacity(l.strings.len() + r.strings.len());
|
|
863
|
+
strings.extend(l.strings.iter().map(|(k, v)| (Arc::clone(k), v.clone())));
|
|
864
|
+
strings.extend(r.strings.iter().map(|(k, v)| (Arc::clone(k), v.clone())));
|
|
865
|
+
let mut symbols: Option<AHashMap<u64, Value>> = None;
|
|
866
|
+
if let Some(ls) = &l.symbols {
|
|
867
|
+
symbols = Some(ls.clone());
|
|
868
|
+
}
|
|
869
|
+
if let Some(rs) = &r.symbols {
|
|
870
|
+
match &mut symbols {
|
|
871
|
+
Some(m) => {
|
|
872
|
+
m.extend(rs.iter().map(|(k, v)| (*k, v.clone())));
|
|
873
|
+
}
|
|
874
|
+
None => symbols = Some(rs.clone()),
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
ObjectData { strings, symbols }
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
impl std::fmt::Debug for Value {
|
|
881
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
882
|
+
match self {
|
|
883
|
+
Value::Number(n) => write!(f, "Number({})", n),
|
|
884
|
+
Value::String(s) => write!(f, "String({:?})", s.as_str()),
|
|
885
|
+
Value::Bool(b) => write!(f, "Bool({})", b),
|
|
886
|
+
Value::Null => write!(f, "Null"),
|
|
887
|
+
Value::Array(arr) => write!(f, "Array({:?})", arr.borrow()),
|
|
888
|
+
Value::NumberArray(arr) => write!(f, "NumberArray({:?})", arr.borrow()),
|
|
889
|
+
Value::Object(obj) => write!(f, "Object({:?})", obj.borrow()),
|
|
890
|
+
Value::Symbol(s) => write!(f, "Symbol({})", s.id),
|
|
891
|
+
Value::Function(_) => write!(f, "Function"),
|
|
892
|
+
#[cfg(feature = "regex")]
|
|
893
|
+
Value::RegExp(re) => write!(
|
|
894
|
+
f,
|
|
895
|
+
"RegExp(/{}/{})",
|
|
896
|
+
re.borrow().source,
|
|
897
|
+
re.borrow().flags_string()
|
|
898
|
+
),
|
|
899
|
+
Value::Promise(_) => write!(f, "Promise"),
|
|
900
|
+
Value::Opaque(o) => write!(f, "{}(opaque)", o.type_name()),
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
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
|
+
|
|
975
|
+
impl Value {
|
|
976
|
+
/// Convert value to display string (for console output).
|
|
977
|
+
pub fn to_display_string(&self) -> String {
|
|
978
|
+
match self {
|
|
979
|
+
Value::Number(n) => js_number_to_string(*n),
|
|
980
|
+
Value::String(s) => s.to_string(),
|
|
981
|
+
Value::Bool(b) => b.to_string(),
|
|
982
|
+
Value::Null => "null".to_string(),
|
|
983
|
+
Value::Array(arr) => {
|
|
984
|
+
let inner: Vec<String> =
|
|
985
|
+
arr.borrow().iter().map(|v| v.to_display_string()).collect();
|
|
986
|
+
format!("[{}]", inner.join(", "))
|
|
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
|
+
}
|
|
996
|
+
Value::Object(obj) => {
|
|
997
|
+
let inner: Vec<String> = obj
|
|
998
|
+
.borrow()
|
|
999
|
+
.strings
|
|
1000
|
+
.iter()
|
|
1001
|
+
.map(|(k, v)| format!("{}: {}", k.as_ref(), v.to_display_string()))
|
|
1002
|
+
.collect();
|
|
1003
|
+
format!("{{{}}}", inner.join(", "))
|
|
1004
|
+
}
|
|
1005
|
+
Value::Symbol(s) => {
|
|
1006
|
+
if let Some(d) = &s.description {
|
|
1007
|
+
format!("Symbol({})", d)
|
|
1008
|
+
} else {
|
|
1009
|
+
"Symbol()".to_string()
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
Value::Function(_) => "[Function]".to_string(),
|
|
1013
|
+
Value::Promise(_) => "[object Promise]".to_string(),
|
|
1014
|
+
Value::Opaque(o) => format!("[object {}]", o.type_name()),
|
|
1015
|
+
#[cfg(feature = "regex")]
|
|
1016
|
+
Value::RegExp(re) => {
|
|
1017
|
+
let re = re.borrow();
|
|
1018
|
+
format!("/{}/{}", re.source, re.flags_string())
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
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
|
+
|
|
1054
|
+
/// Check if value is truthy (for conditionals).
|
|
1055
|
+
pub fn is_truthy(&self) -> bool {
|
|
1056
|
+
match self {
|
|
1057
|
+
Value::Null => false,
|
|
1058
|
+
Value::Bool(b) => *b,
|
|
1059
|
+
Value::Number(n) => *n != 0.0 && !n.is_nan(),
|
|
1060
|
+
Value::String(s) => !s.is_empty(),
|
|
1061
|
+
_ => true,
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/// Strict equality (===).
|
|
1066
|
+
pub fn strict_eq(&self, other: &Value) -> bool {
|
|
1067
|
+
match (self, other) {
|
|
1068
|
+
(Value::Number(a), Value::Number(b)) => {
|
|
1069
|
+
if a.is_nan() || b.is_nan() {
|
|
1070
|
+
false
|
|
1071
|
+
} else {
|
|
1072
|
+
a == b
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
(Value::String(a), Value::String(b)) => a == b,
|
|
1076
|
+
(Value::Bool(a), Value::Bool(b)) => a == b,
|
|
1077
|
+
(Value::Null, Value::Null) => true,
|
|
1078
|
+
(Value::Array(a), Value::Array(b)) => VmRef::ptr_eq(a, b),
|
|
1079
|
+
(Value::NumberArray(a), Value::NumberArray(b)) => VmRef::ptr_eq(a, b),
|
|
1080
|
+
(Value::Object(a), Value::Object(b)) => VmRef::ptr_eq(a, b),
|
|
1081
|
+
#[cfg(feature = "send-values")]
|
|
1082
|
+
(Value::Function(a), Value::Function(b)) => Arc::ptr_eq(a, b),
|
|
1083
|
+
#[cfg(not(feature = "send-values"))]
|
|
1084
|
+
(Value::Function(a), Value::Function(b)) => std::rc::Rc::ptr_eq(a, b),
|
|
1085
|
+
#[cfg(feature = "regex")]
|
|
1086
|
+
(Value::RegExp(a), Value::RegExp(b)) => VmRef::ptr_eq(a, b),
|
|
1087
|
+
(Value::Promise(a), Value::Promise(b)) => Arc::ptr_eq(a, b),
|
|
1088
|
+
(Value::Opaque(a), Value::Opaque(b)) => Arc::ptr_eq(a, b),
|
|
1089
|
+
(Value::Symbol(a), Value::Symbol(b)) => Arc::ptr_eq(a, b),
|
|
1090
|
+
_ => false,
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/// Wrap a Rust closure in a `Value::Function`. Automatically picks
|
|
1095
|
+
/// `Rc<dyn Fn>` or `Arc<dyn Fn + Send + Sync>` based on the
|
|
1096
|
+
/// `send-values` feature, so callers don't have to `cfg`-gate their
|
|
1097
|
+
/// code. The input bound tracks the feature too: when `send-values`
|
|
1098
|
+
/// is enabled the closure must be `Send + Sync`, otherwise any `Fn`
|
|
1099
|
+
/// is accepted.
|
|
1100
|
+
#[cfg(feature = "send-values")]
|
|
1101
|
+
pub fn native<F>(f: F) -> Self
|
|
1102
|
+
where
|
|
1103
|
+
F: Fn(&[Value]) -> Value + Send + Sync + 'static,
|
|
1104
|
+
{
|
|
1105
|
+
Value::Function(Arc::new(FnCallable(f)))
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
#[cfg(not(feature = "send-values"))]
|
|
1109
|
+
pub fn native<F>(f: F) -> Self
|
|
1110
|
+
where
|
|
1111
|
+
F: Fn(&[Value]) -> Value + 'static,
|
|
1112
|
+
{
|
|
1113
|
+
Value::Function(std::rc::Rc::new(FnCallable(f)))
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
/// Create a new array Value from a Vec.
|
|
1117
|
+
pub fn array(items: Vec<Value>) -> Self {
|
|
1118
|
+
Value::Array(VmRef::new(items))
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
/// Create a new object Value from a property map.
|
|
1122
|
+
pub fn object(map: ObjectMap) -> Self {
|
|
1123
|
+
Value::Object(VmRef::new(ObjectData::from_strings(map)))
|
|
1124
|
+
}
|
|
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
|
+
|
|
1141
|
+
/// Create an empty array Value.
|
|
1142
|
+
pub fn empty_array() -> Self {
|
|
1143
|
+
Value::Array(VmRef::new(Vec::new()))
|
|
1144
|
+
}
|
|
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
|
+
|
|
1186
|
+
/// Create an empty object Value.
|
|
1187
|
+
pub fn empty_object() -> Self {
|
|
1188
|
+
Value::Object(VmRef::new(ObjectData::default()))
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
/// Extract the number value, if this is a Number.
|
|
1192
|
+
pub fn as_number(&self) -> Option<f64> {
|
|
1193
|
+
match self {
|
|
1194
|
+
Value::Number(n) => Some(*n),
|
|
1195
|
+
_ => None,
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/// JavaScript-style typeof string for this value.
|
|
1200
|
+
pub fn type_name(&self) -> &'static str {
|
|
1201
|
+
match self {
|
|
1202
|
+
Value::Number(_) => "number",
|
|
1203
|
+
Value::String(_) => "string",
|
|
1204
|
+
Value::Bool(_) => "boolean",
|
|
1205
|
+
Value::Null => "null",
|
|
1206
|
+
Value::Array(_) | Value::NumberArray(_) => "object",
|
|
1207
|
+
Value::Object(_) => "object",
|
|
1208
|
+
Value::Function(_) => "function",
|
|
1209
|
+
#[cfg(feature = "regex")]
|
|
1210
|
+
Value::RegExp(_) => "object",
|
|
1211
|
+
Value::Promise(_) => "object",
|
|
1212
|
+
Value::Opaque(o) => o.type_name(),
|
|
1213
|
+
Value::Symbol(_) => "symbol",
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/// Property/method names for REPL tab completion (e.g. after `obj.`).
|
|
1218
|
+
pub fn completion_keys(&self) -> Vec<String> {
|
|
1219
|
+
match self {
|
|
1220
|
+
Value::Object(m) => {
|
|
1221
|
+
let mut keys: Vec<String> = m
|
|
1222
|
+
.borrow()
|
|
1223
|
+
.strings
|
|
1224
|
+
.keys()
|
|
1225
|
+
.map(|k| k.to_string())
|
|
1226
|
+
.collect();
|
|
1227
|
+
keys.sort();
|
|
1228
|
+
keys
|
|
1229
|
+
}
|
|
1230
|
+
Value::Array(_) => {
|
|
1231
|
+
vec![
|
|
1232
|
+
"length".into(),
|
|
1233
|
+
"at".into(),
|
|
1234
|
+
"concat".into(),
|
|
1235
|
+
"copyWithin".into(),
|
|
1236
|
+
"entries".into(),
|
|
1237
|
+
"every".into(),
|
|
1238
|
+
"fill".into(),
|
|
1239
|
+
"filter".into(),
|
|
1240
|
+
"find".into(),
|
|
1241
|
+
"findIndex".into(),
|
|
1242
|
+
"findLast".into(),
|
|
1243
|
+
"findLastIndex".into(),
|
|
1244
|
+
"flat".into(),
|
|
1245
|
+
"flatMap".into(),
|
|
1246
|
+
"forEach".into(),
|
|
1247
|
+
"includes".into(),
|
|
1248
|
+
"indexOf".into(),
|
|
1249
|
+
"join".into(),
|
|
1250
|
+
"keys".into(),
|
|
1251
|
+
"lastIndexOf".into(),
|
|
1252
|
+
"map".into(),
|
|
1253
|
+
"pop".into(),
|
|
1254
|
+
"push".into(),
|
|
1255
|
+
"reduce".into(),
|
|
1256
|
+
"reduceRight".into(),
|
|
1257
|
+
"reverse".into(),
|
|
1258
|
+
"shift".into(),
|
|
1259
|
+
"slice".into(),
|
|
1260
|
+
"some".into(),
|
|
1261
|
+
"sort".into(),
|
|
1262
|
+
"splice".into(),
|
|
1263
|
+
"toLocaleString".into(),
|
|
1264
|
+
"toReversed".into(),
|
|
1265
|
+
"toSorted".into(),
|
|
1266
|
+
"toSpliced".into(),
|
|
1267
|
+
"toString".into(),
|
|
1268
|
+
"unshift".into(),
|
|
1269
|
+
"values".into(),
|
|
1270
|
+
"shuffle".into(),
|
|
1271
|
+
]
|
|
1272
|
+
}
|
|
1273
|
+
Value::String(_) => {
|
|
1274
|
+
vec![
|
|
1275
|
+
"length".into(),
|
|
1276
|
+
"charAt".into(),
|
|
1277
|
+
"charCodeAt".into(),
|
|
1278
|
+
"endsWith".into(),
|
|
1279
|
+
"includes".into(),
|
|
1280
|
+
"indexOf".into(),
|
|
1281
|
+
"lastIndexOf".into(),
|
|
1282
|
+
"padEnd".into(),
|
|
1283
|
+
"padStart".into(),
|
|
1284
|
+
"repeat".into(),
|
|
1285
|
+
"replace".into(),
|
|
1286
|
+
"replaceAll".into(),
|
|
1287
|
+
"slice".into(),
|
|
1288
|
+
"split".into(),
|
|
1289
|
+
"startsWith".into(),
|
|
1290
|
+
"substring".into(),
|
|
1291
|
+
"toLowerCase".into(),
|
|
1292
|
+
"toUpperCase".into(),
|
|
1293
|
+
"trim".into(),
|
|
1294
|
+
]
|
|
1295
|
+
}
|
|
1296
|
+
Value::Number(_) => vec![
|
|
1297
|
+
"toFixed".into(),
|
|
1298
|
+
"toExponential".into(),
|
|
1299
|
+
"toPrecision".into(),
|
|
1300
|
+
],
|
|
1301
|
+
_ => vec![],
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
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
|
+
|