@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.
Files changed (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. 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(&regex_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
+